Skip to content

Publish Stable Release #18

Publish Stable Release

Publish Stable Release #18

name: Publish Stable Release
on:
workflow_dispatch:
inputs:
release_sha:
description: Full commit SHA to publish
required: true
type: string
concurrency:
group: publish-stable-release-${{ inputs.release_sha }}
cancel-in-progress: false
env:
HUSKY: "0"
NPM_CONFIG_PROVENANCE: "true"
jobs:
validate-release-sha:
runs-on: ubuntu-latest
timeout-minutes: 5
outputs:
release_sha: ${{ steps.release.outputs.release_sha }}
steps:
- name: Validate release SHA
id: release
env:
RELEASE_SHA: ${{ inputs.release_sha }}
run: |
set -euo pipefail
if [ -z "$RELEASE_SHA" ]; then
echo "Stable releases must provide the commit SHA to publish in release_sha." >&2
exit 1
fi
if ! printf '%s\n' "$RELEASE_SHA" | grep -Eq '^[0-9a-fA-F]{40}$'; then
echo "release_sha must be the full 40-character commit SHA." >&2
exit 1
fi
release_sha="$(printf '%s\n' "$RELEASE_SHA" | tr '[:upper:]' '[:lower:]')"
echo "release_sha=$release_sha" >> "$GITHUB_OUTPUT"
print-changelog-links:
needs:
- validate-release-sha
runs-on: ubuntu-latest
timeout-minutes: 5
permissions:
contents: read
steps:
- name: Print changelog links
env:
GH_TOKEN: ${{ github.token }}
GH_REPO: ${{ github.repository }}
RELEASE_SHA: ${{ needs.validate-release-sha.outputs.release_sha }}
GITHUB_SERVER_URL: ${{ github.server_url }}
run: |
set -euo pipefail
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...$RELEASE_SHA"
{
echo "Release SHA: $RELEASE_SHA"
echo "Diff to latest release: $diff_url"
} | tee -a "$GITHUB_STEP_SUMMARY"
publish-stable:
needs:
- validate-release-sha
- print-changelog-links
runs-on: ubuntu-latest
timeout-minutes: 30
permissions:
contents: write
issues: write
id-token: write
pull-requests: read
environment: npm-publish
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
fetch-depth: 0
ref: ${{ needs.validate-release-sha.outputs.release_sha }}
- name: Validate release checkout
env:
RELEASE_SHA: ${{ needs.validate-release-sha.outputs.release_sha }}
run: |
set -euo pipefail
checked_out_sha="$(git rev-parse HEAD | tr '[:upper:]' '[:lower:]')"
if [ "$checked_out_sha" != "$RELEASE_SHA" ]; then
echo "Checked out $checked_out_sha, expected $RELEASE_SHA." >&2
exit 1
fi
- uses: pnpm/action-setup@0ebf47130e4866e96fce0953f49152a61190b271 # v6.0.9
- 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 --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: 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 SHA:* `${{ needs.validate-release-sha.outputs.release_sha }}`\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:
- validate-release-sha
- print-changelog-links
- publish-stable
if: |
always() &&
(
needs.validate-release-sha.result == 'failure' ||
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 SHA:* `${{ inputs.release_sha }}`\n\n<${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}|View Run>"