Skip to content

Publish Stable Release #22

Publish Stable Release

Publish Stable Release #22

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"
# sdk-actions PR #64 merge commit. Keep release action references immutable.
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"
build-package-artifacts:
needs:
- validate-release-sha
- print-changelog-links
runs-on: ubuntu-latest
timeout-minutes: 30
permissions:
contents: read
id-token: write
attestations: write
outputs:
has_work: ${{ steps.detect.outputs.has_work }}
manifest_json: ${{ steps.detect.outputs.manifest_json }}
release_sha: ${{ needs.validate-release-sha.outputs.release_sha }}
branch: ${{ steps.release-context.outputs.branch }}
on_release_branch: ${{ steps.release-context.outputs.on_release_branch }}
# Displayed in the approval summary so reviewers can identify the selected SHA.
commit_message: ${{ steps.release-context.outputs.commit_message }}
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
- name: Read release context
id: release-context
run: |
set -euo pipefail
containing_branches="$(
git branch -r --contains HEAD --format='%(refname:short)' |
sed '/^origin\/HEAD$/d; s|^origin/||'
)"
branch="${containing_branches%%$'\n'*}"
branch="${branch:-unknown}"
on_release_branch=false
case "$branch" in
main | prepare-release/*) on_release_branch=true ;;
esac
{
echo "branch=$branch"
echo "on_release_branch=$on_release_branch"
echo "commit_message=$(git log -1 --format='%s' HEAD)"
} >> "$GITHUB_OUTPUT"
- 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_artifacts == 'true'
run: pnpm run build
- name: Pack packages and generate SBOMs
if: steps.detect.outputs.needs_artifacts == 'true'
run: node scripts/release/pack-publishable-packages.mjs --manifest .release-manifest.json --output-dir artifacts/release-packages --report artifacts/release-packages/pack-report.json
- name: Stage release manifest
if: steps.detect.outputs.needs_artifacts == 'true'
run: cp .release-manifest.json artifacts/release-packages/release-manifest.json
- name: Attest tarball build provenance
if: steps.detect.outputs.needs_artifacts == 'true'
uses: actions/attest-build-provenance@0f67c3f4856b2e3261c31976d6725780e5e4c373 # v4.1.1
with:
subject-path: artifacts/release-packages/*.tgz
- name: Upload release package artifacts
if: steps.detect.outputs.needs_artifacts == 'true'
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: stable-release-packages
path: artifacts/release-packages/
retention-days: 1
if-no-files-found: error
attest-package-sboms:
name: Attest SBOM (${{ matrix.package.name }})
needs:
- build-package-artifacts
if: needs.build-package-artifacts.outputs.has_work == 'true'
strategy:
matrix:
package: ${{ fromJSON(needs.build-package-artifacts.outputs.manifest_json).packages }}
runs-on: ubuntu-latest
timeout-minutes: 10
permissions:
contents: read
id-token: write
attestations: write
steps:
- name: Download release package artifacts
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
name: stable-release-packages
path: release-artifacts
- name: Attest package SBOM
uses: actions/attest@508db95dd578ae2727ebd6217d5ba78e4fbda05d # v4.2.1
with:
subject-path: release-artifacts/${{ matrix.package.tarball_asset }}
sbom-path: release-artifacts/${{ matrix.package.sbom_asset }}
request-approval:
needs:
- build-package-artifacts
- attest-package-sboms
if: needs.build-package-artifacts.outputs.has_work == 'true'
runs-on: ubuntu-latest
timeout-minutes: 5
permissions: {}
steps:
- name: Request stable release approval
uses: braintrustdata/sdk-actions/actions/release/request-approval@337c5475631d724441403f5a4d8ad0b32ce12e29
with:
packages: ${{ needs.build-package-artifacts.outputs.manifest_json }}
title: Braintrust JavaScript SDK stable release
sha: ${{ needs.build-package-artifacts.outputs.release_sha }}
branch: ${{ needs.build-package-artifacts.outputs.branch }}
on_release_branch: ${{ needs.build-package-artifacts.outputs.on_release_branch }}
commit_message: ${{ needs.build-package-artifacts.outputs.commit_message }}
slack_token: ${{ secrets.SLACK_BOT_TOKEN }}
slack_channel: C0ABHT0SWA2
publish-stable:
needs:
- build-package-artifacts
- request-approval
if: |
always() &&
needs.build-package-artifacts.outputs.has_work == 'true' &&
needs.request-approval.result == 'success'
runs-on: ubuntu-latest
timeout-minutes: 30
permissions:
contents: write
issues: write
id-token: write
pull-requests: read
attestations: read
environment: npm-publish
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
fetch-depth: 0
ref: ${{ needs.build-package-artifacts.outputs.release_sha }}
- name: Download release package artifacts
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
name: stable-release-packages
path: release-artifacts
- name: Publish stable package tarballs
uses: braintrustdata/sdk-actions/actions/release/lang/js/publish-npm-tarballs@337c5475631d724441403f5a4d8ad0b32ce12e29
with:
manifest: release-artifacts/release-manifest.json
tarballs: release-artifacts/*.tgz
repo: ${{ github.repository }}
slack_token: ${{ secrets.SLACK_BOT_TOKEN }}
slack_channel: C0ABHT0SWA2
- name: Push Changesets release tags
run: |
set -euo pipefail
git config user.name "github-actions[bot]"
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
manifest="release-artifacts/release-manifest.json"
target_commit="$(jq -r '.commit // empty' "$manifest")"
mapfile -t tags < <(
jq -r '.packages[]? | .tag // "\(.name)@\(.version)"' "$manifest"
)
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
uses: braintrustdata/sdk-actions/actions/release/create-package-github-releases@337c5475631d724441403f5a4d8ad0b32ce12e29
with:
manifest: release-artifacts/release-manifest.json
repo: ${{ github.repository }}
- name: Ensure GitHub release SBOM assets
env:
GH_TOKEN: ${{ github.token }}
run: |
set -euo pipefail
manifest="release-artifacts/release-manifest.json"
manifest_dir="$(dirname "$manifest")"
while IFS= read -r encoded; do
package_json="$(printf '%s' "$encoded" | base64 -d)"
tag="$(jq -r '.tag // "\(.name)@\(.version)"' <<< "$package_json")"
sbom_asset="$(jq -r '.sbom_asset // empty' <<< "$package_json")"
if [ -z "$sbom_asset" ] || [ "$(basename "$sbom_asset")" != "$sbom_asset" ]; then
echo "Invalid SBOM asset for $tag: $sbom_asset" >&2
exit 1
fi
sbom_path="$manifest_dir/$sbom_asset"
if [ ! -f "$sbom_path" ]; then
echo "SBOM asset does not exist for $tag: $sbom_path" >&2
exit 1
fi
gh release upload "$tag" "$sbom_path" \
--repo "$GITHUB_REPOSITORY" \
--clobber
done < <(jq -r '.packages[] | @base64' "$manifest")
- name: Comment on issues closed by released PRs
env:
GITHUB_TOKEN: ${{ github.token }}
run: |
cp release-artifacts/release-manifest.json .release-manifest.json
node scripts/release/comment-release-issues.mjs
notify-failure:
needs:
- validate-release-sha
- print-changelog-links
- build-package-artifacts
- attest-package-sboms
- request-approval
- publish-stable
if: |
always() &&
(
needs.validate-release-sha.result == 'failure' ||
needs.print-changelog-links.result == 'failure' ||
needs.build-package-artifacts.result == 'failure' ||
needs.attest-package-sboms.result == 'failure' ||
needs.request-approval.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@dcb1066f776dd043e64d0e8ba94ca15cc7e1875d # v4.0.0
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>"