From 32a81bfbdbe62a38211e5a27142b3bc124cfbcf5 Mon Sep 17 00:00:00 2001 From: David Elner Date: Wed, 17 Jun 2026 18:07:34 +0000 Subject: [PATCH 1/3] Added: JavaScript release workflow and shared actions --- .github/workflows/release-js.yml | 224 ++++++++- README.md | 8 +- actions/release/lang/js/publish/action.yml | 449 ++++++++++++++++++ actions/release/lang/js/validate/action.yml | 222 +++++++++ .../actions/release/lang/js/publish.yml.erb | 137 ++++++ .../actions/release/lang/js/validate.yml.erb | 97 ++++ templates/steps/lang/js/build.yml.erb | 17 + templates/steps/lang/js/env-dump.yml.erb | 23 + templates/steps/lang/js/read-version.yml.erb | 21 + templates/steps/lang/js/setup.yml.erb | 41 ++ .../release/lang/js/notify-published.yml.erb | 21 + .../steps/release/lang/js/npm-publish.yml.erb | 40 ++ .../steps/release/lang/js/on-failure.yml.erb | 9 + .../release/lang/js/validate-channel.yml.erb | 21 + test/release/js/.gitignore | 2 + test/release/js/package.json | 22 + test/release/js/pnpm-lock.yaml | 9 + 17 files changed, 1351 insertions(+), 12 deletions(-) create mode 100644 actions/release/lang/js/publish/action.yml create mode 100644 actions/release/lang/js/validate/action.yml create mode 100644 templates/actions/release/lang/js/publish.yml.erb create mode 100644 templates/actions/release/lang/js/validate.yml.erb create mode 100644 templates/steps/lang/js/build.yml.erb create mode 100644 templates/steps/lang/js/env-dump.yml.erb create mode 100644 templates/steps/lang/js/read-version.yml.erb create mode 100644 templates/steps/lang/js/setup.yml.erb create mode 100644 templates/steps/release/lang/js/notify-published.yml.erb create mode 100644 templates/steps/release/lang/js/npm-publish.yml.erb create mode 100644 templates/steps/release/lang/js/on-failure.yml.erb create mode 100644 templates/steps/release/lang/js/validate-channel.yml.erb create mode 100644 test/release/js/.gitignore create mode 100644 test/release/js/package.json create mode 100644 test/release/js/pnpm-lock.yaml diff --git a/.github/workflows/release-js.yml b/.github/workflows/release-js.yml index 1ecd4d7..7f2668a 100644 --- a/.github/workflows/release-js.yml +++ b/.github/workflows/release-js.yml @@ -1,11 +1,56 @@ -# Placeholder so the release-js.yml workflow_dispatch trigger is registered on the -# default branch. This lets the full workflow be dispatched from its feature branch -# ("Use workflow from: ") before merge. The functional workflow — which -# publishes the @braintrust/bt-fake dummy npm package end-to-end — replaces this -# stub when the JavaScript release PR lands. -name: Release JavaScript (@braintrust/bt-fake) +# +# Releases @braintrust/bt-publishing-test — a dummy npm package used to validate Braintrust +# release workflow tooling end-to-end. This does NOT release any real Braintrust +# JavaScript SDK. +# +# This workflow serves two purposes: +# 1. End-to-end testing of the composite actions in actions/release/ +# 2. Reference implementation of the JavaScript-specific release template +# +# Generic steps are provided by composite actions in actions/release/. +# Only the sections marked with inline comments need to change for other packages. +# +# ───────────────────────────────────────────────────────────────────────────── +# SETUP REQUIREMENTS +# ───────────────────────────────────────────────────────────────────────────── +# +# GitHub Environments (repo Settings → Environments): +# publish +# - Required reviewers: sdk-eng team or named maintainers +# - Prevent self-review: enabled +# - Deployment branches: main only +# publish-dry-run +# - Required reviewers: at least yourself (to test the approval gate) +# - Allow administrators to bypass: enabled (for testing flexibility) +# - Deployment branches: no restriction +# +# Repository Variables (Settings → Secrets and variables → Variables): +# SLACK_SDK_RELEASE_CHANNEL Slack channel ID for release notifications +# +# Repository Secrets (Settings → Secrets and variables → Secrets): +# SLACK_BOT_TOKEN Org-level secret — Brainbot Slack app token +# Must be invited to SLACK_SDK_RELEASE_CHANNEL +# +# npm Trusted Publishing (OIDC — no token needed): +# Configure for your package at npmjs.com → package → Settings → Trusted Publisher +# Organization/User: @braintrust (or your npm scope/owner) +# Repository: braintrustdata/sdk-actions (or your SDK repo) +# Workflow: release-js.yml (or your workflow filename) +# Environment: publish +# Provenance requires a PUBLIC package; the publish job needs id-token: write. +# +# ───────────────────────────────────────────────────────────────────────────── + +name: Release JavaScript (@braintrust/bt-publishing-test) on: + # Auto dry-run on PRs that touch the generated actions or the workflows, to + # smoke-test the actions end-to-end. (A template edited without regenerating + # is caught separately by the CI check-generated job, which runs on every PR.) + pull_request: + paths: + - 'actions/**' + - '.github/workflows/**' workflow_dispatch: inputs: _instructions: @@ -30,6 +75,8 @@ on: description: "Anchor for release notes: a tag name or commit SHA. If SHA, notes will be empty." type: string required: false + # bt-publishing-test: fixed SHA anchor prevents indefinite changelog accumulation. + # Update this when you want to reset the notes baseline. default: '58a397193105516446c3042361913655bcece509' dry_run: description: "Dry run: Build without tagging or publishing" @@ -37,10 +84,169 @@ on: default: false jobs: - placeholder: + bump: runs-on: ubuntu-24.04 timeout-minutes: 5 permissions: {} + outputs: + version: ${{ steps.bump.outputs.version }} + steps: + # bt-publishing-test: derives next version by querying npm and incrementing the patch. + # Falls back to 0.0.1 on first publish (package not found). Remove this job + # in real SDK workflows; version comes from the version bump PR. + - name: Bump version + id: bump + run: | + # Package may not exist yet (first release / new name); default to 0.0.0 + # so the first publish is 0.0.1. + CURRENT=$(npm view @braintrust/bt-publishing-test version 2>/dev/null || echo "0.0.0") + CURRENT=${CURRENT:-0.0.0} + IFS='.' read -r major minor patch <<< "$CURRENT" + echo "version=$major.$minor.$((patch + 1))" >> $GITHUB_OUTPUT + + validate: + needs: bump + runs-on: ubuntu-24.04 + timeout-minutes: 10 + permissions: + contents: read + outputs: + release_tag: ${{ steps.validate-release.outputs.release_tag }} + prev_release: ${{ steps.validate-release.outputs.prev_release }} + branch: ${{ steps.validate-release.outputs.branch }} + on_release_branch: ${{ steps.validate-release.outputs.on_release_branch }} + commit_message: ${{ steps.validate-release.outputs.commit_message }} steps: - - name: Placeholder - run: echo "Placeholder — registers workflow_dispatch. The functional release-js.yml lands in the JavaScript release PR." + # bt-publishing-test: checkout required to resolve local ./actions/... references. + # Real SDK workflows use external braintrustdata/sdk-actions/...@sha references + # and do not need this step. + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + + - name: Validate release + id: validate-release + uses: ./actions/release/lang/js/validate + with: + # bt-publishing-test: version passed directly from bump job — no package.json read needed. + # In a real JS project, drop `version`; it is read from package.json. + version: ${{ needs.bump.outputs.version }} + sha: ${{ inputs.sha || github.event.pull_request.head.sha }} + dry_run: ${{ inputs.dry_run || github.event_name == 'pull_request' }} + working_directory: test/release/js + package_manager: pnpm + channel: ${{ inputs.channel || 'latest' }} + allowed_channels: 'latest,rc,next,beta' + + prepare: + needs: validate + runs-on: ubuntu-24.04 + timeout-minutes: 5 + permissions: + contents: write # required for releases/generate-notes API + outputs: + pr_list: ${{ steps.prepare.outputs.pr_list }} + notes: ${{ steps.prepare.outputs.notes }} + steps: + # bt-publishing-test: checkout required because actions are referenced locally (./actions/...). + # Real SDK workflows reference actions externally (braintrustdata/sdk-actions/...@sha) + # and do not need this step. + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + + - name: Prepare release + id: prepare + uses: ./actions/release/prepare + with: + release_tag: ${{ needs.validate.outputs.release_tag }} + sha: ${{ inputs.sha || github.event.pull_request.head.sha }} + # On PR auto dry-runs, anchor notes to the head SHA: prepare treats a + # full SHA as "notes unavailable" (no tag range), so the changelog + # doesn't accumulate in the step summary on every PR. + prev_release: ${{ inputs.prev_release || github.event.pull_request.head.sha || needs.validate.outputs.prev_release }} + + notify-pending: + needs: [validate, prepare] + runs-on: ubuntu-24.04 + timeout-minutes: 5 + permissions: {} + steps: + # bt-publishing-test: checkout required for local action resolution (see prepare job comment) + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + + - name: Notify release pending + uses: ./actions/release/notify-pending + with: + sha: ${{ inputs.sha || github.event.pull_request.head.sha }} + release_tag: ${{ needs.validate.outputs.release_tag }} + prev_release: ${{ needs.validate.outputs.prev_release }} + branch: ${{ needs.validate.outputs.branch }} + on_release_branch: ${{ needs.validate.outputs.on_release_branch }} + commit_message: ${{ needs.validate.outputs.commit_message }} + pr_list: ${{ needs.prepare.outputs.pr_list }} + notes: ${{ needs.prepare.outputs.notes }} + dry_run: ${{ inputs.dry_run || github.event_name == 'pull_request' }} + # Suppress Slack on PR auto dry-runs (still exercises message-building; just no real post). + # Gate on != pull_request: GHA's `A && '' || B` returns B (the '' is falsy), so suppression + # must be the trailing '' branch, not the "true" value. + slack_token: ${{ github.event_name != 'pull_request' && secrets.SLACK_BOT_TOKEN || '' }} + slack_channel: ${{ github.event_name != 'pull_request' && vars.SLACK_SDK_RELEASE_CHANNEL || '' }} + # slack_mention: '@sdk-eng' + emoji: ':javascript:' + + publish: + needs: [bump, validate, prepare, notify-pending] + runs-on: ubuntu-24.04 + timeout-minutes: 15 + # No environment on PR dry-runs (avoids the approval gate AND the accumulating + # deployment records); the gated environments apply only to manual dispatch. + # Note the structure: empty must be the trailing `|| ''` branch, since GHA's + # `cond && '' || X` would fall through to X (the '' is falsy). + environment: >- + ${{ github.event_name != 'pull_request' + && (inputs.dry_run && 'publish-dry-run' || 'publish') + || '' }} + permissions: + contents: write + id-token: write # OIDC trusted publishing + provenance + steps: + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + ref: ${{ inputs.sha || github.event.pull_request.head.sha }} + fetch-depth: 0 + + # bt-publishing-test: write the run-number version into package.json. The publish job + # checks out fresh, so the file still reflects the placeholder version. + # Real SDK workflows omit this — the version is committed in the bump PR. + # (No lockfile patch needed: pnpm-lock.yaml doesn't record the package's own version.) + - name: Apply version to package.json + env: + VERSION: ${{ needs.bump.outputs.version }} + run: | + node -e 'const fs=require("fs");const f="test/release/js/package.json";const p=JSON.parse(fs.readFileSync(f,"utf8"));p.version=process.env.VERSION;fs.writeFileSync(f,JSON.stringify(p,null,2)+"\n")' + + - name: Publish + uses: ./actions/release/lang/js/publish + with: + sha: ${{ inputs.sha || github.event.pull_request.head.sha }} + checkout: 'false' # bt-publishing-test: skip internal checkout — version patching above must survive into the build + working_directory: test/release/js + package_manager: pnpm + dry_run: ${{ inputs.dry_run || github.event_name == 'pull_request' }} + release_tag: ${{ needs.validate.outputs.release_tag }} + github_release: 'false' # bt-publishing-test: omit GitHub release to avoid tag/release pollution from repeated test runs + version: ${{ needs.bump.outputs.version }} + package_name: '@braintrust/bt-publishing-test' + access: 'public' + provenance: 'true' + channel: ${{ inputs.channel || 'latest' }} + notes: ${{ needs.prepare.outputs.notes }} + prev_release: ${{ needs.validate.outputs.prev_release }} + branch: ${{ needs.validate.outputs.branch }} + on_release_branch: ${{ needs.validate.outputs.on_release_branch }} + pr_list: ${{ needs.prepare.outputs.pr_list }} + # Suppress Slack on PR auto dry-runs (still exercises message-building; just no real post). + # Gate on != pull_request: GHA's `A && '' || B` returns B (the '' is falsy), so suppression + # must be the trailing '' branch, not the "true" value. + slack_token: ${{ github.event_name != 'pull_request' && secrets.SLACK_BOT_TOKEN || '' }} + slack_channel: ${{ github.event_name != 'pull_request' && vars.SLACK_SDK_RELEASE_CHANNEL || '' }} + # slack_mention: '@sdk-eng' + # failure_slack_mention: '@sdk-eng' + emoji: ':javascript:' diff --git a/README.md b/README.md index 83689e7..05924db 100644 --- a/README.md +++ b/README.md @@ -9,9 +9,9 @@ Shared GitHub actions and workflows for Braintrust SDK repositories. ### Adopting the release workflow 1. **Copy the canonical template** for your language into your repo's - `.github/workflows/`. For Ruby that's - [`release-ruby.yml`](.github/workflows/release-ruby.yml) — it doubles as this - repo's end-to-end test and the reference implementation. + `.github/workflows/`: + - [`release-js.yml`](.github/workflows/release-js.yml) + - [`release-ruby.yml`](.github/workflows/release-ruby.yml) 2. **Adapt the marked sections** — version source, gem name, working directory, and so on. The template's comments flag exactly what changes per repo; everything else is provided by the pinned actions. @@ -33,6 +33,8 @@ pulls in everything it needs. |---|---| | `release/lang/ruby/publish` | Push the gem to RubyGems, create the GitHub release, and notify | | `release/lang/ruby/validate` | Check out the SHA, set up Ruby, read the version, validate the release (tag/branch/metadata), lint + build | +| `release/lang/js/publish` | Publish the package to npm (OIDC trusted publishing), create the GitHub release, and notify | +| `release/lang/js/validate` | Check out the SHA, set up Node + package manager, read the version, validate the release (channel/tag/branch/metadata), build | | `release/notify-pending` | Post the pre-approval job summary and Slack notification | | `release/prepare` | Fetch the PR list and release notes | diff --git a/actions/release/lang/js/publish/action.yml b/actions/release/lang/js/publish/action.yml new file mode 100644 index 0000000..8be8a89 --- /dev/null +++ b/actions/release/lang/js/publish/action.yml @@ -0,0 +1,449 @@ +# GENERATED by scripts/generate.rb — edit templates/, not this file. +name: Publish JavaScript Package +description: > + Full JavaScript package release: checks out the SHA, sets up Node and the + package manager, builds, publishes to npm (OIDC trusted publishing), creates a + GitHub release, and sends a Slack completion notification. Covers the standard + publish sequence for Braintrust npm packages. + +inputs: + sha: + description: "Commit SHA to check out and release" + required: true + checkout: + description: "Check out the SHA before publishing. Set to false when the caller has already checked out and patched files." + required: false + default: 'true' + working_directory: + description: "Path to the package root (where package.json lives)" + required: false + default: '.' + package_manager: + description: "Package manager: pnpm | npm | yarn" + required: false + default: 'pnpm' + node_version: + description: "Node version to use" + required: false + default: '22' + pnpm_version: + description: "pnpm version to install (when package_manager is pnpm)" + required: false + default: '10.33.0' + build_command: + description: "npm script to run for the build" + required: false + default: 'build' + dry_run: + description: "Skip npm publish, tag push, and GitHub release creation" + required: false + default: 'false' + release_tag: + description: "The release tag (e.g. v0.3.2)" + required: true + version: + description: "The package version — used to build the npm link in notifications" + required: false + default: '' + github_release: + description: "Whether to create a GitHub release" + required: false + default: 'true' + npm: + description: "Whether to publish to the npm registry" + required: false + default: 'true' + registry: + description: "npm registry URL" + required: false + default: 'https://registry.npmjs.org' + npm_version: + description: "npm version to install before publishing (pinned; >= 11.5.1 for tokenless OIDC trusted publishing)" + required: false + default: '11.6.2' + access: + description: "npm publish access: public | restricted (provenance requires public)" + required: false + default: 'public' + provenance: + description: "Whether to publish with provenance/attestation (requires a public package)" + required: false + default: 'true' + channel: + description: "npm release channel (dist-tag) to publish under" + required: false + default: 'latest' + notes: + description: "Base64-encoded release notes from the prepare action" + required: false + default: '' + prev_release: + description: "Previous release tag — used in completion notification" + required: false + default: '' + branch: + description: "Branch the release was made from" + required: true + on_release_branch: + description: "Whether the SHA is on an expected release branch" + required: true + pr_list: + description: "x1f-delimited PR list from prepare action" + required: false + default: '' + package_name: + description: "npm package name — used to build the npm link in notifications" + required: false + default: '' + slack_token: + description: "Slack bot token" + required: false + default: '' + slack_channel: + description: "Slack channel ID" + required: false + default: '' + slack_mention: + description: "Optional Slack handle or group to @mention on completion" + required: false + default: '' + failure_slack_mention: + description: "Optional Slack handle or group to @mention if the publish fails" + required: false + default: '' + emoji: + description: "Emoji prefix for Slack messages" + required: false + default: ':package:' + +runs: + using: composite + steps: + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + if: ${{ inputs.checkout == 'true' }} + with: + ref: ${{ inputs.sha }} + fetch-depth: 0 + + - name: Set up pnpm + if: ${{ inputs.package_manager == 'pnpm' }} + uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v5.0.0 + with: + version: ${{ inputs.pnpm_version }} + + - name: Set up Node + uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 + with: + node-version: ${{ inputs.node_version }} + registry-url: ${{ inputs.registry }} + cache: ${{ inputs.package_manager }} + cache-dependency-path: | + ${{ inputs.working_directory }}/pnpm-lock.yaml + ${{ inputs.working_directory }}/package-lock.json + ${{ inputs.working_directory }}/yarn.lock + + - name: Install dependencies + shell: bash + working-directory: ${{ inputs.working_directory }} + env: + PM: ${{ inputs.package_manager }} + run: | + case "$PM" in + pnpm) pnpm install --frozen-lockfile ;; + yarn) yarn install --frozen-lockfile ;; + npm) npm ci ;; + *) echo "Unknown package_manager: $PM"; exit 1 ;; + esac + + - name: Build + shell: bash + working-directory: ${{ inputs.working_directory }} + env: + PM: ${{ inputs.package_manager }} + BUILD_COMMAND: ${{ inputs.build_command }} + run: | + "$PM" run "$BUILD_COMMAND" + + - name: Publish to npm + shell: bash + working-directory: ${{ inputs.working_directory }} + env: + NPM_ENABLED: ${{ inputs.npm }} + DRY_RUN: ${{ inputs.dry_run }} + CHANNEL: ${{ inputs.channel }} + ACCESS: ${{ inputs.access }} + PROVENANCE: ${{ inputs.provenance }} + REGISTRY: ${{ inputs.registry }} + NPM_VERSION: ${{ inputs.npm_version }} + # Force OIDC trusted publishing: clear any inherited token so npm can't fall + # back to token auth. + NODE_AUTH_TOKEN: '' + NPM_TOKEN: '' + run: | + if [ "$NPM_ENABLED" != "true" ]; then + echo "npm publishing disabled; skipping." + exit 0 + fi + npm install -g "npm@$NPM_VERSION" + ARGS=(--tag "$CHANNEL" --access "$ACCESS" --registry "$REGISTRY") + [ "$PROVENANCE" = "true" ] && ARGS+=(--provenance) + if [ "$DRY_RUN" = "true" ]; then + echo "DRY RUN: npm publish ${ARGS[*]} --dry-run" + npm publish "${ARGS[@]}" --dry-run + else + npm publish "${ARGS[@]}" + fi + + - name: Create GitHub release + shell: bash + env: + GITHUB_TOKEN: ${{ github.token }} + ENABLED: ${{ inputs.github_release }} + DRY_RUN: ${{ inputs.dry_run }} + TAG: ${{ inputs.release_tag }} + NOTES_B64: ${{ inputs.notes }} + SHA: ${{ inputs.sha }} + run: | + if [ "$ENABLED" != "true" ]; then + echo "GitHub release disabled; skipping." + exit 0 + fi + if [ "$DRY_RUN" = "true" ]; then + echo "DRY RUN: would create GitHub release $TAG" + echo "--- Release notes preview ---" + echo "$NOTES_B64" | base64 -d 2>/dev/null || echo "(unavailable)" + else + echo "$NOTES_B64" | base64 -d 2>/dev/null > /tmp/release-notes.md + gh release create "$TAG" \ + --title "$TAG" \ + --notes-file /tmp/release-notes.md \ + --target "$SHA" + fi + + - name: Build npm links + id: links + shell: bash + env: + PACKAGE_NAME: ${{ inputs.package_name }} + VERSION: ${{ inputs.version }} + DRY_RUN: ${{ inputs.dry_run }} + run: | + LINKS='[]' + if [ "$DRY_RUN" != "true" ] && [ -n "$PACKAGE_NAME" ] && [ -n "$VERSION" ]; then + LINKS="[{\"label\":\"npm\",\"url\":\"https://www.npmjs.com/package/$PACKAGE_NAME/v/$VERSION\"}]" + fi + echo "links=$LINKS" >> $GITHUB_OUTPUT + + - name: Post release summary + shell: bash + env: + TAG: ${{ inputs.release_tag }} + PREV_RELEASE: ${{ inputs.prev_release }} + BRANCH: ${{ inputs.branch }} + ON_RELEASE_BRANCH: ${{ inputs.on_release_branch }} + DRY_RUN: ${{ inputs.dry_run }} + LINKS_JSON: ${{ steps.links.outputs.links }} + RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} + run: | + RELEASE_URL="$GITHUB_SERVER_URL/$GITHUB_REPOSITORY/releases/tag/$TAG" + BRANCH_URL="$GITHUB_SERVER_URL/$GITHUB_REPOSITORY/tree/$BRANCH" + + if [ "$DRY_RUN" = "true" ]; then + echo "## $GITHUB_REPOSITORY $TAG — dry run complete" >> $GITHUB_STEP_SUMMARY + else + echo "## $GITHUB_REPOSITORY $TAG — published ✅" >> $GITHUB_STEP_SUMMARY + fi + echo "" >> $GITHUB_STEP_SUMMARY + + if [ "$DRY_RUN" = "true" ]; then + echo "> [!NOTE]" >> $GITHUB_STEP_SUMMARY + echo "> Dry run: Nothing was tagged or published." >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + fi + + if [ "$ON_RELEASE_BRANCH" = "false" ]; then + echo "> [!WARNING]" >> $GITHUB_STEP_SUMMARY + echo "> Released from non-release branch: [$BRANCH]($BRANCH_URL)" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + fi + + echo "**Branch:** [$BRANCH]($BRANCH_URL)" >> $GITHUB_STEP_SUMMARY + + if [ -n "$PREV_RELEASE" ]; then + DIFF_URL="$GITHUB_SERVER_URL/$GITHUB_REPOSITORY/compare/${PREV_RELEASE}...$TAG" + echo "**Diff:** [$PREV_RELEASE...$TAG]($DIFF_URL)" >> $GITHUB_STEP_SUMMARY + fi + + echo "" >> $GITHUB_STEP_SUMMARY + echo "### Published to" >> $GITHUB_STEP_SUMMARY + + if [ "$DRY_RUN" = "true" ]; then + echo "- [View dry run]($RUN_URL)" >> $GITHUB_STEP_SUMMARY + else + echo "- [GitHub Release]($RELEASE_URL)" >> $GITHUB_STEP_SUMMARY + if [ -n "$LINKS_JSON" ] && [ "$LINKS_JSON" != "[]" ]; then + echo "$LINKS_JSON" | jq -r '.[] | "- [\(.label)](\(.url))"' >> $GITHUB_STEP_SUMMARY + fi + fi + + - name: Build published message + id: message-published + shell: bash + env: + TAG: ${{ inputs.release_tag }} + EMOJI: ${{ inputs.emoji }} + BRANCH: ${{ inputs.branch }} + ON_RELEASE_BRANCH: ${{ inputs.on_release_branch }} + PREV_RELEASE: ${{ inputs.prev_release }} + DRY_RUN: ${{ inputs.dry_run }} + SLACK_MENTION: ${{ inputs.slack_mention }} + LINKS_JSON: ${{ steps.links.outputs.links }} + RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} + PR_LIST_RAW: ${{ inputs.pr_list }} + run: | + RELEASE_URL="$GITHUB_SERVER_URL/$GITHUB_REPOSITORY/releases/tag/$TAG" + + DIFF_PART="" + if [ -n "$PREV_RELEASE" ]; then + DIFF_URL="$GITHUB_SERVER_URL/$GITHUB_REPOSITORY/compare/${PREV_RELEASE}...$TAG" + DIFF_PART="<$DIFF_URL|${PREV_RELEASE}...$TAG> · " + fi + + PR_LIST=$(echo "$PR_LIST_RAW" | tr $'\x1f' '\n' | sed '/^$/d') + + if [ "$DRY_RUN" = "true" ]; then + TEXT="$EMOJI *$GITHUB_REPOSITORY $TAG* complete" + TEXT="$TEXT\n> :information_source: _Dry run: nothing tagged or published._" + else + TEXT="$EMOJI *$GITHUB_REPOSITORY $TAG* published" + fi + + if [ "$ON_RELEASE_BRANCH" = "false" ]; then + BRANCH_URL="$GITHUB_SERVER_URL/$GITHUB_REPOSITORY/tree/$BRANCH" + TEXT="$TEXT\n> ⚠️ NOT on release branch: <$BRANCH_URL|$BRANCH>" + fi + + if [ "$DRY_RUN" = "true" ]; then + TEXT="$TEXT\n${DIFF_PART}<$RUN_URL|View dry run>" + else + SLACK_LINKS="${DIFF_PART}<$RELEASE_URL|View release>" + if [ -n "$LINKS_JSON" ] && [ "$LINKS_JSON" != "[]" ]; then + EXTRA=$(echo "$LINKS_JSON" | jq -r '[.[] | "<\(.url)|\(.label)>"] | join(" · ")') + SLACK_LINKS="$SLACK_LINKS · $EXTRA" + fi + TEXT="$TEXT\n$SLACK_LINKS" + fi + + if [ -n "$PR_LIST" ]; then + TEXT="$TEXT\n$PR_LIST" + fi + + if [ -n "$SLACK_MENTION" ]; then + TEXT="$TEXT\n$SLACK_MENTION" + fi + + { echo 'text<> $GITHUB_OUTPUT + + - name: Send Slack message + shell: bash + env: + SLACK_BOT_TOKEN: ${{ inputs.slack_token }} + SLACK_CHANNEL: ${{ inputs.slack_channel }} + TEXT: ${{ steps.message-published.outputs.text }} + FAIL_ON_ERROR: 'false' + run: | + # Self-guard: no-op when Slack isn't configured, so callers needn't gate this step. + if [ -z "$SLACK_BOT_TOKEN" ] || [ -z "$SLACK_CHANNEL" ]; then + echo "Slack not configured; skipping notification." + exit 0 + fi + # jq --arg passes strings literally, so \n must be real newlines before encoding + TEXT=$(printf '%b' "$TEXT") + RESPONSE=$(curl -s --max-time 10 -X POST "https://slack.com/api/chat.postMessage" \ + -H "Authorization: Bearer $SLACK_BOT_TOKEN" \ + -H "Content-Type: application/json; charset=utf-8" \ + -d "$(jq -n --arg channel "$SLACK_CHANNEL" --arg text "$TEXT" \ + '{channel: $channel, text: $text}')") || true + if echo "$RESPONSE" | jq -e '.ok' > /dev/null 2>&1; then + echo "Slack notification sent." + elif [ "$FAIL_ON_ERROR" = "true" ]; then + echo "Slack notification failed: $RESPONSE" + exit 1 + else + echo "::warning::Slack notification failed: $RESPONSE" + fi + + - name: Dump JavaScript environment + if: ${{ failure() }} + shell: bash + working-directory: ${{ inputs.working_directory }} + env: + PM: ${{ inputs.package_manager }} + run: | + echo "=== Node environment ===" + node --version + npm --version + echo "=== Package manager ($PM) ===" + "$PM" --version || echo "(unavailable)" + echo "=== Registry ===" + npm config get registry + echo "=== package.json version ===" + node -p "require('./package.json').version" 2>/dev/null || echo "(unavailable)" + + - name: Build failure message + if: ${{ failure() }} + id: message-failure + shell: bash + env: + RELEASE_TAG: ${{ inputs.release_tag }} + STEP: publish + SLACK_MENTION: ${{ inputs.failure_slack_mention }} + RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} + ACTOR_LINK: <${{ github.server_url }}/${{ github.actor }}|${{ github.actor }}> + run: | + TEXT=":x: *$GITHUB_REPOSITORY $RELEASE_TAG* failed at $STEP" + TEXT="$TEXT\nInitiated by: $ACTOR_LINK" + + if [ -n "$SLACK_MENTION" ]; then + TEXT="$TEXT $SLACK_MENTION" + fi + + TEXT="$TEXT\n<$RUN_URL|View failed run>" + + { echo 'text<> $GITHUB_OUTPUT + + - name: Send Slack message + if: ${{ failure() }} + shell: bash + env: + SLACK_BOT_TOKEN: ${{ inputs.slack_token }} + SLACK_CHANNEL: ${{ inputs.slack_channel }} + TEXT: ${{ steps.message-failure.outputs.text }} + FAIL_ON_ERROR: 'false' + run: | + # Self-guard: no-op when Slack isn't configured, so callers needn't gate this step. + if [ -z "$SLACK_BOT_TOKEN" ] || [ -z "$SLACK_CHANNEL" ]; then + echo "Slack not configured; skipping notification." + exit 0 + fi + # jq --arg passes strings literally, so \n must be real newlines before encoding + TEXT=$(printf '%b' "$TEXT") + RESPONSE=$(curl -s --max-time 10 -X POST "https://slack.com/api/chat.postMessage" \ + -H "Authorization: Bearer $SLACK_BOT_TOKEN" \ + -H "Content-Type: application/json; charset=utf-8" \ + -d "$(jq -n --arg channel "$SLACK_CHANNEL" --arg text "$TEXT" \ + '{channel: $channel, text: $text}')") || true + if echo "$RESPONSE" | jq -e '.ok' > /dev/null 2>&1; then + echo "Slack notification sent." + elif [ "$FAIL_ON_ERROR" = "true" ]; then + echo "Slack notification failed: $RESPONSE" + exit 1 + else + echo "::warning::Slack notification failed: $RESPONSE" + fi diff --git a/actions/release/lang/js/validate/action.yml b/actions/release/lang/js/validate/action.yml new file mode 100644 index 0000000..2ab9199 --- /dev/null +++ b/actions/release/lang/js/validate/action.yml @@ -0,0 +1,222 @@ +# GENERATED by scripts/generate.rb — edit templates/, not this file. +name: Validate JavaScript Release +description: > + Checks out the release SHA, sets up Node and the package manager, reads the + package version, validates the release channel and metadata (tag existence, + branch, commit metadata), and builds to confirm the package is publishable + before the approval gate. + +inputs: + sha: + description: "Commit SHA being released" + required: true + dry_run: + description: "Skip tag existence failure in dry runs" + required: false + default: 'false' + version: + description: > + Explicit version override. If provided, the version is not read from + package.json. Use when the version is computed externally (e.g. bt-publishing-test + auto-bump from run number). + required: false + default: '' + working_directory: + description: "Path to the package root (where package.json lives)" + required: false + default: '.' + package_manager: + description: "Package manager: pnpm | npm | yarn" + required: false + default: 'pnpm' + node_version: + description: "Node version to use" + required: false + default: '22' + pnpm_version: + description: "pnpm version to install (when package_manager is pnpm)" + required: false + default: '10.33.0' + registry: + description: "npm registry URL" + required: false + default: 'https://registry.npmjs.org' + tag_format: + description: "Git tag format, with a {version} placeholder (e.g. js-{version})" + required: false + default: 'v{version}' + channel: + description: "npm release channel (dist-tag) this release targets" + required: false + default: 'latest' + allowed_channels: + description: "Comma-separated allowlist of accepted release channels" + required: false + default: 'latest' + build_command: + description: "npm script to run for the build" + required: false + default: 'build' +outputs: + release_tag: + description: "The release tag (e.g. v0.3.2)" + value: ${{ steps.validate.outputs.release_tag }} + prev_release: + description: "The previous release tag" + value: ${{ steps.validate.outputs.prev_release }} + branch: + description: "The branch containing the SHA" + value: ${{ steps.validate.outputs.branch }} + on_release_branch: + description: "Whether the SHA is on an expected release branch" + value: ${{ steps.validate.outputs.on_release_branch }} + commit_message: + description: "The commit message at the SHA" + value: ${{ steps.validate.outputs.commit_message }} + version: + description: "The resolved package version" + value: ${{ steps.read-version.outputs.version }} + +runs: + using: composite + steps: + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + ref: ${{ inputs.sha }} + fetch-depth: 0 + + - name: Set up pnpm + if: ${{ inputs.package_manager == 'pnpm' }} + uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v5.0.0 + with: + version: ${{ inputs.pnpm_version }} + + - name: Set up Node + uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 + with: + node-version: ${{ inputs.node_version }} + registry-url: ${{ inputs.registry }} + cache: ${{ inputs.package_manager }} + cache-dependency-path: | + ${{ inputs.working_directory }}/pnpm-lock.yaml + ${{ inputs.working_directory }}/package-lock.json + ${{ inputs.working_directory }}/yarn.lock + + - name: Install dependencies + shell: bash + working-directory: ${{ inputs.working_directory }} + env: + PM: ${{ inputs.package_manager }} + run: | + case "$PM" in + pnpm) pnpm install --frozen-lockfile ;; + yarn) yarn install --frozen-lockfile ;; + npm) npm ci ;; + *) echo "Unknown package_manager: $PM"; exit 1 ;; + esac + + - name: Read version + id: read-version + shell: bash + working-directory: ${{ inputs.working_directory }} + env: + VERSION_OVERRIDE: ${{ inputs.version }} + run: | + if [ -n "$VERSION_OVERRIDE" ]; then + echo "version=$VERSION_OVERRIDE" >> $GITHUB_OUTPUT + else + VERSION=$(node -p "require('./package.json').version") + echo "version=$VERSION" >> $GITHUB_OUTPUT + fi + + - name: Validate release channel + shell: bash + env: + CHANNEL: ${{ inputs.channel }} + ALLOWED: ${{ inputs.allowed_channels }} + run: | + IFS=',' read -ra OK <<< "$ALLOWED" + for c in "${OK[@]}"; do + [ "$(echo "$c" | xargs)" = "$CHANNEL" ] && exit 0 + done + echo "Error: release channel '$CHANNEL' is not in the allowed set: $ALLOWED" + exit 1 + + - name: Validate SHA format + shell: bash + env: + SHA: ${{ inputs.sha }} + run: | + if ! echo "$SHA" | grep -qE '^[0-9a-f]{40}$'; then + echo "Error: sha must be a full 40-character commit SHA — branch names and short SHAs are not accepted." + exit 1 + fi + + - name: Validate release + id: validate + shell: bash + env: + VERSION: ${{ steps.read-version.outputs.version }} + TAG_FORMAT: ${{ inputs.tag_format }} + DRY_RUN: ${{ inputs.dry_run }} + RELEASE_BRANCH: 'main' + SHA: ${{ inputs.sha }} + run: | + TAG="${TAG_FORMAT//\{version\}/$VERSION}" + COMMIT_MSG=$(git log -1 --format="%s" HEAD) + BRANCH=$(git branch -r --contains HEAD --format="%(refname:short)" | sed 's|origin/||' | head -1) + + if git rev-parse "$TAG" >/dev/null 2>&1; then + if [ "$DRY_RUN" = "true" ]; then + echo "Warning: Tag $TAG already exists — skipping in dry run" + else + echo "Error: Tag $TAG already exists — has the version been bumped?" + exit 1 + fi + fi + + ON_RELEASE_BRANCH=false + IFS=',' read -ra PATTERNS <<< "$RELEASE_BRANCH" + for pattern in "${PATTERNS[@]}"; do + pattern=$(echo "$pattern" | xargs) + if [[ "$BRANCH" == $pattern ]]; then + ON_RELEASE_BRANCH=true + break + fi + done + + PREV_RELEASE_PATTERN="${TAG_FORMAT//\{version\}/[0-9]*.[0-9]*.[0-9]*}" + PREV_RELEASE=$(git describe --tags --abbrev=0 --match="$PREV_RELEASE_PATTERN" HEAD^ 2>/dev/null || echo "") + + echo "release_tag=$TAG" >> $GITHUB_OUTPUT + echo "commit_message=$COMMIT_MSG" >> $GITHUB_OUTPUT + echo "branch=$BRANCH" >> $GITHUB_OUTPUT + echo "on_release_branch=$ON_RELEASE_BRANCH" >> $GITHUB_OUTPUT + echo "prev_release=$PREV_RELEASE" >> $GITHUB_OUTPUT + echo "Validated $TAG @ $SHA ($COMMIT_MSG)" + + - name: Build + shell: bash + working-directory: ${{ inputs.working_directory }} + env: + PM: ${{ inputs.package_manager }} + BUILD_COMMAND: ${{ inputs.build_command }} + run: | + "$PM" run "$BUILD_COMMAND" + + - name: Dump JavaScript environment + if: ${{ failure() }} + shell: bash + working-directory: ${{ inputs.working_directory }} + env: + PM: ${{ inputs.package_manager }} + run: | + echo "=== Node environment ===" + node --version + npm --version + echo "=== Package manager ($PM) ===" + "$PM" --version || echo "(unavailable)" + echo "=== Registry ===" + npm config get registry + echo "=== package.json version ===" + node -p "require('./package.json').version" 2>/dev/null || echo "(unavailable)" diff --git a/templates/actions/release/lang/js/publish.yml.erb b/templates/actions/release/lang/js/publish.yml.erb new file mode 100644 index 0000000..67e0249 --- /dev/null +++ b/templates/actions/release/lang/js/publish.yml.erb @@ -0,0 +1,137 @@ +name: Publish JavaScript Package +description: > + Full JavaScript package release: checks out the SHA, sets up Node and the + package manager, builds, publishes to npm (OIDC trusted publishing), creates a + GitHub release, and sends a Slack completion notification. Covers the standard + publish sequence for Braintrust npm packages. + +inputs: + sha: + description: "Commit SHA to check out and release" + required: true + checkout: + description: "Check out the SHA before publishing. Set to false when the caller has already checked out and patched files." + required: false + default: 'true' + working_directory: + description: "Path to the package root (where package.json lives)" + required: false + default: '.' + package_manager: + description: "Package manager: pnpm | npm | yarn" + required: false + default: 'pnpm' + node_version: + description: "Node version to use" + required: false + default: '22' + pnpm_version: + description: "pnpm version to install (when package_manager is pnpm)" + required: false + default: '10.33.0' + build_command: + description: "npm script to run for the build" + required: false + default: 'build' + dry_run: + description: "Skip npm publish, tag push, and GitHub release creation" + required: false + default: 'false' + release_tag: + description: "The release tag (e.g. v0.3.2)" + required: true + version: + description: "The package version — used to build the npm link in notifications" + required: false + default: '' + github_release: + description: "Whether to create a GitHub release" + required: false + default: 'true' + npm: + description: "Whether to publish to the npm registry" + required: false + default: 'true' + registry: + description: "npm registry URL" + required: false + default: 'https://registry.npmjs.org' + npm_version: + description: "npm version to install before publishing (pinned; >= 11.5.1 for tokenless OIDC trusted publishing)" + required: false + default: '11.6.2' + access: + description: "npm publish access: public | restricted (provenance requires public)" + required: false + default: 'public' + provenance: + description: "Whether to publish with provenance/attestation (requires a public package)" + required: false + default: 'true' + channel: + description: "npm release channel (dist-tag) to publish under" + required: false + default: 'latest' + notes: + description: "Base64-encoded release notes from the prepare action" + required: false + default: '' + prev_release: + description: "Previous release tag — used in completion notification" + required: false + default: '' + branch: + description: "Branch the release was made from" + required: true + on_release_branch: + description: "Whether the SHA is on an expected release branch" + required: true + pr_list: + description: "x1f-delimited PR list from prepare action" + required: false + default: '' + package_name: + description: "npm package name — used to build the npm link in notifications" + required: false + default: '' + slack_token: + description: "Slack bot token" + required: false + default: '' + slack_channel: + description: "Slack channel ID" + required: false + default: '' + slack_mention: + description: "Optional Slack handle or group to @mention on completion" + required: false + default: '' + failure_slack_mention: + description: "Optional Slack handle or group to @mention if the publish fails" + required: false + default: '' + emoji: + description: "Emoji prefix for Slack messages" + required: false + default: ':package:' + +runs: + using: composite + steps: + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + if: ${{ inputs.checkout == 'true' }} + with: + ref: ${{ inputs.sha }} + fetch-depth: 0 + +<%= render_step('lang/js/setup') %> + +<%= render_step('lang/js/build') %> + +<%= render_step('release/lang/js/npm-publish') %> + +<%= render_step('release/create-github-release') %> + +<%= render_step('release/lang/js/notify-published') %> + +<%= render_step('release/lang/js/on-failure', if: "${{ failure() }}") %> diff --git a/templates/actions/release/lang/js/validate.yml.erb b/templates/actions/release/lang/js/validate.yml.erb new file mode 100644 index 0000000..2b254f9 --- /dev/null +++ b/templates/actions/release/lang/js/validate.yml.erb @@ -0,0 +1,97 @@ +name: Validate JavaScript Release +description: > + Checks out the release SHA, sets up Node and the package manager, reads the + package version, validates the release channel and metadata (tag existence, + branch, commit metadata), and builds to confirm the package is publishable + before the approval gate. + +inputs: + sha: + description: "Commit SHA being released" + required: true + dry_run: + description: "Skip tag existence failure in dry runs" + required: false + default: 'false' + version: + description: > + Explicit version override. If provided, the version is not read from + package.json. Use when the version is computed externally (e.g. bt-publishing-test + auto-bump from run number). + required: false + default: '' + working_directory: + description: "Path to the package root (where package.json lives)" + required: false + default: '.' + package_manager: + description: "Package manager: pnpm | npm | yarn" + required: false + default: 'pnpm' + node_version: + description: "Node version to use" + required: false + default: '22' + pnpm_version: + description: "pnpm version to install (when package_manager is pnpm)" + required: false + default: '10.33.0' + registry: + description: "npm registry URL" + required: false + default: 'https://registry.npmjs.org' + tag_format: + description: "Git tag format, with a {version} placeholder (e.g. js-{version})" + required: false + default: 'v{version}' + channel: + description: "npm release channel (dist-tag) this release targets" + required: false + default: 'latest' + allowed_channels: + description: "Comma-separated allowlist of accepted release channels" + required: false + default: 'latest' + build_command: + description: "npm script to run for the build" + required: false + default: 'build' +outputs: + release_tag: + description: "The release tag (e.g. v0.3.2)" + value: ${{ steps.validate.outputs.release_tag }} + prev_release: + description: "The previous release tag" + value: ${{ steps.validate.outputs.prev_release }} + branch: + description: "The branch containing the SHA" + value: ${{ steps.validate.outputs.branch }} + on_release_branch: + description: "Whether the SHA is on an expected release branch" + value: ${{ steps.validate.outputs.on_release_branch }} + commit_message: + description: "The commit message at the SHA" + value: ${{ steps.validate.outputs.commit_message }} + version: + description: "The resolved package version" + value: ${{ steps.read-version.outputs.version }} + +runs: + using: composite + steps: + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + ref: ${{ inputs.sha }} + fetch-depth: 0 + +<%= render_step('lang/js/setup') %> + +<%= render_step('lang/js/read-version') %> + +<%= render_step('release/lang/js/validate-channel') %> + +<%= render_step('release/validate', version: "${{ steps.read-version.outputs.version }}", tag_format: "${{ inputs.tag_format }}") %> + +<%= render_step('lang/js/build') %> + +<%= render_step('lang/js/env-dump', if: "${{ failure() }}") %> diff --git a/templates/steps/lang/js/build.yml.erb b/templates/steps/lang/js/build.yml.erb new file mode 100644 index 0000000..69b8ac1 --- /dev/null +++ b/templates/steps/lang/js/build.yml.erb @@ -0,0 +1,17 @@ +<%# + Runs the package's build script with the selected package manager. + Composed into both validate (to confirm the package builds before the + approval gate) and publish (to produce the artifact npm publishes). + + @param package_manager [String] expr for pnpm | npm | yarn. Default: ${{ inputs.package_manager }} + @param build_command [String] expr for the npm script name. Default: ${{ inputs.build_command }} + @param working_directory [String] expr for the package root. Default: ${{ inputs.working_directory }} +-%> +- name: Build + shell: bash + working-directory: <%= locals.fetch(:working_directory) { "${{ inputs.working_directory }}" } %> + env: + PM: <%= locals.fetch(:package_manager) { "${{ inputs.package_manager }}" } %> + BUILD_COMMAND: <%= locals.fetch(:build_command) { "${{ inputs.build_command }}" } %> + run: | + "$PM" run "$BUILD_COMMAND" diff --git a/templates/steps/lang/js/env-dump.yml.erb b/templates/steps/lang/js/env-dump.yml.erb new file mode 100644 index 0000000..07dffa0 --- /dev/null +++ b/templates/steps/lang/js/env-dump.yml.erb @@ -0,0 +1,23 @@ +<%# + Prints environment diagnostics (Node/npm/package-manager versions, + the configured registry, the package.json version) to help debug a failed + release. Run from the package root. + + @param package_manager [String] expr for pnpm | npm | yarn. Default: ${{ inputs.package_manager }} + @param working_directory [String] expr for the package root. Default: ${{ inputs.working_directory }} +-%> +- name: Dump JavaScript environment + shell: bash + working-directory: <%= locals.fetch(:working_directory) { "${{ inputs.working_directory }}" } %> + env: + PM: <%= locals.fetch(:package_manager) { "${{ inputs.package_manager }}" } %> + run: | + echo "=== Node environment ===" + node --version + npm --version + echo "=== Package manager ($PM) ===" + "$PM" --version || echo "(unavailable)" + echo "=== Registry ===" + npm config get registry + echo "=== package.json version ===" + node -p "require('./package.json').version" 2>/dev/null || echo "(unavailable)" diff --git a/templates/steps/lang/js/read-version.yml.erb b/templates/steps/lang/js/read-version.yml.erb new file mode 100644 index 0000000..466fd94 --- /dev/null +++ b/templates/steps/lang/js/read-version.yml.erb @@ -0,0 +1,21 @@ +<%# + Resolves the package version into a `version` output. Uses the + explicit override when given, otherwise reads the `version` field from + package.json. Sets id: read-version. Run from the package root. + + @param version [String] expr for an explicit version override. Default: ${{ inputs.version }} + @param working_directory [String] expr for the package root. Default: ${{ inputs.working_directory }} +-%> +- name: Read version + id: read-version + shell: bash + working-directory: <%= locals.fetch(:working_directory) { "${{ inputs.working_directory }}" } %> + env: + VERSION_OVERRIDE: <%= locals.fetch(:version) { "${{ inputs.version }}" } %> + run: | + if [ -n "$VERSION_OVERRIDE" ]; then + echo "version=$VERSION_OVERRIDE" >> $GITHUB_OUTPUT + else + VERSION=$(node -p "require('./package.json').version") + echo "version=$VERSION" >> $GITHUB_OUTPUT + fi diff --git a/templates/steps/lang/js/setup.yml.erb b/templates/steps/lang/js/setup.yml.erb new file mode 100644 index 0000000..9f5f68b --- /dev/null +++ b/templates/steps/lang/js/setup.yml.erb @@ -0,0 +1,41 @@ +<%# + Sets up Node and the selected package manager, then installs the + dependencies (frozen). The package-manager `if:` selects which runtime to + install — genuine control flow that can't be a shell self-guard because the + pnpm install is a `uses:` action, not a `run:` step. + + @param node_version [String] expr for the Node version. Default: ${{ inputs.node_version }} + @param package_manager [String] expr for pnpm | npm | yarn. Default: ${{ inputs.package_manager }} + @param pnpm_version [String] expr for the pnpm version (pnpm). Default: ${{ inputs.pnpm_version }} + @param registry [String] expr for the npm registry URL. Default: ${{ inputs.registry }} + @param working_directory [String] expr for the package root. Default: ${{ inputs.working_directory }} +-%> +- name: Set up pnpm + if: ${{ <%= locals.fetch(:package_manager) { "inputs.package_manager" } %> == 'pnpm' }} + uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v5.0.0 + with: + version: <%= locals.fetch(:pnpm_version) { "${{ inputs.pnpm_version }}" } %> + +- name: Set up Node + uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 + with: + node-version: <%= locals.fetch(:node_version) { "${{ inputs.node_version }}" } %> + registry-url: <%= locals.fetch(:registry) { "${{ inputs.registry }}" } %> + cache: <%= locals.fetch(:package_manager) { "${{ inputs.package_manager }}" } %> + cache-dependency-path: | + <%= locals.fetch(:working_directory) { "${{ inputs.working_directory }}" } %>/pnpm-lock.yaml + <%= locals.fetch(:working_directory) { "${{ inputs.working_directory }}" } %>/package-lock.json + <%= locals.fetch(:working_directory) { "${{ inputs.working_directory }}" } %>/yarn.lock + +- name: Install dependencies + shell: bash + working-directory: <%= locals.fetch(:working_directory) { "${{ inputs.working_directory }}" } %> + env: + PM: <%= locals.fetch(:package_manager) { "${{ inputs.package_manager }}" } %> + run: | + case "$PM" in + pnpm) pnpm install --frozen-lockfile ;; + yarn) yarn install --frozen-lockfile ;; + npm) npm ci ;; + *) echo "Unknown package_manager: $PM"; exit 1 ;; + esac diff --git a/templates/steps/release/lang/js/notify-published.yml.erb b/templates/steps/release/lang/js/notify-published.yml.erb new file mode 100644 index 0000000..09a9f98 --- /dev/null +++ b/templates/steps/release/lang/js/notify-published.yml.erb @@ -0,0 +1,21 @@ +<%# + Builds the npm links JSON, then posts the shared release completion + notification (release/notify-published) with those links. The version comes + in explicitly (inputs.version) rather than being parsed off the tag, since the + tag format is configurable (e.g. js-{version}). +-%> +- name: Build npm links + id: links + shell: bash + env: + PACKAGE_NAME: ${{ inputs.package_name }} + VERSION: ${{ inputs.version }} + DRY_RUN: ${{ inputs.dry_run }} + run: | + LINKS='[]' + if [ "$DRY_RUN" != "true" ] && [ -n "$PACKAGE_NAME" ] && [ -n "$VERSION" ]; then + LINKS="[{\"label\":\"npm\",\"url\":\"https://www.npmjs.com/package/$PACKAGE_NAME/v/$VERSION\"}]" + fi + echo "links=$LINKS" >> $GITHUB_OUTPUT + +<%= render_step('release/notify-published', indent: 0, links: "${{ steps.links.outputs.links }}") %> diff --git a/templates/steps/release/lang/js/npm-publish.yml.erb b/templates/steps/release/lang/js/npm-publish.yml.erb new file mode 100644 index 0000000..f0e3515 --- /dev/null +++ b/templates/steps/release/lang/js/npm-publish.yml.erb @@ -0,0 +1,40 @@ +<%# + Publishes the package to the npm registry with OIDC trusted + publishing (no token) and, optionally, provenance/attestation. On a dry run, + packs without pushing. Self-guards on the `npm` toggle and `dry_run` in shell, + so the composing action needs no `if:` on it. Assumes Node + the package + manager are already set up and the package is built. + + npm is pinned to inputs.npm_version (not @latest): tokenless OIDC trusted + publishing requires npm >= 11.5.1, which is newer than the npm bundled with + setup-node. Provenance requires a public package and `id-token: write`. +-%> +- name: Publish to npm + shell: bash + working-directory: ${{ inputs.working_directory }} + env: + NPM_ENABLED: ${{ inputs.npm }} + DRY_RUN: ${{ inputs.dry_run }} + CHANNEL: ${{ inputs.channel }} + ACCESS: ${{ inputs.access }} + PROVENANCE: ${{ inputs.provenance }} + REGISTRY: ${{ inputs.registry }} + NPM_VERSION: ${{ inputs.npm_version }} + # Force OIDC trusted publishing: clear any inherited token so npm can't fall + # back to token auth. + NODE_AUTH_TOKEN: '' + NPM_TOKEN: '' + run: | + if [ "$NPM_ENABLED" != "true" ]; then + echo "npm publishing disabled; skipping." + exit 0 + fi + npm install -g "npm@$NPM_VERSION" + ARGS=(--tag "$CHANNEL" --access "$ACCESS" --registry "$REGISTRY") + [ "$PROVENANCE" = "true" ] && ARGS+=(--provenance) + if [ "$DRY_RUN" = "true" ]; then + echo "DRY RUN: npm publish ${ARGS[*]} --dry-run" + npm publish "${ARGS[@]}" --dry-run + else + npm publish "${ARGS[@]}" + fi diff --git a/templates/steps/release/lang/js/on-failure.yml.erb b/templates/steps/release/lang/js/on-failure.yml.erb new file mode 100644 index 0000000..89d72eb --- /dev/null +++ b/templates/steps/release/lang/js/on-failure.yml.erb @@ -0,0 +1,9 @@ +<%# + The publish failure handler — dumps the JavaScript environment for + diagnostics and posts a Slack failure notification. Composed as the terminal + step of js/publish under `if: failure()`; the composing action stamps that + guard onto each step here, and slack/send self-guards when Slack isn't set up. +-%> +<%= render_step('lang/js/env-dump', indent: 0) %> + +<%= render_step('release/notify-failure', indent: 0, failed_step: "publish", mention: "${{ inputs.failure_slack_mention }}") %> diff --git a/templates/steps/release/lang/js/validate-channel.yml.erb b/templates/steps/release/lang/js/validate-channel.yml.erb new file mode 100644 index 0000000..7dc2437 --- /dev/null +++ b/templates/steps/release/lang/js/validate-channel.yml.erb @@ -0,0 +1,21 @@ +<%# + Fails unless the requested npm release channel (dist-tag) is a member + of the comma-separated allowlist. Composed into the validate action so an + unknown channel fails fast, before the approval gate. Parallels the + release_branch membership check in release/validate. + + @param channel [String] expr for the requested channel. Default: ${{ inputs.channel }} + @param allowed_channels [String] expr for the allowed channel set. Default: ${{ inputs.allowed_channels }} +-%> +- name: Validate release channel + shell: bash + env: + CHANNEL: <%= locals.fetch(:channel) { "${{ inputs.channel }}" } %> + ALLOWED: <%= locals.fetch(:allowed_channels) { "${{ inputs.allowed_channels }}" } %> + run: | + IFS=',' read -ra OK <<< "$ALLOWED" + for c in "${OK[@]}"; do + [ "$(echo "$c" | xargs)" = "$CHANNEL" ] && exit 0 + done + echo "Error: release channel '$CHANNEL' is not in the allowed set: $ALLOWED" + exit 1 diff --git a/test/release/js/.gitignore b/test/release/js/.gitignore new file mode 100644 index 0000000..1eae0cf --- /dev/null +++ b/test/release/js/.gitignore @@ -0,0 +1,2 @@ +dist/ +node_modules/ diff --git a/test/release/js/package.json b/test/release/js/package.json new file mode 100644 index 0000000..2eb3a08 --- /dev/null +++ b/test/release/js/package.json @@ -0,0 +1,22 @@ +{ + "name": "@braintrust/bt-publishing-test", + "version": "0.0.1", + "description": "Braintrust test package. Used only to validate release workflows end-to-end. Not a real package; do not use.", + "license": "Apache-2.0", + "repository": { + "type": "git", + "url": "git+https://github.com/braintrustdata/sdk-actions.git" + }, + "main": "dist/index.js", + "types": "dist/index.d.ts", + "files": [ + "dist" + ], + "publishConfig": { + "access": "public", + "provenance": true + }, + "scripts": { + "build": "node -e \"const fs=require('fs');fs.mkdirSync('dist',{recursive:true});fs.writeFileSync('dist/index.js','export {};\\n');fs.writeFileSync('dist/index.d.ts','export {};\\n')\"" + } +} diff --git a/test/release/js/pnpm-lock.yaml b/test/release/js/pnpm-lock.yaml new file mode 100644 index 0000000..9b60ae1 --- /dev/null +++ b/test/release/js/pnpm-lock.yaml @@ -0,0 +1,9 @@ +lockfileVersion: '9.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +importers: + + .: {} From e21c04acd322a4d9a84a17c533c43eeb981b9027 Mon Sep 17 00:00:00 2001 From: David Elner Date: Thu, 18 Jun 2026 00:27:55 +0000 Subject: [PATCH 2/3] Changed: Ruby test gem from bt-fake to bt-publishing-test --- .github/workflows/release-ruby.yml | 34 +++++++++++-------- actions/release/lang/ruby/validate/action.yml | 2 +- .../release/lang/ruby/validate.yml.erb | 2 +- test/release/ruby/Gemfile.lock | 4 +-- test/release/ruby/Rakefile | 14 ++++---- ...ake.gemspec => bt-publishing-test.gemspec} | 8 ++--- .../version.rb | 2 +- 7 files changed, 35 insertions(+), 31 deletions(-) rename test/release/ruby/{bt-fake.gemspec => bt-publishing-test.gemspec} (68%) rename test/release/ruby/lib/{bt_fake => bt_publishing_test}/version.rb (69%) diff --git a/.github/workflows/release-ruby.yml b/.github/workflows/release-ruby.yml index c280678..e75ea33 100644 --- a/.github/workflows/release-ruby.yml +++ b/.github/workflows/release-ruby.yml @@ -1,5 +1,5 @@ # -# Releases bt-fake — a dummy Ruby gem used to validate Braintrust release +# Releases bt-publishing-test — a dummy Ruby gem used to validate Braintrust release # workflow tooling end-to-end. This does NOT release the braintrust Ruby SDK. # # This workflow serves two purposes: @@ -38,7 +38,7 @@ # # ───────────────────────────────────────────────────────────────────────────── -name: Release Ruby (bt-fake) +name: Release Ruby (bt-publishing-test) on: # Auto dry-run on PRs that touch the generated actions or the workflows, to @@ -63,7 +63,7 @@ on: description: "Anchor for release notes: a tag name or commit SHA. If SHA, notes will be empty." type: string required: false - # bt-fake: fixed SHA anchor prevents indefinite changelog accumulation. + # bt-publishing-test: fixed SHA anchor prevents indefinite changelog accumulation. # Update this when you want to reset the notes baseline. default: '58a397193105516446c3042361913655bcece509' dry_run: @@ -79,13 +79,17 @@ jobs: outputs: version: ${{ steps.bump.outputs.version }} steps: - # bt-fake: derives next version by querying RubyGems.org and incrementing the + # bt-publishing-test: derives next version by querying RubyGems.org and incrementing the # patch. Falls back to 0.0.1 on first publish (404). Remove this job in real # SDK workflows; version comes from the version bump PR. - name: Bump version id: bump run: | - CURRENT=$(curl -sf https://rubygems.org/api/v1/gems/bt-fake.json | jq -r '.version' || echo "0.0.0") + # Gem may not exist yet (first release / new name) → curl 404s and the + # pipeline yields no version; default to 0.0.0 so the first publish is 0.0.1. + # (Robust whether or not the shell has pipefail set.) + CURRENT=$(curl -sf https://rubygems.org/api/v1/gems/bt-publishing-test.json 2>/dev/null | jq -r '.version // empty' 2>/dev/null || true) + CURRENT=${CURRENT:-0.0.0} IFS='.' read -r major minor patch <<< "$CURRENT" echo "version=$major.$minor.$((patch + 1))" >> $GITHUB_OUTPUT @@ -102,7 +106,7 @@ jobs: on_release_branch: ${{ steps.validate-release.outputs.on_release_branch }} commit_message: ${{ steps.validate-release.outputs.commit_message }} steps: - # bt-fake: checkout required to resolve local ./actions/... references. + # bt-publishing-test: checkout required to resolve local ./actions/... references. # Real SDK workflows use external braintrustdata/sdk-actions/...@sha references # and do not need this step. - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 @@ -111,7 +115,7 @@ jobs: id: validate-release uses: ./actions/release/lang/ruby/validate with: - # bt-fake: version passed directly from bump job — no version file needed. + # bt-publishing-test: version passed directly from bump job — no version file needed. # In a real Ruby project, replace `version` with: # version_file: lib/your_gem/version.rb # version_module: YourGem @@ -130,7 +134,7 @@ jobs: pr_list: ${{ steps.prepare.outputs.pr_list }} notes: ${{ steps.prepare.outputs.notes }} steps: - # bt-fake: checkout required because actions are referenced locally (./actions/...). + # bt-publishing-test: checkout required because actions are referenced locally (./actions/...). # Real SDK workflows reference actions externally (braintrustdata/sdk-actions/...@sha) # and do not need this step. - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 @@ -152,7 +156,7 @@ jobs: timeout-minutes: 5 permissions: {} steps: - # bt-fake: checkout required for local action resolution (see prepare job comment) + # bt-publishing-test: checkout required for local action resolution (see prepare job comment) - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - name: Notify release pending @@ -196,7 +200,7 @@ jobs: ref: ${{ inputs.sha || github.event.pull_request.head.sha }} fetch-depth: 0 - # bt-fake: update both version.rb and Gemfile.lock to the run-number version. + # bt-publishing-test: update both version.rb and Gemfile.lock to the run-number version. # The publish job checks out fresh so both files still reflect the placeholder. # Real SDK workflows omit this — both files are committed together in the bump PR. - name: Apply version to version.rb and Gemfile.lock @@ -204,19 +208,19 @@ jobs: VERSION: ${{ needs.bump.outputs.version }} run: | sed -i "s|VERSION = \".*\"|VERSION = \"$VERSION\"|" \ - test/release/ruby/lib/bt_fake/version.rb - sed -i "s|bt-fake ([0-9]*\.[0-9]*\.[0-9]*)|bt-fake ($VERSION)|" \ + test/release/ruby/lib/bt_publishing_test/version.rb + sed -i "s|bt-publishing-test ([0-9]*\.[0-9]*\.[0-9]*)|bt-publishing-test ($VERSION)|" \ test/release/ruby/Gemfile.lock - name: Publish uses: ./actions/release/lang/ruby/publish with: sha: ${{ inputs.sha || github.event.pull_request.head.sha }} - checkout: 'false' # bt-fake: skip internal checkout — version patching above must survive into the gem build + checkout: 'false' # bt-publishing-test: skip internal checkout — version patching above must survive into the gem build working_directory: test/release/ruby dry_run: ${{ inputs.dry_run || github.event_name == 'pull_request' }} release_tag: ${{ needs.validate.outputs.release_tag }} - github_release: 'false' # bt-fake: omit GitHub release to avoid tag/release pollution from repeated test runs + github_release: 'false' # bt-publishing-test: omit GitHub release to avoid tag/release pollution from repeated test runs notes: ${{ needs.prepare.outputs.notes }} prev_release: ${{ needs.validate.outputs.prev_release }} branch: ${{ needs.validate.outputs.branch }} @@ -229,4 +233,4 @@ jobs: slack_channel: ${{ github.event_name != 'pull_request' && vars.SLACK_SDK_RELEASE_CHANNEL || '' }} # slack_mention: '@sdk-eng' # failure_slack_mention: '@sdk-eng' - gem_name: bt-fake + gem_name: bt-publishing-test diff --git a/actions/release/lang/ruby/validate/action.yml b/actions/release/lang/ruby/validate/action.yml index 70ab823..b141b2b 100644 --- a/actions/release/lang/ruby/validate/action.yml +++ b/actions/release/lang/ruby/validate/action.yml @@ -16,7 +16,7 @@ inputs: version: description: > Explicit version override. If provided, version_file and version_module - are ignored. Use when the version is computed externally (e.g. bt-fake + are ignored. Use when the version is computed externally (e.g. bt-publishing-test auto-bump from run number). required: false default: '' diff --git a/templates/actions/release/lang/ruby/validate.yml.erb b/templates/actions/release/lang/ruby/validate.yml.erb index 801f740..6402ac7 100644 --- a/templates/actions/release/lang/ruby/validate.yml.erb +++ b/templates/actions/release/lang/ruby/validate.yml.erb @@ -15,7 +15,7 @@ inputs: version: description: > Explicit version override. If provided, version_file and version_module - are ignored. Use when the version is computed externally (e.g. bt-fake + are ignored. Use when the version is computed externally (e.g. bt-publishing-test auto-bump from run number). required: false default: '' diff --git a/test/release/ruby/Gemfile.lock b/test/release/ruby/Gemfile.lock index 5672c9b..d7a888e 100644 --- a/test/release/ruby/Gemfile.lock +++ b/test/release/ruby/Gemfile.lock @@ -1,7 +1,7 @@ PATH remote: . specs: - bt-fake (0.0.1) + bt-publishing-test (0.0.1) openssl (>= 3.3.1, < 5.0) GEM @@ -60,7 +60,7 @@ PLATFORMS x86_64-linux DEPENDENCIES - bt-fake! + bt-publishing-test! openssl (= 4.0.0) rake (~> 13.0) standard (~> 1.0) diff --git a/test/release/ruby/Rakefile b/test/release/ruby/Rakefile index b034cae..24b7d37 100644 --- a/test/release/ruby/Rakefile +++ b/test/release/ruby/Rakefile @@ -9,15 +9,15 @@ end desc "Build gem into pkg/" task :build do - require_relative "lib/bt_fake/version" - sh "gem build bt-fake.gemspec" + require_relative "lib/bt_publishing_test/version" + sh "gem build bt-publishing-test.gemspec" mkdir_p "pkg" - mv "bt-fake-#{BtFake::VERSION}.gem", "pkg/" + mv "bt-publishing-test-#{BtPublishingTest::VERSION}.gem", "pkg/" end namespace :release do task publish: [:lint, :build] do - gem_files = FileList["pkg/bt-fake-*.gem"] + gem_files = FileList["pkg/bt-publishing-test-*.gem"] raise "No gem file found after build" if gem_files.empty? if ENV["DRY_RUN"] == "true" @@ -29,8 +29,8 @@ namespace :release do end task :push_tag do - require_relative "lib/bt_fake/version" - tag = "v#{BtFake::VERSION}" + require_relative "lib/bt_publishing_test/version" + tag = "v#{BtPublishingTest::VERSION}" if ENV["DRY_RUN"] == "true" puts "DRY RUN: would push tag #{tag} to origin (skipped)" else @@ -42,7 +42,7 @@ namespace :release do end # Called by rubygems/release-gem in the release workflow. -# bt-fake: tag push omitted to avoid tag pollution from repeated test runs. +# bt-publishing-test: tag push omitted to avoid tag pollution from repeated test runs. # In real SDK workflows, include "release:push_tag" here. task release: ["release:publish"] do puts "✓ Release complete" diff --git a/test/release/ruby/bt-fake.gemspec b/test/release/ruby/bt-publishing-test.gemspec similarity index 68% rename from test/release/ruby/bt-fake.gemspec rename to test/release/ruby/bt-publishing-test.gemspec index 656df93..182ae2a 100644 --- a/test/release/ruby/bt-fake.gemspec +++ b/test/release/ruby/bt-publishing-test.gemspec @@ -1,13 +1,13 @@ # frozen_string_literal: true -require_relative "lib/bt_fake/version" +require_relative "lib/bt_publishing_test/version" Gem::Specification.new do |spec| - spec.name = "bt-fake" - spec.version = BtFake::VERSION + spec.name = "bt-publishing-test" + spec.version = BtPublishingTest::VERSION spec.authors = ["Braintrust"] spec.email = ["info@braintrust.dev"] - spec.summary = "Braintrust dummy gem for testing purposes." + spec.summary = "Braintrust test package. Used only to validate release workflows end-to-end. Not a real package; do not use." spec.license = "Apache-2.0" spec.required_ruby_version = ">= 3.2.0" diff --git a/test/release/ruby/lib/bt_fake/version.rb b/test/release/ruby/lib/bt_publishing_test/version.rb similarity index 69% rename from test/release/ruby/lib/bt_fake/version.rb rename to test/release/ruby/lib/bt_publishing_test/version.rb index d1f1730..c315de2 100644 --- a/test/release/ruby/lib/bt_fake/version.rb +++ b/test/release/ruby/lib/bt_publishing_test/version.rb @@ -1,5 +1,5 @@ # frozen_string_literal: true -module BtFake +module BtPublishingTest VERSION = "0.0.1" end From 297403e239435b5f469aac1286cca38561c89483 Mon Sep 17 00:00:00 2001 From: David Elner Date: Wed, 17 Jun 2026 19:32:22 +0000 Subject: [PATCH 3/3] Added: label input to help distinguish multiple packages from one repository --- .github/workflows/release-js.yml | 2 ++ .github/workflows/release-ruby.yml | 2 ++ actions/release/lang/js/publish/action.yml | 34 +++++++++++++++--- actions/release/lang/ruby/publish/action.yml | 36 +++++++++++++++---- actions/release/notify-pending/action.yml | 22 ++++++++++-- .../actions/release/lang/js/publish.yml.erb | 7 ++++ .../actions/release/lang/ruby/publish.yml.erb | 9 ++++- .../actions/release/notify-pending.yml.erb | 22 ++++++++++-- .../steps/release/notify-failure.yml.erb | 7 +++- .../steps/release/notify-published.yml.erb | 20 ++++++++--- 10 files changed, 140 insertions(+), 21 deletions(-) diff --git a/.github/workflows/release-js.yml b/.github/workflows/release-js.yml index 7f2668a..8f75d0d 100644 --- a/.github/workflows/release-js.yml +++ b/.github/workflows/release-js.yml @@ -189,6 +189,7 @@ jobs: slack_token: ${{ github.event_name != 'pull_request' && secrets.SLACK_BOT_TOKEN || '' }} slack_channel: ${{ github.event_name != 'pull_request' && vars.SLACK_SDK_RELEASE_CHANNEL || '' }} # slack_mention: '@sdk-eng' + label: '@braintrust/bt-publishing-test' # distinguishes this from other packages in this repo emoji: ':javascript:' publish: @@ -234,6 +235,7 @@ jobs: github_release: 'false' # bt-publishing-test: omit GitHub release to avoid tag/release pollution from repeated test runs version: ${{ needs.bump.outputs.version }} package_name: '@braintrust/bt-publishing-test' + label: '@braintrust/bt-publishing-test' # distinguishes this from other packages in this repo access: 'public' provenance: 'true' channel: ${{ inputs.channel || 'latest' }} diff --git a/.github/workflows/release-ruby.yml b/.github/workflows/release-ruby.yml index e75ea33..b157201 100644 --- a/.github/workflows/release-ruby.yml +++ b/.github/workflows/release-ruby.yml @@ -177,6 +177,7 @@ jobs: slack_token: ${{ github.event_name != 'pull_request' && secrets.SLACK_BOT_TOKEN || '' }} slack_channel: ${{ github.event_name != 'pull_request' && vars.SLACK_SDK_RELEASE_CHANNEL || '' }} # slack_mention: '@sdk-eng' + label: bt-publishing-test # distinguishes this from other packages in this repo emoji: ':ruby:' publish: @@ -234,3 +235,4 @@ jobs: # slack_mention: '@sdk-eng' # failure_slack_mention: '@sdk-eng' gem_name: bt-publishing-test + label: bt-publishing-test # distinguishes this from other packages in this repo diff --git a/actions/release/lang/js/publish/action.yml b/actions/release/lang/js/publish/action.yml index 8be8a89..0d22a0a 100644 --- a/actions/release/lang/js/publish/action.yml +++ b/actions/release/lang/js/publish/action.yml @@ -95,6 +95,13 @@ inputs: description: "npm package name — used to build the npm link in notifications" required: false default: '' + label: + description: > + Display name for release notifications (e.g. the package name). When set, it + becomes the notification subject instead of the repository — useful when a repo + publishes multiple packages. Defaults to the repository. + required: false + default: '' slack_token: description: "Slack bot token" required: false @@ -238,6 +245,7 @@ runs: shell: bash env: TAG: ${{ inputs.release_tag }} + LABEL: ${{ inputs.label }} PREV_RELEASE: ${{ inputs.prev_release }} BRANCH: ${{ inputs.branch }} ON_RELEASE_BRANCH: ${{ inputs.on_release_branch }} @@ -248,10 +256,16 @@ runs: RELEASE_URL="$GITHUB_SERVER_URL/$GITHUB_REPOSITORY/releases/tag/$TAG" BRANCH_URL="$GITHUB_SERVER_URL/$GITHUB_REPOSITORY/tree/$BRANCH" + # Subject is the package label when set (disambiguates repos that publish + # multiple packages); otherwise the repository. The repo is still present in + # every link below, so it is never lost. + SUBJECT="$GITHUB_REPOSITORY" + [ -n "$LABEL" ] && SUBJECT="$LABEL" + if [ "$DRY_RUN" = "true" ]; then - echo "## $GITHUB_REPOSITORY $TAG — dry run complete" >> $GITHUB_STEP_SUMMARY + echo "## $SUBJECT $TAG — dry run complete" >> $GITHUB_STEP_SUMMARY else - echo "## $GITHUB_REPOSITORY $TAG — published ✅" >> $GITHUB_STEP_SUMMARY + echo "## $SUBJECT $TAG — published ✅" >> $GITHUB_STEP_SUMMARY fi echo "" >> $GITHUB_STEP_SUMMARY @@ -291,6 +305,7 @@ runs: shell: bash env: TAG: ${{ inputs.release_tag }} + LABEL: ${{ inputs.label }} EMOJI: ${{ inputs.emoji }} BRANCH: ${{ inputs.branch }} ON_RELEASE_BRANCH: ${{ inputs.on_release_branch }} @@ -303,6 +318,10 @@ runs: run: | RELEASE_URL="$GITHUB_SERVER_URL/$GITHUB_REPOSITORY/releases/tag/$TAG" + # Subject is the package label when set, else the repository (see summary step). + SUBJECT="$GITHUB_REPOSITORY" + [ -n "$LABEL" ] && SUBJECT="$LABEL" + DIFF_PART="" if [ -n "$PREV_RELEASE" ]; then DIFF_URL="$GITHUB_SERVER_URL/$GITHUB_REPOSITORY/compare/${PREV_RELEASE}...$TAG" @@ -312,10 +331,10 @@ runs: PR_LIST=$(echo "$PR_LIST_RAW" | tr $'\x1f' '\n' | sed '/^$/d') if [ "$DRY_RUN" = "true" ]; then - TEXT="$EMOJI *$GITHUB_REPOSITORY $TAG* complete" + TEXT="$EMOJI *$SUBJECT $TAG* complete" TEXT="$TEXT\n> :information_source: _Dry run: nothing tagged or published._" else - TEXT="$EMOJI *$GITHUB_REPOSITORY $TAG* published" + TEXT="$EMOJI *$SUBJECT $TAG* published" fi if [ "$ON_RELEASE_BRANCH" = "false" ]; then @@ -399,12 +418,17 @@ runs: shell: bash env: RELEASE_TAG: ${{ inputs.release_tag }} + LABEL: ${{ inputs.label }} STEP: publish SLACK_MENTION: ${{ inputs.failure_slack_mention }} RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} ACTOR_LINK: <${{ github.server_url }}/${{ github.actor }}|${{ github.actor }}> run: | - TEXT=":x: *$GITHUB_REPOSITORY $RELEASE_TAG* failed at $STEP" + # Subject is the package label when set, else the repository. + SUBJECT="$GITHUB_REPOSITORY" + [ -n "$LABEL" ] && SUBJECT="$LABEL" + + TEXT=":x: *$SUBJECT $RELEASE_TAG* failed at $STEP" TEXT="$TEXT\nInitiated by: $ACTOR_LINK" if [ -n "$SLACK_MENTION" ]; then diff --git a/actions/release/lang/ruby/publish/action.yml b/actions/release/lang/ruby/publish/action.yml index 7a2aed8..8bda7fa 100644 --- a/actions/release/lang/ruby/publish/action.yml +++ b/actions/release/lang/ruby/publish/action.yml @@ -1,5 +1,5 @@ # GENERATED by scripts/generate.rb — edit templates/, not this file. -name: Publish Ruby SDK Release +name: Publish Ruby Package description: > Full Ruby gem release: checks out the SHA, pushes to RubyGems, creates a GitHub release, and sends a Slack completion notification. Covers the @@ -54,6 +54,13 @@ inputs: description: "RubyGems gem name — used to build the RubyGems link in notifications" required: false default: '' + label: + description: > + Display name for release notifications (e.g. the gem name). When set, it becomes + the notification subject instead of the repository — useful when a repo publishes + multiple packages. Defaults to the repository. + required: false + default: '' slack_token: description: "Slack bot token" required: false @@ -147,6 +154,7 @@ runs: shell: bash env: TAG: ${{ inputs.release_tag }} + LABEL: ${{ inputs.label }} PREV_RELEASE: ${{ inputs.prev_release }} BRANCH: ${{ inputs.branch }} ON_RELEASE_BRANCH: ${{ inputs.on_release_branch }} @@ -157,10 +165,16 @@ runs: RELEASE_URL="$GITHUB_SERVER_URL/$GITHUB_REPOSITORY/releases/tag/$TAG" BRANCH_URL="$GITHUB_SERVER_URL/$GITHUB_REPOSITORY/tree/$BRANCH" + # Subject is the package label when set (disambiguates repos that publish + # multiple packages); otherwise the repository. The repo is still present in + # every link below, so it is never lost. + SUBJECT="$GITHUB_REPOSITORY" + [ -n "$LABEL" ] && SUBJECT="$LABEL" + if [ "$DRY_RUN" = "true" ]; then - echo "## $GITHUB_REPOSITORY $TAG — dry run complete" >> $GITHUB_STEP_SUMMARY + echo "## $SUBJECT $TAG — dry run complete" >> $GITHUB_STEP_SUMMARY else - echo "## $GITHUB_REPOSITORY $TAG — published ✅" >> $GITHUB_STEP_SUMMARY + echo "## $SUBJECT $TAG — published ✅" >> $GITHUB_STEP_SUMMARY fi echo "" >> $GITHUB_STEP_SUMMARY @@ -200,6 +214,7 @@ runs: shell: bash env: TAG: ${{ inputs.release_tag }} + LABEL: ${{ inputs.label }} EMOJI: ${{ inputs.emoji }} BRANCH: ${{ inputs.branch }} ON_RELEASE_BRANCH: ${{ inputs.on_release_branch }} @@ -212,6 +227,10 @@ runs: run: | RELEASE_URL="$GITHUB_SERVER_URL/$GITHUB_REPOSITORY/releases/tag/$TAG" + # Subject is the package label when set, else the repository (see summary step). + SUBJECT="$GITHUB_REPOSITORY" + [ -n "$LABEL" ] && SUBJECT="$LABEL" + DIFF_PART="" if [ -n "$PREV_RELEASE" ]; then DIFF_URL="$GITHUB_SERVER_URL/$GITHUB_REPOSITORY/compare/${PREV_RELEASE}...$TAG" @@ -221,10 +240,10 @@ runs: PR_LIST=$(echo "$PR_LIST_RAW" | tr $'\x1f' '\n' | sed '/^$/d') if [ "$DRY_RUN" = "true" ]; then - TEXT="$EMOJI *$GITHUB_REPOSITORY $TAG* complete" + TEXT="$EMOJI *$SUBJECT $TAG* complete" TEXT="$TEXT\n> :information_source: _Dry run: nothing tagged or published._" else - TEXT="$EMOJI *$GITHUB_REPOSITORY $TAG* published" + TEXT="$EMOJI *$SUBJECT $TAG* published" fi if [ "$ON_RELEASE_BRANCH" = "false" ]; then @@ -304,12 +323,17 @@ runs: shell: bash env: RELEASE_TAG: ${{ inputs.release_tag }} + LABEL: ${{ inputs.label }} STEP: publish SLACK_MENTION: ${{ inputs.failure_slack_mention }} RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} ACTOR_LINK: <${{ github.server_url }}/${{ github.actor }}|${{ github.actor }}> run: | - TEXT=":x: *$GITHUB_REPOSITORY $RELEASE_TAG* failed at $STEP" + # Subject is the package label when set, else the repository. + SUBJECT="$GITHUB_REPOSITORY" + [ -n "$LABEL" ] && SUBJECT="$LABEL" + + TEXT=":x: *$SUBJECT $RELEASE_TAG* failed at $STEP" TEXT="$TEXT\nInitiated by: $ACTOR_LINK" if [ -n "$SLACK_MENTION" ]; then diff --git a/actions/release/notify-pending/action.yml b/actions/release/notify-pending/action.yml index cefa791..aa9ad99 100644 --- a/actions/release/notify-pending/action.yml +++ b/actions/release/notify-pending/action.yml @@ -52,6 +52,13 @@ inputs: description: "Emoji prefix for Slack messages" required: false default: ':package:' + label: + description: > + Display name for the thing being released (e.g. the package name). When set, + it becomes the notification subject instead of the repository — useful when a + repo publishes multiple packages. Defaults to the repository. + required: false + default: '' runs: using: composite @@ -60,6 +67,7 @@ runs: shell: bash env: TAG: ${{ inputs.release_tag }} + LABEL: ${{ inputs.label }} COMMIT_MSG: ${{ inputs.commit_message }} NOTES_B64: ${{ inputs.notes }} BRANCH: ${{ inputs.branch }} @@ -77,7 +85,12 @@ runs: BRANCH_LABEL="$BRANCH_LABEL ⚠️" fi - echo "## $GITHUB_REPOSITORY $TAG" >> $GITHUB_STEP_SUMMARY + # Subject is the package label when set, else the repository. The repo + # remains in every link below, so it is never lost. + SUBJECT="$GITHUB_REPOSITORY" + [ -n "$LABEL" ] && SUBJECT="$LABEL" + + echo "## $SUBJECT $TAG" >> $GITHUB_STEP_SUMMARY echo "" >> $GITHUB_STEP_SUMMARY if [ "$DRY_RUN" = "true" ]; then @@ -110,6 +123,7 @@ runs: shell: bash env: TAG: ${{ inputs.release_tag }} + LABEL: ${{ inputs.label }} EMOJI: ${{ inputs.emoji }} BRANCH: ${{ inputs.branch }} ON_RELEASE_BRANCH: ${{ inputs.on_release_branch }} @@ -121,6 +135,10 @@ runs: ACTOR_LINK: <${{ github.server_url }}/${{ github.actor }}|${{ github.actor }}> PR_LIST_RAW: ${{ inputs.pr_list }} run: | + # Subject is the package label when set, else the repository. + SUBJECT="$GITHUB_REPOSITORY" + [ -n "$LABEL" ] && SUBJECT="$LABEL" + BRANCH_URL="$GITHUB_SERVER_URL/$GITHUB_REPOSITORY/tree/$BRANCH" BRANCH_LINK="<$BRANCH_URL|$BRANCH>" if [ "$ON_RELEASE_BRANCH" = "false" ]; then @@ -137,7 +155,7 @@ runs: PR_LIST=$(echo "$PR_LIST_RAW" | tr $'\x1f' '\n' | sed '/^$/d') - TEXT="$EMOJI *$GITHUB_REPOSITORY $TAG* awaiting approval" + TEXT="$EMOJI *$SUBJECT $TAG* awaiting approval" TEXT="$TEXT\nInitiated by: $ACTOR_LINK" if [ "$DRY_RUN" = "true" ]; then diff --git a/templates/actions/release/lang/js/publish.yml.erb b/templates/actions/release/lang/js/publish.yml.erb index 67e0249..bcc2759 100644 --- a/templates/actions/release/lang/js/publish.yml.erb +++ b/templates/actions/release/lang/js/publish.yml.erb @@ -94,6 +94,13 @@ inputs: description: "npm package name — used to build the npm link in notifications" required: false default: '' + label: + description: > + Display name for release notifications (e.g. the package name). When set, it + becomes the notification subject instead of the repository — useful when a repo + publishes multiple packages. Defaults to the repository. + required: false + default: '' slack_token: description: "Slack bot token" required: false diff --git a/templates/actions/release/lang/ruby/publish.yml.erb b/templates/actions/release/lang/ruby/publish.yml.erb index 7e5496e..816f250 100644 --- a/templates/actions/release/lang/ruby/publish.yml.erb +++ b/templates/actions/release/lang/ruby/publish.yml.erb @@ -1,4 +1,4 @@ -name: Publish Ruby SDK Release +name: Publish Ruby Package description: > Full Ruby gem release: checks out the SHA, pushes to RubyGems, creates a GitHub release, and sends a Slack completion notification. Covers the @@ -53,6 +53,13 @@ inputs: description: "RubyGems gem name — used to build the RubyGems link in notifications" required: false default: '' + label: + description: > + Display name for release notifications (e.g. the gem name). When set, it becomes + the notification subject instead of the repository — useful when a repo publishes + multiple packages. Defaults to the repository. + required: false + default: '' slack_token: description: "Slack bot token" required: false diff --git a/templates/actions/release/notify-pending.yml.erb b/templates/actions/release/notify-pending.yml.erb index 126cb2f..dd619dd 100644 --- a/templates/actions/release/notify-pending.yml.erb +++ b/templates/actions/release/notify-pending.yml.erb @@ -51,6 +51,13 @@ inputs: description: "Emoji prefix for Slack messages" required: false default: ':package:' + label: + description: > + Display name for the thing being released (e.g. the package name). When set, + it becomes the notification subject instead of the repository — useful when a + repo publishes multiple packages. Defaults to the repository. + required: false + default: '' runs: using: composite @@ -59,6 +66,7 @@ runs: shell: bash env: TAG: ${{ inputs.release_tag }} + LABEL: ${{ inputs.label }} COMMIT_MSG: ${{ inputs.commit_message }} NOTES_B64: ${{ inputs.notes }} BRANCH: ${{ inputs.branch }} @@ -76,7 +84,12 @@ runs: BRANCH_LABEL="$BRANCH_LABEL ⚠️" fi - echo "## $GITHUB_REPOSITORY $TAG" >> $GITHUB_STEP_SUMMARY + # Subject is the package label when set, else the repository. The repo + # remains in every link below, so it is never lost. + SUBJECT="$GITHUB_REPOSITORY" + [ -n "$LABEL" ] && SUBJECT="$LABEL" + + echo "## $SUBJECT $TAG" >> $GITHUB_STEP_SUMMARY echo "" >> $GITHUB_STEP_SUMMARY if [ "$DRY_RUN" = "true" ]; then @@ -109,6 +122,7 @@ runs: shell: bash env: TAG: ${{ inputs.release_tag }} + LABEL: ${{ inputs.label }} EMOJI: ${{ inputs.emoji }} BRANCH: ${{ inputs.branch }} ON_RELEASE_BRANCH: ${{ inputs.on_release_branch }} @@ -120,6 +134,10 @@ runs: ACTOR_LINK: <${{ github.server_url }}/${{ github.actor }}|${{ github.actor }}> PR_LIST_RAW: ${{ inputs.pr_list }} run: | + # Subject is the package label when set, else the repository. + SUBJECT="$GITHUB_REPOSITORY" + [ -n "$LABEL" ] && SUBJECT="$LABEL" + BRANCH_URL="$GITHUB_SERVER_URL/$GITHUB_REPOSITORY/tree/$BRANCH" BRANCH_LINK="<$BRANCH_URL|$BRANCH>" if [ "$ON_RELEASE_BRANCH" = "false" ]; then @@ -136,7 +154,7 @@ runs: PR_LIST=$(echo "$PR_LIST_RAW" | tr $'\x1f' '\n' | sed '/^$/d') - TEXT="$EMOJI *$GITHUB_REPOSITORY $TAG* awaiting approval" + TEXT="$EMOJI *$SUBJECT $TAG* awaiting approval" TEXT="$TEXT\nInitiated by: $ACTOR_LINK" if [ "$DRY_RUN" = "true" ]; then diff --git a/templates/steps/release/notify-failure.yml.erb b/templates/steps/release/notify-failure.yml.erb index 9ebf016..ae90f21 100644 --- a/templates/steps/release/notify-failure.yml.erb +++ b/templates/steps/release/notify-failure.yml.erb @@ -11,12 +11,17 @@ shell: bash env: RELEASE_TAG: ${{ inputs.release_tag }} + LABEL: ${{ inputs.label }} STEP: <%= locals.fetch(:failed_step) { "${{ inputs.step }}" } %> SLACK_MENTION: <%= locals.fetch(:mention) { "${{ inputs.slack_mention }}" } %> RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} ACTOR_LINK: <${{ github.server_url }}/${{ github.actor }}|${{ github.actor }}> run: | - TEXT=":x: *$GITHUB_REPOSITORY $RELEASE_TAG* failed at $STEP" + # Subject is the package label when set, else the repository. + SUBJECT="$GITHUB_REPOSITORY" + [ -n "$LABEL" ] && SUBJECT="$LABEL" + + TEXT=":x: *$SUBJECT $RELEASE_TAG* failed at $STEP" TEXT="$TEXT\nInitiated by: $ACTOR_LINK" if [ -n "$SLACK_MENTION" ]; then diff --git a/templates/steps/release/notify-published.yml.erb b/templates/steps/release/notify-published.yml.erb index b162dbf..3857e3d 100644 --- a/templates/steps/release/notify-published.yml.erb +++ b/templates/steps/release/notify-published.yml.erb @@ -10,6 +10,7 @@ shell: bash env: TAG: ${{ inputs.release_tag }} + LABEL: ${{ inputs.label }} PREV_RELEASE: ${{ inputs.prev_release }} BRANCH: ${{ inputs.branch }} ON_RELEASE_BRANCH: ${{ inputs.on_release_branch }} @@ -20,10 +21,16 @@ RELEASE_URL="$GITHUB_SERVER_URL/$GITHUB_REPOSITORY/releases/tag/$TAG" BRANCH_URL="$GITHUB_SERVER_URL/$GITHUB_REPOSITORY/tree/$BRANCH" + # Subject is the package label when set (disambiguates repos that publish + # multiple packages); otherwise the repository. The repo is still present in + # every link below, so it is never lost. + SUBJECT="$GITHUB_REPOSITORY" + [ -n "$LABEL" ] && SUBJECT="$LABEL" + if [ "$DRY_RUN" = "true" ]; then - echo "## $GITHUB_REPOSITORY $TAG — dry run complete" >> $GITHUB_STEP_SUMMARY + echo "## $SUBJECT $TAG — dry run complete" >> $GITHUB_STEP_SUMMARY else - echo "## $GITHUB_REPOSITORY $TAG — published ✅" >> $GITHUB_STEP_SUMMARY + echo "## $SUBJECT $TAG — published ✅" >> $GITHUB_STEP_SUMMARY fi echo "" >> $GITHUB_STEP_SUMMARY @@ -63,6 +70,7 @@ shell: bash env: TAG: ${{ inputs.release_tag }} + LABEL: ${{ inputs.label }} EMOJI: ${{ inputs.emoji }} BRANCH: ${{ inputs.branch }} ON_RELEASE_BRANCH: ${{ inputs.on_release_branch }} @@ -75,6 +83,10 @@ run: | RELEASE_URL="$GITHUB_SERVER_URL/$GITHUB_REPOSITORY/releases/tag/$TAG" + # Subject is the package label when set, else the repository (see summary step). + SUBJECT="$GITHUB_REPOSITORY" + [ -n "$LABEL" ] && SUBJECT="$LABEL" + DIFF_PART="" if [ -n "$PREV_RELEASE" ]; then DIFF_URL="$GITHUB_SERVER_URL/$GITHUB_REPOSITORY/compare/${PREV_RELEASE}...$TAG" @@ -84,10 +96,10 @@ PR_LIST=$(echo "$PR_LIST_RAW" | tr $'\x1f' '\n' | sed '/^$/d') if [ "$DRY_RUN" = "true" ]; then - TEXT="$EMOJI *$GITHUB_REPOSITORY $TAG* complete" + TEXT="$EMOJI *$SUBJECT $TAG* complete" TEXT="$TEXT\n> :information_source: _Dry run: nothing tagged or published._" else - TEXT="$EMOJI *$GITHUB_REPOSITORY $TAG* published" + TEXT="$EMOJI *$SUBJECT $TAG* published" fi if [ "$ON_RELEASE_BRANCH" = "false" ]; then