Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions .github/workflows/prepare-release.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -72,8 +72,10 @@ jobs:
fi

git commit -m "chore: Prepare release"
release_sha="$(git rev-parse HEAD)"
git push origin HEAD:refs/heads/${{ steps.source.outputs.prepare_branch }}
echo "has_changes=true" >> "$GITHUB_OUTPUT"
echo "release_sha=$release_sha" >> "$GITHUB_OUTPUT"
- name: Summarize empty release
if: steps.release_commit.outputs.has_changes == 'false'
run: |
Expand All @@ -98,7 +100,6 @@ jobs:
--head "${{ steps.source.outputs.prepare_branch }}" \
--title "chore: Prepare release (${{ steps.source.outputs.short_main_sha }})" \
--body-file release-pr-body.md)"
pr_number="${pr_url##*/}"

{
echo "## Prepare release"
Expand All @@ -108,6 +109,6 @@ jobs:
echo "To publish this release, run the \`Publish Stable Release\` workflow with:"
echo
echo "\`\`\`text"
echo "release_pr: $pr_number"
echo "release_sha: ${{ steps.release_commit.outputs.release_sha }}"
echo "\`\`\`"
} >> "$GITHUB_STEP_SUMMARY"
156 changes: 55 additions & 101 deletions .github/workflows/publish-stable-release.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -3,140 +3,109 @@ name: Publish Stable Release
on:
workflow_dispatch:
inputs:
release_pr:
description: Prepare release PR number to publish
release_sha:
description: Full commit SHA to publish
required: true
type: string

concurrency:
group: publish-stable-release-${{ inputs.release_pr }}
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
pull-requests: read
steps:
- name: Print changelog links
env:
GH_TOKEN: ${{ github.token }}
GH_REPO: ${{ github.repository }}
RELEASE_PR: ${{ inputs.release_pr }}
RELEASE_SHA: ${{ needs.validate-release-sha.outputs.release_sha }}
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"
diff_url="$GITHUB_SERVER_URL/$GH_REPO/compare/$encoded_latest_release_tag...$RELEASE_SHA"

{
echo "Changelog PR: $changelog_pr_url"
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: write
pull-requests: read
environment: npm-publish
steps:
- name: Resolve release PR
id: release_pr
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
fetch-depth: 0
ref: ${{ needs.validate-release-sha.outputs.release_sha }}
- name: Resolve release commits
id: release
env:
GH_TOKEN: ${{ github.token }}
GH_REPO: ${{ github.repository }}
RELEASE_PR: ${{ inputs.release_pr }}
RELEASE_SHA: ${{ needs.validate-release-sha.outputs.release_sha }}
run: |
set -euo pipefail

if [ -z "$RELEASE_PR" ]; then
echo "Stable releases must provide the prepare release PR number in release_pr." >&2
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

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
first_parent="$(git rev-parse --verify HEAD^1 2>/dev/null || true)"
if [ -z "$first_parent" ]; then
echo "release_sha must identify a commit with at least one parent." >&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 }}
echo "since_sha=$first_parent" >> "$GITHUB_OUTPUT"
- uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v5.0.0
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
with:
Expand All @@ -149,7 +118,7 @@ jobs:
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
run: node scripts/release/release-manifest.mjs --mode stable --since "${{ steps.release.outputs.since_sha }}" --output .release-manifest.json
- name: Build packages
if: steps.detect.outputs.needs_publish == 'true'
run: pnpm run build
Expand Down Expand Up @@ -223,23 +192,6 @@ jobs:
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
Expand All @@ -257,15 +209,17 @@ jobs:
- 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>"
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'
)
Expand All @@ -288,4 +242,4 @@ jobs:
- 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>"
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>"
26 changes: 14 additions & 12 deletions PUBLISHING.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,11 @@ All package publishing happens in GitHub Actions. Do not publish from your local
- Merge the PR into `main`.
- For a stable release:
- Run **Prepare Release** on `main`.
- Review the generated prepare release PR, but do not merge it manually.
- Run **Publish Stable Release** with the `release_pr` number shown in the prepare workflow summary.
- Review the generated prepare release PR.
- Run **Publish Stable Release** with the `release_sha` shown in the prepare workflow summary.
- Approve the `npm-publish` environment.
- The publish workflow publishes from the exact prepare PR head commit, creates tags and GitHub Releases for that commit, then attempts to squash-merge the prepare PR into `main`.
- The publish workflow publishes from the exact commit SHA, creates tags and GitHub Releases for that commit, and comments on issues closed by released PRs.
- Merge the prepare release PR after publishing succeeds.
- For a prerelease from a branch:
- Run **Publish Prerelease Snapshot** with `ref=<branch, tag, or SHA>`.
- Install from the `rc` tag, for example `npm install braintrust@rc`.
Expand Down Expand Up @@ -50,23 +51,24 @@ This is the normal production release flow.
- runs `changeset version`
- creates a branch named `prepare-release/{short-main-sha}`
- opens a prepare release PR from that branch into `main`
- writes the `release_pr` number to the workflow summary
- writes the `release_sha` to the workflow summary
4. Review the prepare release PR to confirm the package versions, changelogs, and consumed changeset deletions look right.
5. Run **Publish Stable Release** with:
- `release_pr=<prepare release PR number>`
- `release_sha=<full commit SHA to publish>`
6. Approve the `npm-publish` environment when GitHub asks.
7. Merge the prepare release PR after publishing succeeds.

The stable publish workflow checks out the exact prepare PR head SHA. It does not publish from the current tip of `main`.
The stable publish workflow checks out `release_sha` exactly. It accepts any full commit SHA with a parent, does not resolve the prepare release PR, and does not publish from the current tip of `main`.

The workflow then:

- detects packages whose versions changed between the recorded `Source-main-sha` and the prepare PR head
- detects packages whose versions changed between the release commit and its first parent
- publishes any of those package versions that are not already on npm
- pushes Changesets-style release tags for the prepare PR head commit
- pushes Changesets-style release tags for the release commit
- creates GitHub Releases
- attempts to squash-merge the prepare release PR into `main` with `--match-head-commit`
- comments on issues closed by PRs included in the release

If publishing succeeds but the PR cannot be auto-merged, the release is still published. Merge or repair the prepare release PR manually afterward.
If publishing succeeds but the prepare release PR cannot be merged cleanly afterward, the release is still published. Repair or recreate the prepare release PR manually afterward.

Stable releases publish to the npm `latest` dist-tag.

Expand Down Expand Up @@ -95,6 +97,6 @@ Prereleases:
## Notes

- **Prepare Release** must be run from `main`.
- Stable publishing must use a prepare release PR created by **Prepare Release**.
- Stable publishing must use a full 40-character commit SHA, not a PR number. In the normal prepare release flow, use the `release_sha` from the **Prepare Release** summary.
- The old `release` branch and backsync workflow are no longer part of the release process.
- Re-running **Publish Stable Release** for the same `release_pr` is safe after a partial failure. Already-published npm versions are skipped, but tags and GitHub Releases are still reconciled for the prepared release set.
- Re-running **Publish Stable Release** for the same `release_sha` is safe after a partial failure. Already-published npm versions are skipped, but tags and GitHub Releases are still reconciled for the prepared release set.
Loading