Publish Stable Release #28
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_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>" |