Skip to content

Release

Release #2

Workflow file for this run

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
previous_version:
description: "Optional: start tag for release notes (e.g. v3.0.1). Auto-detected from the previous semver tag if left blank."
required: false
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@d23441a48e516b6c34aea4fa41551a30e30af803 # v6.1.0
with:
ssh-key: ${{ secrets.RELEASE_DEPLOY_KEY }}
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@249970729cb0ef3589644e2896645e5dc5ba9c38 # v6.5.0
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: Verify build produced the action entry points
run: |
main_path=$(grep -oE "main:[[:space:]]*'[^']+'" action.yml | grep -oE "'[^']+'" | tr -d "'" | head -1)
post_path=$(grep -oE "post:[[:space:]]*'[^']+'" action.yml | grep -oE "'[^']+'" | tr -d "'" | head -1)
if [ -z "$main_path" ] || [ -z "$post_path" ]; then
echo "::error::could not read main/post entry points from action.yml"; exit 1
fi
if [ ! -f "$main_path" ] || [ ! -f "$post_path" ]; then
echo "::error::build did not produce $main_path and $post_path (from action.yml); refusing to create a release"; exit 1
fi
- 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 }}
PREV_INPUT: ${{ inputs.previous_version }}
run: |
# Pin the release-notes start tag so --generate-notes diffs against the
# correct previous release. Without this, GitHub auto-selects the "previous
# tag" and can pick the wrong one (e.g. the floating major tag v3), which
# dumps the entire history into the notes.
PREV="$PREV_INPUT"
if [[ -z "$PREV" ]]; then
# Previous X.Y.Z tag: sort semver, exclude floating major tags (v3) and $V itself.
PREV=$(git tag -l 'v[0-9]*.[0-9]*.[0-9]*' | sort -V | awk -v v="$V" '$0==v{print prev} {prev=$0}')
fi
echo "Release notes start tag: ${PREV:-<none, first release>}"
# Build the notes-start-tag argument as an array so an empty PREV (first
# release) appends nothing, and a value is passed as a single argument.
notes_args=(--generate-notes)
if [[ -n "$PREV" ]]; then
notes_args+=(--notes-start-tag "$PREV")
fi
# 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" \
"${notes_args[@]}"
open-selfpin-pr:
needs: release
if: ${{ inputs.ref == 'master' }}
runs-on: ubuntu-latest
environment: release
permissions:
contents: write
pull-requests: write
steps:
- uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6.1.0
with:
ssh-key: ${{ secrets.RELEASE_DEPLOY_KEY }}
ref: master
fetch-depth: 0
fetch-tags: true
- name: Bump azure/login self-references and open PR
env:
GH_TOKEN: ${{ github.token }}
V: ${{ inputs.version }}
run: |
SHA=$(git rev-parse "refs/tags/$V^{commit}")
# Rewrite only azure/login self-references. The match starts at "uses:",
# so leading indentation is preserved and other actions are left untouched.
for f in .github/workflows/*.yml .github/workflows/*.yaml; do
[ -e "$f" ] || continue
sed -i -E "s|(uses:[[:space:]]+azure/login@)[^[:space:]]+.*|\1${SHA} # ${V}|" "$f"
done
if git diff --quiet; then
echo "No azure/login self-references to update."
exit 0
fi
BRANCH="chore/pin-azure-login-$V"
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
git checkout -B "$BRANCH"
git commit .github/workflows -m "chore: pin azure/login self-references to $V"
# Force-push: this version-specific bot branch is safe to overwrite if a
# prior run left it behind, and there is no other writer to protect.
git push --force --set-upstream origin "$BRANCH"
# Tolerate an existing PR for this branch (e.g. on a re-run) without failing.
gh pr create --base master --head "$BRANCH" \
--title "Pin azure/login self-references to $V" \
--body "Automated post-release update: repoints \`azure/login@…\` in workflow files to the built release commit \`$SHA\` (\`$V\`)." \
|| echo "PR for $BRANCH may already exist; skipping create."