Repository navigation
Release #2
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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." |