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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
133 changes: 71 additions & 62 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
@@ -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 <this file>
# 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:
Expand All @@ -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:'
78 changes: 57 additions & 21 deletions PUBLISHING.md
Original file line number Diff line number Diff line change
@@ -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

Expand All @@ -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

Expand All @@ -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-<version>.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.
45 changes: 3 additions & 42 deletions Rakefile
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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])"
Expand Down
Loading