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
220 changes: 212 additions & 8 deletions .github/workflows/release-py.yml
Original file line number Diff line number Diff line change
@@ -1,11 +1,57 @@
# Placeholder so the release-py.yml workflow_dispatch trigger is registered on the
# default branch. This lets the full workflow be dispatched from its feature branch
# ("Use workflow from: <branch>") before merge. The functional workflow — which
# publishes the bt-publishing-test dummy PyPI package end-to-end — replaces this
# stub when the Python release PR lands.
#
# Releases bt-publishing-test — a dummy PyPI package used to validate Braintrust
# release workflow tooling end-to-end. This does NOT release any real Braintrust
# Python SDK.
#
# This workflow serves two purposes:
# 1. End-to-end testing of the composite actions in actions/release/
# 2. Reference implementation of the Python-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
#
# PyPI Trusted Publishing (OIDC — no token needed):
# Configure for your package at pypi.org → project → Settings → Publishing,
# or pre-register a "pending publisher" before the first publish:
# Owner: braintrustdata (or your org)
# Repository: sdk-actions (or your SDK repo)
# Workflow: release-py.yml (or your workflow filename)
# Environment: publish
# The publish job needs id-token: write for OIDC + PEP 740 attestations.
#
# ─────────────────────────────────────────────────────────────────────────────

name: Release Python (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:
Expand All @@ -21,17 +67,175 @@ 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"
type: boolean
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 PyPI and incrementing the patch.
# Falls back to 0.0.1 on first publish (project not found). Remove this job
# in real SDK workflows; version comes from the version bump PR.
- name: Bump version
id: bump
run: |
# Project may not exist yet (first release / new name); default to 0.0.0
# so the first publish is 0.0.1.
CURRENT=$(curl -sf https://pypi.org/pypi/bt-publishing-test/json 2>/dev/null | jq -r '.info.version // empty' || true)
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-py.yml lands in the Python 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/py/validate
with:
# bt-publishing-test: version passed directly from bump job — no version_file read needed.
# In a real Python project, drop `version`; it is read from version_file.
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/py
python_version: test/release/py/.python-version # exercises version-file sourcing (vs explicit version)
package_name: bt-publishing-test # exercises the PyPI-availability fail-fast check

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'
label: 'bt-publishing-test' # distinguishes this from other packages in this repo
emoji: ':python:'

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 + PEP 740 attestations
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 version.py. 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.
- name: Apply version to version.py
env:
VERSION: ${{ needs.bump.outputs.version }}
run: |
printf 'VERSION = "%s"\n' "$VERSION" > test/release/py/src/bt_publishing_test/version.py

- name: Publish
uses: ./actions/release/lang/py/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/py
python_version: test/release/py/.python-version
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: bt-publishing-test
label: 'bt-publishing-test' # distinguishes this from other packages in this repo
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: ':python:'
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ Shared GitHub actions and workflows for Braintrust SDK repositories.
1. **Copy the canonical template** for your language into your repo's
`.github/workflows/`:
- [`release-js.yml`](.github/workflows/release-js.yml)
- [`release-py.yml`](.github/workflows/release-py.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;
Expand All @@ -35,6 +36,8 @@ pulls in everything it needs.
| `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/lang/py/publish` | Publish the package to PyPI (OIDC trusted publishing + PEP 740 attestations), create the GitHub release, and notify |
| `release/lang/py/validate` | Check out the SHA, set up uv + Python, read the version, validate the release (tag/branch/metadata, PyPI availability), build |
| `release/notify-pending` | Post the pre-approval job summary and Slack notification |
| `release/prepare` | Fetch the PR list and release notes |

Expand Down
Loading