Skip to content

Publish Stable Release #9

Publish Stable Release

Publish Stable Release #9

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>"