Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
145 changes: 145 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
name: Release

# Manually triggered by a maintainer. Reproduces the Azure Login Action
# release runbook: build -> commit built lib to a release branch -> tag ->
# publish GitHub Release -> move the major tag (e.g. v3).
#
# ADMIN-ONLY: GitHub cannot restrict workflow_dispatch to admins at the trigger
# level (any user with write access can dispatch). Admin-only is enforced two ways:
# 1. environment: release -> requires an admin reviewer to approve the run
# (configure required reviewers under Settings > Environments > release).
# This is the real gate: the destructive steps cannot run without approval.
# 2. The "Ensure triggered by an admin" step below fails fast for non-admins.
on:
workflow_dispatch:
inputs:
version:
description: "Release version, e.g. v3.1.0"
required: true
ref:
description: "Branch to release from. Use master for the current major (v3). For a back-major release (e.g. v2), branch a hotfix off the latest release tag (git checkout -b hotfix/v2.3.2 v2.3.1) and pass that hotfix/* branch."
required: false
default: master

permissions:
contents: write # push branch/tag, force-move major tag, create the Release

# Serialize releases: never let two release runs push tags/branches at once.
concurrency:
group: release
cancel-in-progress: false

jobs:
release:
runs-on: ubuntu-latest
# Admin gate: this environment must have required reviewers (admins) configured
# in repo settings. The run pauses here until an admin approves.
environment: release
steps:
- name: Ensure triggered by an admin
env:
GH_TOKEN: ${{ github.token }}
REPO: ${{ github.repository }}
ACTOR: ${{ github.actor }}
run: |
PERM=$(gh api "repos/$REPO/collaborators/$ACTOR/permission" --jq '.permission')
echo "Actor '$ACTOR' has repository permission: $PERM"
if [[ "$PERM" != "admin" ]]; then
echo "::error::Release must be triggered by a repository admin (actor has '$PERM')."
exit 1
fi

