Publish Stable Release #9
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: Publish Stable Release | |
| on: | |
| workflow_dispatch: | |
| inputs: | |
| release_pr: | |
| description: Prepare release PR number to publish | |
| required: true | |
| type: string | |
| concurrency: | |
| group: publish-stable-release-${{ inputs.release_pr }} | |
| cancel-in-progress: false | |
| env: | |
| HUSKY: "0" | |
| NPM_CONFIG_PROVENANCE: "true" | |
| jobs: | |
| print-changelog-links: | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 5 | |
| permissions: | |
| contents: read | |
| pull-requests: read | |
| steps: | |
| - name: Print changelog links | |
| env: | |
| GH_TOKEN: ${{ github.token }} | |
| GH_REPO: ${{ github.repository }} | |
| RELEASE_PR: ${{ inputs.release_pr }} | |
| GITHUB_SERVER_URL: ${{ github.server_url }} | |
| run: | | |
| set -euo pipefail | |
| if [ -z "$RELEASE_PR" ]; then | |
| echo "Stable releases must provide the prepare release PR number in release_pr." >&2 | |
| exit 1 | |
| fi | |
| gh pr view "$RELEASE_PR" \ | |
| --repo "$GH_REPO" \ | |
| --json headRefOid,url \ | |
| > changelog-pr.json | |
| changelog_pr_url="$(jq -r '.url' changelog-pr.json)" | |
| head_sha="$(jq -r '.headRefOid' changelog-pr.json)" | |
| latest_release_tag="$(gh api "repos/$GH_REPO/releases/latest" --jq '.tag_name')" | |
| encoded_latest_release_tag="$(jq -rn --arg tag "$latest_release_tag" '$tag|@uri')" | |
| diff_url="$GITHUB_SERVER_URL/$GH_REPO/compare/$encoded_latest_release_tag...$head_sha" | |
| { | |
| echo "Changelog PR: $changelog_pr_url" | |
| echo "Diff to latest release: $diff_url" | |
| } | tee -a "$GITHUB_STEP_SUMMARY" | |
| publish-stable: | |
| needs: | |
| - print-changelog-links | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 30 | |
| permissions: | |
| contents: write | |
| issues: write | |
| id-token: write | |
| pull-requests: write | |
| environment: npm-publish | |
| steps: | |
| - name: Resolve release PR | |
| id: release_pr | |
| env: | |
| GH_TOKEN: ${{ github.token }} | |
| GH_REPO: ${{ github.repository }} | |
| RELEASE_PR: ${{ inputs.release_pr }} | |
| run: | | |
| set -euo pipefail | |
| if [ -z "$RELEASE_PR" ]; then | |
| echo "Stable releases must provide the prepare release PR number in release_pr." >&2 | |
| exit 1 | |
| fi | |
| gh pr view "$RELEASE_PR" \ | |
| --repo "$GH_REPO" \ | |
| --json number,state,baseRefName,body,headRefName,headRefOid,isCrossRepository,url \ | |
| > release-pr.json | |
| number="$(jq -r '.number' release-pr.json)" | |
| state="$(jq -r '.state' release-pr.json)" | |
| base_ref="$(jq -r '.baseRefName' release-pr.json)" | |
| head_ref="$(jq -r '.headRefName' release-pr.json)" | |
| head_sha="$(jq -r '.headRefOid' release-pr.json)" | |
| is_cross_repository="$(jq -r '.isCrossRepository' release-pr.json)" | |
| url="$(jq -r '.url' release-pr.json)" | |
| source_main_sha="$( | |
| jq -r '.body // ""' release-pr.json | | |
| sed -nE 's/^Source-main-sha:[[:space:]]*([0-9a-f]{7,40})$/\1/p' | | |
| head -n 1 | |
| )" | |
| if [ "$state" != "OPEN" ]; then | |
| echo "Release PR #$number must be open. Current state: $state." >&2 | |
| exit 1 | |
| fi | |
| if [ "$base_ref" != "main" ]; then | |
| echo "Release PR #$number must target main. Current base: $base_ref." >&2 | |
| exit 1 | |
| fi | |
| if [ "$is_cross_repository" != "false" ]; then | |
| echo "Release PR #$number must come from this repository." >&2 | |
| exit 1 | |
| fi | |
| case "$head_ref" in | |
| prepare-release/*) ;; | |
| *) | |
| echo "Release PR #$number must use a prepare-release/* branch. Current head: $head_ref." >&2 | |
| exit 1 | |
| ;; | |
| esac | |
| if [ -z "$source_main_sha" ]; then | |
| echo "Release PR #$number is missing Source-main-sha in the body." >&2 | |
| exit 1 | |
| fi | |
| { | |
| echo "number=$number" | |
| echo "head_ref=$head_ref" | |
| echo "head_sha=$head_sha" | |
| echo "source_main_sha=$source_main_sha" | |
| echo "url=$url" | |
| } >> "$GITHUB_OUTPUT" | |
| - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 | |
| with: | |
| fetch-depth: 0 | |
| ref: ${{ steps.release_pr.outputs.head_sha }} | |
| - uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v5.0.0 | |
| - uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 | |
| with: | |
| node-version-file: .tool-versions | |
| cache: pnpm | |
| registry-url: https://registry.npmjs.org | |
| - name: Install dependencies | |
| run: pnpm install --frozen-lockfile | |
| - name: Validate package metadata | |
| run: node scripts/release/validate-publishable-packages.mjs | |
| - name: Detect stable publish work | |
| id: detect | |
| run: node scripts/release/release-manifest.mjs --mode stable --since "${{ steps.release_pr.outputs.source_main_sha }}" --output .release-manifest.json | |
| - name: Build packages | |
| if: steps.detect.outputs.needs_publish == 'true' | |
| run: pnpm run build | |
| - name: Publish stable packages to npm | |
| if: steps.detect.outputs.needs_publish == 'true' | |
| env: | |
| NODE_AUTH_TOKEN: "" | |
| NPM_TOKEN: "" | |
| run: node scripts/release/publish-release-manifest.mjs --manifest .release-manifest.json | |
| - name: Push Changesets release tags | |
| if: steps.detect.outputs.has_work == 'true' | |
| run: | | |
| set -euo pipefail | |
| git config user.name "github-actions[bot]" | |
| git config user.email "41898282+github-actions[bot]@users.noreply.github.com" | |
| target_commit="$(jq -r '.commit // empty' .release-manifest.json)" | |
| mapfile -t tags < <( | |
| jq -r '.packages[]? | .tag // "\(.name)@\(.version)"' .release-manifest.json | |
| ) | |
| if [ "${#tags[@]}" -eq 0 ]; then | |
| echo "No release tags to push." | |
| exit 0 | |
| fi | |
| if [ -z "$target_commit" ]; then | |
| echo "Release manifest is missing commit." >&2 | |
| exit 1 | |
| fi | |
| to_push=() | |
| for tag in "${tags[@]}"; do | |
| remote_ref="$(git ls-remote --tags origin "refs/tags/$tag" | head -n 1 || true)" | |
| if [ -n "$remote_ref" ]; then | |
| remote_target="$(printf '%s\n' "$remote_ref" | awk '{print $1}')" | |
| if [ "$remote_target" != "$target_commit" ]; then | |
| echo "Remote tag $tag already exists on $remote_target, expected $target_commit." >&2 | |
| exit 1 | |
| fi | |
| continue | |
| fi | |
| if git rev-parse -q --verify "refs/tags/$tag" >/dev/null 2>&1; then | |
| local_target="$(git rev-list -n 1 "$tag")" | |
| if [ "$local_target" != "$target_commit" ]; then | |
| echo "Local tag $tag already exists on $local_target, expected $target_commit." >&2 | |
| exit 1 | |
| fi | |
| else | |
| git tag "$tag" "$target_commit" | |
| fi | |
| to_push+=("refs/tags/$tag") | |
| done | |
| if [ "${#to_push[@]}" -eq 0 ]; then | |
| echo "All release tags already exist on origin." | |
| exit 0 | |
| fi | |
| git push origin "${to_push[@]}" | |
| - name: Create GitHub Releases | |
| if: steps.detect.outputs.has_work == 'true' | |
| env: | |
| GITHUB_TOKEN: ${{ github.token }} | |
| run: node scripts/release/create-github-releases.mjs --manifest .release-manifest.json | |
| - name: Comment on issues closed by released PRs | |
| if: steps.detect.outputs.has_work == 'true' | |
| env: | |
| GITHUB_TOKEN: ${{ github.token }} | |
| run: node scripts/release/comment-release-issues.mjs | |
| - name: Auto-merge release PR | |
| if: steps.detect.outputs.has_work == 'true' | |
| env: | |
| GH_TOKEN: ${{ github.token }} | |
| GH_REPO: ${{ github.repository }} | |
| run: | | |
| set -euo pipefail | |
| if gh pr merge "${{ steps.release_pr.outputs.number }}" \ | |
| --repo "$GH_REPO" \ | |
| --auto \ | |
| --squash \ | |
| --match-head-commit "${{ steps.release_pr.outputs.head_sha }}"; then | |
| echo "Release PR auto-merge requested." | |
| else | |
| echo "::warning::Published packages, but could not auto-merge release PR #${{ steps.release_pr.outputs.number }}." | |
| fi | |
| - name: Post stable release to Slack | |
| if: steps.detect.outputs.has_work == 'true' | |
| uses: slackapi/slack-github-action@af78098f536edbc4de71162a307590698245be95 # v3.0.1 | |
| with: | |
| method: chat.postMessage | |
| token: ${{ secrets.SLACK_BOT_TOKEN }} | |
| payload: | | |
| channel: C0ABHT0SWA2 | |
| text: "✅ Packages published" | |
| blocks: | |
| - type: "header" | |
| text: | |
| type: "plain_text" | |
| text: "✅ Packages published" | |
| - type: "section" | |
| text: | |
| type: "mrkdwn" | |
| text: "*Release PR:* <${{ steps.release_pr.outputs.url }}|#${{ steps.release_pr.outputs.number }}>\n\n*Packages:*\n${{ steps.detect.outputs.markdown }}\n\n<${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}|View Run>" | |
| notify-failure: | |
| needs: | |
| - print-changelog-links | |
| - publish-stable | |
| if: | | |
| always() && | |
| ( | |
| needs.print-changelog-links.result == 'failure' || | |
| needs.publish-stable.result == 'failure' | |
| ) | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 5 | |
| steps: | |
| - name: Post to Slack on failure | |
| uses: slackapi/slack-github-action@af78098f536edbc4de71162a307590698245be95 # v3.0.1 | |
| with: | |
| method: chat.postMessage | |
| token: ${{ secrets.SLACK_BOT_TOKEN }} | |
| payload: | | |
| channel: C0ABHT0SWA2 | |
| text: "🚨 Stable release failed" | |
| blocks: | |
| - type: "header" | |
| text: | |
| type: "plain_text" | |
| text: "🚨 Stable release failed" | |
| - type: "section" | |
| text: | |
| type: "mrkdwn" | |
| text: "*Workflow:* `Publish Stable Release`\n*Release PR:* `${{ inputs.release_pr }}`\n\n<${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}|View Run>" |