diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 9037ce1..5edd4b8 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -1,15 +1,10 @@ -# -# Release workflow for the Ruby SDK. -# Triggered manually via GitHub Actions UI — requires an explicit commit SHA. -# Version is read from version.rb at that SHA; a version bump PR is always required before releasing. -# Covers all release types: standard releases, backports, hotfixes, and release candidates. -# -# Generated from the canonical Ruby release template in braintrustdata/sdk-actions: -# https://github.com/braintrustdata/sdk-actions/blob/e055cc17a5317e10ea94c8328cf79dcdaeda6f7f/.github/workflows/release-ruby.yml -# +# GENERATED by sdk-actions `bin/workflow` — update with: bin/workflow update +# sdk-actions: {"template":"release/ruby/turnkey","ref":"71731d4a726df3e3bf42e50c2da4a75842b038dd","version":"1.0.0","params":{"dry_run_environment":"publish-dry-run","emoji":":ruby:","gem_name":"braintrust","publish_environment":"publish","ruby_version":"","slack_channel_var":"SLACK_SDK_RELEASE_CHANNEL","slack_token_secret":"SLACK_BOT_TOKEN","version_file":"lib/braintrust/version.rb","version_module":"Braintrust","workflow_name":"Release Ruby SDK","working_directory":"."}} name: Release Ruby SDK +# Releases braintrust to RubyGems via braintrustdata/sdk-actions. Dispatch after merging +# the commit that bumps Braintrust::VERSION; the release runs against that commit's SHA. on: workflow_dispatch: inputs: @@ -18,106 +13,120 @@ on: type: string default: "I have merged a version bump PR" required: false + release_type: + description: "Release type (stable → GitHub release; prerelease → none)" + type: choice + default: stable + options: [stable, prerelease] sha: description: "Commit SHA (of the version bump) to release" required: true type: string prev_release: - description: "(Optional) Tag or SHA of previous release. Specify if previous tag in git history was not the previous release." + description: "Release-notes anchor: a tag or SHA (empty → the previous tag)" type: string required: false dry_run: - description: "Dry run: Build without tagging or publishing" + description: "Dry run: build without tagging or publishing" type: boolean default: false jobs: - validate: + # FACT-FIND — derive version/tag/notes, check the registry, and emit the package manifest. + # contents: write is for the releases/generate-notes API. + configure: runs-on: ubuntu-24.04 - timeout-minutes: 10 + timeout-minutes: 5 permissions: - contents: read + contents: write outputs: - release_tag: ${{ steps.validate-release.outputs.release_tag }} - commit_message: ${{ steps.validate-release.outputs.commit_message }} - branch: ${{ steps.validate-release.outputs.branch }} - on_release_branch: ${{ steps.validate-release.outputs.on_release_branch }} - prev_release: ${{ steps.validate-release.outputs.prev_release }} + version: ${{ steps.configure.outputs.version }} + release_tag: ${{ steps.configure.outputs.release_tag }} + prev_release: ${{ steps.configure.outputs.prev_release }} + branch: ${{ steps.configure.outputs.branch }} + on_release_branch: ${{ steps.configure.outputs.on_release_branch }} + commit_message: ${{ steps.configure.outputs.commit_message }} + github_release: ${{ steps.configure.outputs.github_release }} + already_published: ${{ steps.configure.outputs.already_published }} + notes: ${{ steps.configure.outputs.notes }} + package: ${{ steps.configure.outputs.package }} steps: - - name: Validate release - id: validate-release - uses: braintrustdata/sdk-actions/actions/release/lang/ruby/validate@e055cc17a5317e10ea94c8328cf79dcdaeda6f7f + - name: Configure release + id: configure + uses: braintrustdata/sdk-actions/actions/release/lang/ruby/configure@71731d4a726df3e3bf42e50c2da4a75842b038dd with: version_file: lib/braintrust/version.rb version_module: Braintrust sha: ${{ inputs.sha }} - dry_run: ${{ inputs.dry_run }} + working_directory: . + release_type: ${{ inputs.release_type }} + rubygems_package_name: braintrust + package_label: braintrust + emoji: ':ruby:' + prev_release: ${{ inputs.prev_release }} - prepare: - needs: validate + validate: + needs: [configure] runs-on: ubuntu-24.04 - timeout-minutes: 5 + timeout-minutes: 10 permissions: - contents: write # required for releases/generate-notes API - outputs: - pr_list: ${{ steps.prepare.outputs.pr_list }} - notes: ${{ steps.prepare.outputs.notes }} + contents: read steps: - - name: Prepare release - id: prepare - uses: braintrustdata/sdk-actions/actions/release/prepare@e055cc17a5317e10ea94c8328cf79dcdaeda6f7f + - name: Validate release + uses: braintrustdata/sdk-actions/actions/release/lang/ruby/validate@71731d4a726df3e3bf42e50c2da4a75842b038dd with: - release_tag: ${{ needs.validate.outputs.release_tag }} sha: ${{ inputs.sha }} - prev_release: ${{ inputs.prev_release || needs.validate.outputs.prev_release }} + dry_run: ${{ inputs.dry_run }} + working_directory: . + release_tag: ${{ needs.configure.outputs.release_tag }} + on_release_branch: ${{ needs.configure.outputs.on_release_branch }} + notes: ${{ needs.configure.outputs.notes }} + already_published: ${{ needs.configure.outputs.already_published }} - notify-pending: - needs: [validate, prepare] + request-approval: + needs: [configure, validate] runs-on: ubuntu-24.04 timeout-minutes: 5 permissions: {} steps: - - name: Notify release pending - uses: braintrustdata/sdk-actions/actions/release/notify-pending@e055cc17a5317e10ea94c8328cf79dcdaeda6f7f + - name: Request release approval + uses: braintrustdata/sdk-actions/actions/release/request-approval@71731d4a726df3e3bf42e50c2da4a75842b038dd with: + packages: '{"packages":[${{ needs.configure.outputs.package }}]}' sha: ${{ inputs.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 }} + branch: ${{ needs.configure.outputs.branch }} + on_release_branch: ${{ needs.configure.outputs.on_release_branch }} + commit_message: ${{ needs.configure.outputs.commit_message }} dry_run: ${{ inputs.dry_run }} slack_token: ${{ secrets.SLACK_BOT_TOKEN }} slack_channel: ${{ vars.SLACK_SDK_RELEASE_CHANNEL }} slack_mention: '@sdk-eng' - emoji: ':ruby:' - publish: - needs: [validate, prepare, notify-pending] + build-and-ship: + needs: [configure, validate, request-approval] runs-on: ubuntu-24.04 timeout-minutes: 15 - environment: ${{ inputs.dry_run && 'rubygems-publish-dry-run' || 'rubygems-publish' }} - + environment: ${{ inputs.dry_run && 'publish-dry-run' || 'publish' }} permissions: contents: write id-token: write - + attestations: write # signed SBOM + SLSA build provenance steps: - - name: Publish gem - uses: braintrustdata/sdk-actions/actions/release/lang/ruby/publish@e055cc17a5317e10ea94c8328cf79dcdaeda6f7f + - name: Build and ship + uses: braintrustdata/sdk-actions/actions/release/lang/ruby/build-and-ship@71731d4a726df3e3bf42e50c2da4a75842b038dd with: sha: ${{ inputs.sha }} - release_tag: ${{ needs.validate.outputs.release_tag }} + working_directory: . dry_run: ${{ inputs.dry_run }} - 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 }} + release_tag: ${{ needs.configure.outputs.release_tag }} + already_published: ${{ needs.configure.outputs.already_published }} + github_release: ${{ needs.configure.outputs.github_release }} + notes: ${{ needs.configure.outputs.notes }} + prev_release: ${{ needs.configure.outputs.prev_release }} + branch: ${{ needs.configure.outputs.branch }} + on_release_branch: ${{ needs.configure.outputs.on_release_branch }} slack_token: ${{ secrets.SLACK_BOT_TOKEN }} slack_channel: ${{ vars.SLACK_SDK_RELEASE_CHANNEL }} - # slack_mention: '@sdk-eng' - # failure_slack_mention: '@sdk-eng' gem_name: braintrust + label: braintrust + emoji: ':ruby:' diff --git a/PUBLISHING.md b/PUBLISHING.md index 76c9ca2..d96e387 100644 --- a/PUBLISHING.md +++ b/PUBLISHING.md @@ -1,15 +1,8 @@ # Publishing the Ruby SDK -The `braintrust` gem is published from GitHub Actions via the **Release Ruby SDK** workflow -([`.github/workflows/release.yml`](.github/workflows/release.yml)), which is generated from the -canonical Ruby release template in -[`braintrustdata/sdk-actions`](https://github.com/braintrustdata/sdk-actions) (pinned by commit SHA). -Do not publish from your local machine. +The `braintrust` gem is published from GitHub Actions via the **Release Ruby SDK** workflow ([`.github/workflows/release.yml`](.github/workflows/release.yml)). Do not publish from your local machine — there are no local publish or tag Rake tasks, and RubyGems accepts pushes only from this workflow. -The workflow is triggered manually (`workflow_dispatch`) and requires an explicit commit SHA. It -**reads the version from `lib/braintrust/version.rb` at that SHA** — the version cannot be overridden -from the workflow inputs, so a version bump must already be committed at the SHA you release. The -same workflow covers standard releases, backports, hotfixes, and release candidates. +The workflow is triggered manually (`workflow_dispatch`) and requires an explicit commit SHA. It **reads the version from `lib/braintrust/version.rb` at that SHA** — the version cannot be overridden from the workflow inputs, so a version bump must already be committed at the SHA you release. The same workflow covers standard releases, backports, hotfixes, and release candidates. ## Bumping the version @@ -23,32 +16,32 @@ rake version:bump:patch # or :minor, :major 1. Open a PR that bumps `lib/braintrust/version.rb` to the target version and merge it to `main`. 2. Copy the SHA of the version-bump **merge commit** on `main`. -3. Run the **Release Ruby SDK** workflow - (Actions → Release Ruby SDK → Run workflow) with: +3. Run the **Release Ruby SDK** workflow (Actions → Release Ruby SDK → Run workflow) with: + - **Release type** — `stable`. - **Commit SHA** — the merge commit SHA from step 2. - **Dry run** — leave unchecked for a real release. -4. Approve the `rubygems-publish` environment if prompted. +4. Approve the `publish` environment when prompted. -The workflow validates the release, generates release notes, posts a pending notice to Slack, then -publishes the gem to RubyGems via trusted publishing (OIDC — no long-lived tokens), tags the commit, -and creates the GitHub Release. +The workflow validates the release, generates release notes, posts a pending notice to Slack, then publishes the gem to RubyGems via trusted publishing (OIDC — no long-lived tokens), tags the commit, and creates the GitHub Release with a signed SBOM attached. ## Pre-release (release candidate / alpha) Release candidates use the same workflow; just release from a pre-release version on a branch. -1. Create a pre-release branch and commit `lib/braintrust/version.rb` set to the desired pre-release - version (e.g. `0.1.0.alpha2`). Copy the SHA of that commit. +1. Create a pre-release branch and commit `lib/braintrust/version.rb` set to the desired pre-release version (e.g. `0.1.0.alpha2`). Copy the SHA of that commit. 2. Run the **Release Ruby SDK** workflow with: + - **Release type** — `prerelease`. - **Commit SHA** — the version-bump commit on your pre-release branch. - - **Tag or SHA of previous release** — *(optional)* the version-bump SHA of the previous - pre-release. Set this when you want the release notes to diff against, say, `alpha1` only rather than against the last stable release. + - **Release-notes anchor** — *(optional)* the tag or version-bump SHA of the previous pre-release. Set this when you want the release notes to diff against, say, `alpha1` only rather than against the last stable release. Leave it empty to diff against the previous tag. Note that a full SHA (rather than a tag) yields empty notes. - **Dry run** — leave unchecked for a real pre-release. +> **`prerelease` publishes the gem but does not tag the commit or create a GitHub Release.** Choose `stable` if you want either of those. + +Releasing from a branch other than `main` produces a warning, not a failure. + ## Dry run -Check **Dry run** to exercise the workflow (build only) without tagging or publishing. It runs in the -`rubygems-publish-dry-run` environment and ships nothing. +Check **Dry run** to exercise the workflow (build only) without tagging or publishing. It runs in the `publish-dry-run` environment and ships nothing. ## Verify @@ -57,6 +50,49 @@ Spot-check that the release and changelog look right: - https://rubygems.org/gems/braintrust - https://github.com/braintrustdata/braintrust-sdk-ruby/releases +Confirm the published gem carries its SBOM and build provenance: + +```bash +gh attestation verify braintrust-.gem --repo braintrustdata/braintrust-sdk-ruby +``` + Then run the test app with the newly published gem: - https://github.com/braintrustdata/sdk-test-apps — `make verify-ruby` + +--- + +## Maintenance + +`release.yml` is **generated** from the `release/ruby/turnkey` template in [`braintrustdata/sdk-actions`](https://github.com/braintrustdata/sdk-actions), which supplies the release actions as SHA-pinned building blocks. Do not hand-edit it to pick up upstream changes; use the generator. + +### Updating to a newer sdk-actions + +From an `sdk-actions` checkout: + +```bash +ruby bin/workflow compare /path/to/braintrust-sdk-ruby/.github/workflows/release.yml # preview the delta +ruby bin/workflow update /path/to/braintrust-sdk-ruby/.github/workflows/release.yml # apply it +ruby bin/workflow validate /path/to/braintrust-sdk-ruby/.github/workflows/release.yml # schema-check +``` + +`update` 3-way merges the upstream delta and bumps the pinned ref, keeping local edits. Then: + +1. Check `git diff` — a major-version jump in the actions' `# sdk-actions:` header means breaking changes, so read the upstream changelog before merging. +2. Run `bash scripts/ensure-pinned-actions.sh` (CI enforces this too). +3. Open a PR and verify with a **Dry run** before the next real release. + +The `# sdk-actions: {...}` header on line 2 records the template, pinned ref, and generation parameters — `compare`/`update` need it, so keep it intact. + +### Local edits + +This repo carries two deliberate edits on top of the template. `compare` should report these and nothing else; if `update` drops one, re-apply it: + +- the `_instructions` dispatch input (the merge-a-version-bump-PR warning) +- `slack_mention: '@sdk-eng'` on the `request-approval` job + +### Required repository configuration + +- GitHub Environments `publish` (required reviewers, prevent self-review) and `publish-dry-run`. +- A RubyGems trusted publisher for `braintrust`: repo `braintrustdata/braintrust-sdk-ruby`, workflow `release.yml`, environment `publish`. Renaming the workflow file or the environment requires updating this, or `gem push` fails with an OIDC error. +- Repo secret `SLACK_BOT_TOKEN` and variable `SLACK_SDK_RELEASE_CHANNEL` for notifications. diff --git a/Rakefile b/Rakefile index f0e07b0..cc54c18 100644 --- a/Rakefile +++ b/Rakefile @@ -67,8 +67,9 @@ end desc "Build the gem" task build: [:clean] do - # Output to pkg/ (Bundler convention). rubygems/release-gem awaits pkg/*.gem - # after pushing, so the gem must land there — not the repo root. + # Output to pkg/ (Bundler convention). The release workflow's build-and-ship step + # globs pkg/*.gem and requires exactly one match, so the gem must land there — + # not the repo root. require_relative "lib/braintrust/version" sh "gem build braintrust.gemspec" mkdir_p "pkg" @@ -202,46 +203,6 @@ namespace :test do end end -# Release tasks -namespace :release do - task publish: [:lint, :build] do - gem_files = FileList["pkg/braintrust-*.gem"] - if gem_files.empty? - puts "Error: No gem file found. Build task should have created it." - exit 1 - elsif gem_files.length > 1 - puts "Error: Multiple gem files found. Clean task should have removed them." - puts "Found: #{gem_files.join(", ")}" - exit 1 - end - if ENV["DRY_RUN"] == "true" - puts "DRY RUN: would push #{gem_files.first} to RubyGems (skipped)" - else - sh "gem push #{gem_files.first}" - puts "✓ Gem pushed to RubyGems" - end - end - - task :push_tag do - require_relative "lib/braintrust/version" - tag = "v#{Braintrust::VERSION}" - if ENV["DRY_RUN"] == "true" - puts "DRY RUN: would push tag #{tag} to origin (skipped)" - else - sh "git tag #{tag}" - sh "git push origin #{tag}" - puts "✓ Tag #{tag} pushed" - end - end -end - -# Called by rubygems/release-gem in the release workflow. -# Follows Bundler convention: build, push gem, push tag. -# GitHub release creation is handled separately by the workflow. -task release: ["release:publish", "release:push_tag"] do - puts "✓ Release complete" -end - # Contrib tasks namespace :contrib do desc "Generate a new integration (NAME=name [GEM_NAMES=gem1,gem2] [REQUIRE_PATHS=path1,path2] [MIN_VERSION=1.0.0] [MAX_VERSION=2.0.0] [AUTO_REGISTER=true])"