- name: Validate version input
env:
V: ${{ inputs.version }}
run: |
if [[ ! "$V" =~ ^v[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
echo "::error::version must look like v3.1.0 (got '$V')"
exit 1
fi

- name: Validate release ref
env:
REF: ${{ inputs.ref }}
run: |
# Only allow releasing from vetted lines: master (current major) and
# the v2 maintenance branch. Prevents shipping arbitrary unreviewed
# branches straight to consumers via the moved major tag.
case "$REF" in
master|hotfix/*) echo "ref '$REF' allowed" ;;
*) echo "::error::ref '$REF' not permitted for release (allowed: master, or a hotfix/* branch based on the latest release tag for a back-major release)"; exit 1 ;;
esac

- uses: actions/checkout@v6
with:
ref: ${{ inputs.ref }} # master for the current major; a hotfix/* branch for a back-major release
fetch-depth: 0
fetch-tags: true

- name: Ensure version is new
env:
V: ${{ inputs.version }}
run: |
if git ls-remote --exit-code --tags origin "refs/tags/$V" >/dev/null 2>&1; then
echo "::error::tag $V already exists"; exit 1
fi
if git ls-remote --exit-code --heads origin "refs/heads/releases/$V" >/dev/null 2>&1; then
echo "::error::branch releases/$V already exists"; exit 1
fi

- name: Determine runtime node from action.yml
id: node
shell: bash
run: |
ver=$(grep "using:" action.yml | grep -oE "node[0-9]+" | grep -oE "[0-9]+" | head -1)
echo "version=$ver" >> "$GITHUB_OUTPUT"

- uses: actions/setup-node@v6
with:
node-version: ${{ steps.node.outputs.version }} # matches action.yml runs.using
cache: npm

- name: Install, build, test
run: |
npm ci
npm run build
npm test

- name: Create release branch with built lib
env:
V: ${{ inputs.version }}
run: |
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
git checkout -b "releases/$V"
git add -f lib
git commit -m "prepare release $V"
git push --set-upstream origin "releases/$V"

- name: Create and push tag
env:
V: ${{ inputs.version }}
run: |
git tag -a -m "$V" "$V"
git push origin "$V"

- name: Move major tag (e.g. v3)
env:
V: ${{ inputs.version }}
run: |
MAJOR="${V%%.*}" # v3.1.0 -> v3
git tag -d "$MAJOR" || true
git tag -a "$MAJOR" -m "Update $MAJOR to $V"
git push origin "$MAJOR" -f

- name: Publish GitHub Release
env:
GH_TOKEN: ${{ github.token }}
V: ${{ inputs.version }}
run: |
# No --latest flag: GitHub automatically marks the highest-version
# release as "Latest", so a back-major release (e.g. a v2 hotfix) does
# not steal the badge from the current major.
gh release create "$V" \
--title "Azure Login Action $V" \
--generate-notes
109 changes: 109 additions & 0 deletions .github/workflows/rollback.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
name: Rollback

# Manually triggered by a maintainer to undo a bad release. A release moves the
# major tag (e.g. v3) forward to the new version; consumers pinned to
# `azure/login@v3` immediately get it. If that release is broken, rollback
# re-points the major tag back to a known-good, already-published version tag
# (e.g. v3.0.1), which contains the built lib/. This is the fast consumer-facing
# fix. It does NOT delete the bad tag/branch/release by default - deletion is
# destructive (someone may have pinned the exact version) and is left to a
# deliberate manual step if ever needed.
#
# ADMIN-ONLY: enforced the same two ways as release.yml:
# 1. environment: release -> requires an admin reviewer to approve the run.
# 2. The "Ensure triggered by an admin" step fails fast for non-admins.
on:
workflow_dispatch:
inputs:
target_version:
description: "Known-good version to roll the major tag back to, e.g. v3.0.1 (must be an existing tag)"
required: true

permissions:
contents: write # force-move the major tag, update the Release

# Share the 'release' concurrency group so a rollback can never race an
# in-flight release (both push the same major tag).
concurrency:
group: release
cancel-in-progress: false

jobs:
rollback:
runs-on: ubuntu-latest
# Admin gate: this environment must have required reviewers (admins)
# configured in repo settings. The run pauses here until an admin approves.
environment: release
steps:
- name: Ensure triggered by an admin
env:
GH_TOKEN: ${{ github.token }}
REPO: ${{ github.repository }}
ACTOR: ${{ github.actor }}
run: |
PERM=$(gh api "repos/$REPO/collaborators/$ACTOR/permission" --jq '.permission')
echo "Actor '$ACTOR' has repository permission: $PERM"
if [[ "$PERM" != "admin" ]]; then
echo "::error::Rollback must be triggered by a repository admin (actor has '$PERM')."
exit 1
fi

- name: Validate target_version input
env:
V: ${{ inputs.target_version }}
run: |
if [[ ! "$V" =~ ^v[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
echo "::error::target_version must look like v3.0.1 (got '$V')"
exit 1
fi

- uses: actions/checkout@v6
with:
fetch-depth: 0
fetch-tags: true

- name: Verify target tag exists and contains built lib
env:
V: ${{ inputs.target_version }}
run: |
# The target must be an existing published version tag...
if ! git rev-parse -q --verify "refs/tags/$V" >/dev/null; then
echo "::error::tag $V does not exist - can only roll back to an already-published version"
exit 1
fi
# ...and it must carry the built entry points (main + post), or
# consumers would break worse than before (the major tag must always
# point at runnable code). action.yml uses lib/main/index.js and
# lib/cleanup/index.js.
git checkout --quiet "$V"
missing=""
[[ -f lib/main/index.js ]] || missing="$missing lib/main/index.js"
[[ -f lib/cleanup/index.js ]] || missing="$missing lib/cleanup/index.js"
if [[ -n "$missing" ]]; then
echo "::error::tag $V is missing built entry point(s):$missing - refusing to point the major tag at unbuilt code"
exit 1
fi

- name: Re-point major tag to target
env:
V: ${{ inputs.target_version }}
run: |
MAJOR="${V%%.*}" # v3.0.1 -> v3
CURRENT=$(git rev-parse "refs/tags/$MAJOR^{commit}" 2>/dev/null || echo "none")
TARGET=$(git rev-parse "$V^{commit}")
echo "Rolling major $MAJOR: $CURRENT -> $TARGET ($V)"
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
git tag -d "$MAJOR" || true
git tag -a "$MAJOR" -m "Rollback $MAJOR to $V" "$V"
git push origin "$MAJOR" -f

- name: Summary
env:
V: ${{ inputs.target_version }}
run: |
MAJOR="${V%%.*}"
echo "### Rollback complete" >> "$GITHUB_STEP_SUMMARY"
echo "- Major tag \`$MAJOR\` now points at \`$V\`" >> "$GITHUB_STEP_SUMMARY"
echo "- Consumers using \`azure/login@$MAJOR\` now get \`$V\`" >> "$GITHUB_STEP_SUMMARY"
echo "- The bad tag/branch/release were NOT deleted (delete manually if required)." >> "$GITHUB_STEP_SUMMARY"
Loading