Skip to content

publish

publish #5

Workflow file for this run

# Releases the autoevals npm + PyPI packages TOGETHER, at one shared version, behind ONE
# approval gate — via braintrustdata/sdk-actions (custom / build-ownership shape).
#
# Shape (fan-out → fan-in → fan-out), one lane per package:
# configure-{js,py} → prepare-{js,py} → validate-{js,py} → pack-{js,py} (unprivileged build+attest)
# └────────────────────────── request-approval (single gate: builds one approval covering
# both packages, then parks at the `publish` environment)
# request-approval → ship-package-{js,py} (gated: verify attestation → publish → GitHub release)
#
# This is hand-authored, but deliberately modeled as the output a future sdk-actions "monorepo"
# template should generate: each lane is an independent, uniformly-parameterized column; the only
# shared, once-per-release job is request-approval, which composes a `packages` array (one entry per
# lane, via build-package-manifest) so reviewers approve BOTH packages in one gate. A failure in
# either lane skips request-approval → neither ships (the packages always release together).
#
# ── SETUP (repo Settings) ─────────────────────────────────────────────────────────────────────
# Environments: `publish` (required reviewers) and `publish-dry-run`.
# Trusted publishers (OIDC, no tokens) — BOTH must set environment=`publish`, which is what keeps
# the unprivileged pack jobs (id-token for attestation only) from being able to publish:
# npm `autoevals` → repo braintrustdata/autoevals, workflow publish.yaml, environment publish
# PyPI `autoevals` → repo braintrustdata/autoevals, workflow publish.yaml, environment publish
# Secrets/vars (optional Slack): SLACK_BOT_TOKEN, SLACK_SDK_RELEASE_CHANNEL.
#
# Version-sync (package.json == py/autoevals/version.py) is enforced by the version-sync.yaml CI
# workflow on PRs, so it is intentionally NOT re-checked here. Prereleases publish the COMMITTED
# version (release_type only suppresses the GitHub release / sets the npm rc channel) — bump to an
# rc version in a PR if you want a prerelease.
#
# sdk-actions pinned @3cdedcc (branch release_request_approval_multi_package, off #63). RE-PIN to the
# merged main SHA before merging this PR.
name: publish
concurrency:
group: publish-${{ inputs.release_type }}-${{ inputs.sha }}
cancel-in-progress: false
on:
workflow_dispatch:
inputs:
_instructions:
description: "⚠️ Before starting: merge a version-bump PR (package.json + py/autoevals/version.py, matching). The release publishes the version at the SHA — it can't be overridden here."
type: string
default: "I have merged a version bump PR"
required: false
release_type:
description: "Release type (stable → GitHub releases; prerelease → none, npm rc channel)"
type: choice
default: stable
options: [stable, prerelease]
sha:
description: "Commit SHA (of the version bump) to release — same commit for js + py"
required: true
type: string
prev_release:
description: "Release-notes anchor: a tag or SHA (empty → each lane's previous tag)"
type: string
required: false
dry_run:
description: "Dry run: build + pack + attest, but do not publish or create releases"
type: boolean
default: false
jobs:
# ─────────────────────────── JS lane ───────────────────────────
configure-js:
runs-on: ubuntu-24.04
timeout-minutes: 5
permissions:
contents: read
outputs:
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 }}
channel: ${{ steps.configure.outputs.channel }}
github_release: ${{ steps.configure.outputs.github_release }}
steps:
- name: Configure release
id: configure
uses: braintrustdata/sdk-actions/actions/release/lang/js/configure@3cdedcc1ffa219aa05e8687ab7451f2fd72a8950
with:
sha: ${{ inputs.sha }}
working_directory: .
tag_format: 'js-{version}'
release_type: ${{ inputs.release_type }}
prepare-js:
needs: configure-js
runs-on: ubuntu-24.04
timeout-minutes: 5
permissions:
contents: write # releases/generate-notes API
outputs:
pr_list: ${{ steps.prepare.outputs.pr_list }}
notes: ${{ steps.prepare.outputs.notes }}
steps:
- name: Prepare release
id: prepare
uses: braintrustdata/sdk-actions/actions/release/prepare@3cdedcc1ffa219aa05e8687ab7451f2fd72a8950
with:
release_tag: ${{ needs.configure-js.outputs.release_tag }}
sha: ${{ inputs.sha }}
prev_release: ${{ inputs.prev_release || needs.configure-js.outputs.prev_release }}
validate-js:
needs: [configure-js, prepare-js]
runs-on: ubuntu-24.04
timeout-minutes: 10
permissions:
contents: read
steps:
- name: Validate release
uses: braintrustdata/sdk-actions/actions/release/lang/js/validate@3cdedcc1ffa219aa05e8687ab7451f2fd72a8950
with:
version: ${{ needs.configure-js.outputs.version }}
sha: ${{ inputs.sha }}
dry_run: ${{ inputs.dry_run }}
working_directory: .
node_version: .tool-versions
package_name: autoevals
release_tag: ${{ needs.configure-js.outputs.release_tag }}
channel: ${{ needs.configure-js.outputs.channel }}
allowed_channels: 'latest,rc'
on_release_branch: ${{ needs.configure-js.outputs.on_release_branch }}
notes: ${{ needs.prepare-js.outputs.notes }}
build: 'false' # pack-js owns the build
sbom: 'false' # pack-js generates + attests the SBOM
pack-js:
needs: [configure-js, prepare-js, validate-js]
runs-on: ubuntu-24.04
timeout-minutes: 15
permissions:
contents: read
id-token: write
attestations: write
steps:
- name: Pack + attest
id: pack
uses: braintrustdata/sdk-actions/actions/release/lang/js/pack-pnpm@3cdedcc1ffa219aa05e8687ab7451f2fd72a8950
with:
sha: ${{ inputs.sha }}
working_directory: .
node_version: .tool-versions
# autoevals installs + builds on its pinned pnpm 10.33 (to honor its supply-chain config);
# the packer resolves a pnpm >= 11.8 on its own for `pnpm pack` + the SBOM. No override here.
dry_run: ${{ inputs.dry_run }}
- name: Stage artifact
run: |
mkdir -p artifact
cp "${{ steps.pack.outputs.tarball }}" artifact/
cp ./sbom.json artifact/ 2>/dev/null || true
- name: Upload artifact
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: release-package-js
path: artifact/
retention-days: 1
if-no-files-found: error
# ─────────────────────────── PY lane ───────────────────────────
configure-py:
runs-on: ubuntu-24.04
timeout-minutes: 5
permissions:
contents: read
outputs:
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 }}
steps:
- name: Configure release
id: configure
uses: braintrustdata/sdk-actions/actions/release/lang/py/configure@3cdedcc1ffa219aa05e8687ab7451f2fd72a8950
with:
version_file: py/autoevals/version.py
sha: ${{ inputs.sha }}
working_directory: .
tag_format: 'py-{version}'
release_type: ${{ inputs.release_type }}
prepare-py:
needs: configure-py
runs-on: ubuntu-24.04
timeout-minutes: 5
permissions:
contents: write
outputs:
pr_list: ${{ steps.prepare.outputs.pr_list }}
notes: ${{ steps.prepare.outputs.notes }}
steps:
- name: Prepare release
id: prepare
uses: braintrustdata/sdk-actions/actions/release/prepare@3cdedcc1ffa219aa05e8687ab7451f2fd72a8950
with:
release_tag: ${{ needs.configure-py.outputs.release_tag }}
sha: ${{ inputs.sha }}
prev_release: ${{ inputs.prev_release || needs.configure-py.outputs.prev_release }}
validate-py:
needs: [configure-py, prepare-py]
runs-on: ubuntu-24.04
timeout-minutes: 10
permissions:
contents: read
steps:
- name: Validate release
uses: braintrustdata/sdk-actions/actions/release/lang/py/validate@3cdedcc1ffa219aa05e8687ab7451f2fd72a8950
with:
version: ${{ needs.configure-py.outputs.version }}
sha: ${{ inputs.sha }}
dry_run: ${{ inputs.dry_run }}
working_directory: .
python_version: .tool-versions
package_name: autoevals
release_tag: ${{ needs.configure-py.outputs.release_tag }}
on_release_branch: ${{ needs.configure-py.outputs.on_release_branch }}
notes: ${{ needs.prepare-py.outputs.notes }}
build: 'false' # pack-py owns the build
sbom: 'false' # pack-py generates + attests the SBOM
pack-py:
needs: [configure-py, prepare-py, validate-py]
runs-on: ubuntu-24.04
timeout-minutes: 15
permissions:
contents: read
id-token: write
attestations: write
steps:
- name: Pack + attest
uses: braintrustdata/sdk-actions/actions/release/lang/py/pack@3cdedcc1ffa219aa05e8687ab7451f2fd72a8950
with:
sha: ${{ inputs.sha }}
working_directory: .
python_version: .tool-versions
dry_run: ${{ inputs.dry_run }}
- name: Stage artifact
run: |
mkdir -p artifact/dist
cp ./dist/*.whl ./dist/*.tar.gz artifact/dist/
cp ./sbom.json artifact/ 2>/dev/null || true
- name: Upload artifact
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: release-package-py
path: artifact/
retention-days: 1
if-no-files-found: error
# ─────────────── fan-in: ONE approval covering both packages ───────────────
request-approval:
needs: [configure-js, prepare-js, pack-js, configure-py, prepare-py, pack-py]
runs-on: ubuntu-24.04
timeout-minutes: 5
permissions: {}
steps:
# One package-manifest entry per lane (label/emoji specified here, near use). Splicing the two
# entries into `packages` is safe — build-package-manifest jq-escapes pr_list (x1f) / notes.
- name: Build js package entry
id: pkg-js
uses: braintrustdata/sdk-actions/actions/release/build-package-manifest@3cdedcc1ffa219aa05e8687ab7451f2fd72a8950
with:
name: autoevals
label: 'autoevals (js)'
emoji: ':javascript:'
version: ${{ needs.configure-js.outputs.version }}
tag: ${{ needs.configure-js.outputs.release_tag }}
prev_release: ${{ needs.configure-js.outputs.prev_release }}
pr_list: ${{ needs.prepare-js.outputs.pr_list }}
notes: ${{ needs.prepare-js.outputs.notes }}
- name: Build py package entry
id: pkg-py
uses: braintrustdata/sdk-actions/actions/release/build-package-manifest@3cdedcc1ffa219aa05e8687ab7451f2fd72a8950
with:
name: autoevals
label: 'autoevals (py)'
emoji: ':python:'
version: ${{ needs.configure-py.outputs.version }}
tag: ${{ needs.configure-py.outputs.release_tag }}
prev_release: ${{ needs.configure-py.outputs.prev_release }}
pr_list: ${{ needs.prepare-py.outputs.pr_list }}
notes: ${{ needs.prepare-py.outputs.notes }}
- name: Request release approval
uses: braintrustdata/sdk-actions/actions/release/request-approval@3cdedcc1ffa219aa05e8687ab7451f2fd72a8950
with:
packages: '[${{ steps.pkg-js.outputs.package }}, ${{ steps.pkg-py.outputs.package }}]'
title: autoevals
sha: ${{ inputs.sha }}
branch: ${{ needs.configure-js.outputs.branch }}
on_release_branch: ${{ needs.configure-js.outputs.on_release_branch }}
commit_message: ${{ needs.configure-js.outputs.commit_message }}
dry_run: ${{ inputs.dry_run }}
slack_token: ${{ secrets.SLACK_BOT_TOKEN }}
slack_channel: ${{ vars.SLACK_SDK_RELEASE_CHANNEL }}
# ─────────────── fan-out: gated per-lane publish (one approval releases both) ───────────────
ship-package-js:
needs: [configure-js, prepare-js, pack-js, request-approval]
runs-on: ubuntu-24.04
timeout-minutes: 15
environment: ${{ inputs.dry_run && 'publish-dry-run' || 'publish' }}
permissions:
contents: write
id-token: write # OIDC trusted publishing + provenance
attestations: read # gh attestation verify
steps:
- name: Download artifact
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
name: release-package-js
path: dl
- name: Resolve tarball
id: art
run: echo "tarball=$(ls dl/*.tgz)" >> "$GITHUB_OUTPUT"
- name: Ship package
uses: braintrustdata/sdk-actions/actions/release/lang/js/ship-package@3cdedcc1ffa219aa05e8687ab7451f2fd72a8950
with:
tarball: ${{ steps.art.outputs.tarball }}
working_directory: dl
sha: ${{ inputs.sha }}
dry_run: ${{ inputs.dry_run }}
release_tag: ${{ needs.configure-js.outputs.release_tag }}
github_release: ${{ needs.configure-js.outputs.github_release }}
version: ${{ needs.configure-js.outputs.version }}
access: 'public'
provenance: 'true'
channel: ${{ needs.configure-js.outputs.channel }}
package_name: autoevals
label: 'autoevals (js)'
notes: ${{ needs.prepare-js.outputs.notes }}
prev_release: ${{ needs.configure-js.outputs.prev_release }}
branch: ${{ needs.configure-js.outputs.branch }}
on_release_branch: ${{ needs.configure-js.outputs.on_release_branch }}
pr_list: ${{ needs.prepare-js.outputs.pr_list }}
slack_token: ${{ secrets.SLACK_BOT_TOKEN }}
slack_channel: ${{ vars.SLACK_SDK_RELEASE_CHANNEL }}
emoji: ':javascript:'
ship-package-py:
needs: [configure-py, prepare-py, pack-py, request-approval]
runs-on: ubuntu-24.04
timeout-minutes: 15
environment: ${{ inputs.dry_run && 'publish-dry-run' || 'publish' }}
permissions:
contents: write
id-token: write # OIDC trusted publishing + PEP 740
attestations: read # gh attestation verify
steps:
- name: Download artifact
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
name: release-package-py
path: dl
- name: Ship package
uses: braintrustdata/sdk-actions/actions/release/lang/py/ship-package@3cdedcc1ffa219aa05e8687ab7451f2fd72a8950
with:
working_directory: dl # holds dist/ (whl + sdist) + sbom.json
sha: ${{ inputs.sha }}
dry_run: ${{ inputs.dry_run }}
release_tag: ${{ needs.configure-py.outputs.release_tag }}
github_release: ${{ needs.configure-py.outputs.github_release }}
version: ${{ needs.configure-py.outputs.version }}
package_name: autoevals
label: 'autoevals (py)'
notes: ${{ needs.prepare-py.outputs.notes }}
prev_release: ${{ needs.configure-py.outputs.prev_release }}
branch: ${{ needs.configure-py.outputs.branch }}
on_release_branch: ${{ needs.configure-py.outputs.on_release_branch }}
pr_list: ${{ needs.prepare-py.outputs.pr_list }}
slack_token: ${{ secrets.SLACK_BOT_TOKEN }}
slack_channel: ${{ vars.SLACK_SDK_RELEASE_CHANNEL }}
emoji: ':python:'