Skip to content
Draft
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
232 changes: 196 additions & 36 deletions .github/workflows/publish-stable-release.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,9 @@ env:
HUSKY: "0"
NPM_CONFIG_PROVENANCE: "true"

# Replace the sdk-actions ref below with the final commit SHA containing the
# manifest-driven publish action before using this workflow for production.

jobs:
validate-release-sha:
runs-on: ubuntu-latest
Expand Down Expand Up @@ -69,18 +72,20 @@ jobs:
echo "Diff to latest release: $diff_url"
} | tee -a "$GITHUB_STEP_SUMMARY"

publish-stable:
build-package-artifacts:
needs:
- validate-release-sha
- print-changelog-links
runs-on: ubuntu-latest
timeout-minutes: 30
permissions:
contents: write
issues: write
contents: read
id-token: write
pull-requests: read
environment: npm-publish
attestations: write
outputs:
has_work: ${{ steps.detect.outputs.has_work }}
markdown: ${{ steps.detect.outputs.markdown }}
release_sha: ${{ needs.validate-release-sha.outputs.release_sha }}
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
Expand Down Expand Up @@ -113,23 +118,185 @@ jobs:
- name: Build packages
if: steps.detect.outputs.needs_publish == 'true'
run: pnpm run build
- name: Publish stable packages to npm
- name: Pack packages and generate SBOMs
if: steps.detect.outputs.needs_publish == 'true'
run: node scripts/release/pack-publishable-packages.mjs --manifest .release-manifest.json --output-dir artifacts/release-packages --report artifacts/release-packages/pack-report.json
- name: Read release artifact paths
id: artifact_paths
if: steps.detect.outputs.needs_publish == 'true'
run: |
set -euo pipefail

emit_paths() {
local output_prefix="$1"
local package_name="$2"
local tarball sbom
tarball="$(jq -r --arg name "$package_name" '.packages[]? | select(.name == $name) | .tarball_asset // empty' .release-manifest.json)"
sbom="$(jq -r --arg name "$package_name" '.packages[]? | select(.name == $name) | .sbom_asset // empty' .release-manifest.json)"

if [ -n "$tarball" ]; then
echo "${output_prefix}_tarball=artifacts/release-packages/$tarball" >> "$GITHUB_OUTPUT"
echo "${output_prefix}_sbom=artifacts/release-packages/$sbom" >> "$GITHUB_OUTPUT"
fi
}

emit_paths braintrust "braintrust"
emit_paths braintrust_browser "@braintrust/browser"
emit_paths braintrust_langchain_js "@braintrust/langchain-js"
emit_paths braintrust_openai_agents "@braintrust/openai-agents"
emit_paths braintrust_otel "@braintrust/otel"
emit_paths braintrust_templates_nunjucks_js "@braintrust/templates-nunjucks-js"
emit_paths braintrust_temporal "@braintrust/temporal"
emit_paths braintrust_vercel_ai_sdk "@braintrust/vercel-ai-sdk"
- name: Attest tarball build provenance
if: steps.detect.outputs.needs_publish == 'true'
uses: actions/attest-build-provenance@0f67c3f4856b2e3261c31976d6725780e5e4c373 # v4.1.1
with:
subject-path: artifacts/release-packages/*.tgz
- name: Attest braintrust SBOM
if: steps.artifact_paths.outputs.braintrust_tarball != ''
uses: actions/attest@a1948c3f048ba23858d222213b7c278aabede763 # v4.1.1
with:
subject-path: ${{ steps.artifact_paths.outputs.braintrust_tarball }}
sbom-path: ${{ steps.artifact_paths.outputs.braintrust_sbom }}
- name: Attest @braintrust/browser SBOM
if: steps.artifact_paths.outputs.braintrust_browser_tarball != ''
uses: actions/attest@a1948c3f048ba23858d222213b7c278aabede763 # v4.1.1
with:
subject-path: ${{ steps.artifact_paths.outputs.braintrust_browser_tarball }}
sbom-path: ${{ steps.artifact_paths.outputs.braintrust_browser_sbom }}
- name: Attest @braintrust/langchain-js SBOM
if: steps.artifact_paths.outputs.braintrust_langchain_js_tarball != ''
uses: actions/attest@a1948c3f048ba23858d222213b7c278aabede763 # v4.1.1
with:
subject-path: ${{ steps.artifact_paths.outputs.braintrust_langchain_js_tarball }}
sbom-path: ${{ steps.artifact_paths.outputs.braintrust_langchain_js_sbom }}
- name: Attest @braintrust/openai-agents SBOM
if: steps.artifact_paths.outputs.braintrust_openai_agents_tarball != ''
uses: actions/attest@a1948c3f048ba23858d222213b7c278aabede763 # v4.1.1
with:
subject-path: ${{ steps.artifact_paths.outputs.braintrust_openai_agents_tarball }}
sbom-path: ${{ steps.artifact_paths.outputs.braintrust_openai_agents_sbom }}
- name: Attest @braintrust/otel SBOM
if: steps.artifact_paths.outputs.braintrust_otel_tarball != ''
uses: actions/attest@a1948c3f048ba23858d222213b7c278aabede763 # v4.1.1
with:
subject-path: ${{ steps.artifact_paths.outputs.braintrust_otel_tarball }}
sbom-path: ${{ steps.artifact_paths.outputs.braintrust_otel_sbom }}
- name: Attest @braintrust/templates-nunjucks-js SBOM
if: steps.artifact_paths.outputs.braintrust_templates_nunjucks_js_tarball != ''
uses: actions/attest@a1948c3f048ba23858d222213b7c278aabede763 # v4.1.1
with:
subject-path: ${{ steps.artifact_paths.outputs.braintrust_templates_nunjucks_js_tarball }}
sbom-path: ${{ steps.artifact_paths.outputs.braintrust_templates_nunjucks_js_sbom }}
- name: Attest @braintrust/temporal SBOM
if: steps.artifact_paths.outputs.braintrust_temporal_tarball != ''
uses: actions/attest@a1948c3f048ba23858d222213b7c278aabede763 # v4.1.1
with:
subject-path: ${{ steps.artifact_paths.outputs.braintrust_temporal_tarball }}
sbom-path: ${{ steps.artifact_paths.outputs.braintrust_temporal_sbom }}
- name: Attest @braintrust/vercel-ai-sdk SBOM
if: steps.artifact_paths.outputs.braintrust_vercel_ai_sdk_tarball != ''
uses: actions/attest@a1948c3f048ba23858d222213b7c278aabede763 # v4.1.1
with:
subject-path: ${{ steps.artifact_paths.outputs.braintrust_vercel_ai_sdk_tarball }}
sbom-path: ${{ steps.artifact_paths.outputs.braintrust_vercel_ai_sdk_sbom }}
- name: Upload release package artifacts
if: steps.detect.outputs.needs_publish == 'true'
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: stable-release-packages
path: |
.release-manifest.json
artifacts/release-packages/
retention-days: 1
if-no-files-found: error

request-approval:
needs:
- build-package-artifacts
if: needs.build-package-artifacts.outputs.has_work == 'true'
runs-on: ubuntu-latest
timeout-minutes: 5
permissions: {}
steps:
- name: Post release approval summary
env:
NODE_AUTH_TOKEN: ""
NPM_TOKEN: ""
run: node scripts/release/publish-release-manifest.mjs --manifest .release-manifest.json
RELEASE_SHA: ${{ needs.build-package-artifacts.outputs.release_sha }}
PACKAGES: ${{ needs.build-package-artifacts.outputs.markdown }}
run: |
{
echo "## Stable release awaiting approval"
echo
echo "**Release SHA:** \`$RELEASE_SHA\`"
echo
echo "### Packages"
echo "$PACKAGES"
} >> "$GITHUB_STEP_SUMMARY"
- name: Post approval request to Slack
uses: slackapi/slack-github-action@af78098f536edbc4de71162a307590698245be95 # v3.0.1
with:
method: chat.postMessage
token: ${{ secrets.SLACK_BOT_TOKEN }}
payload: |
channel: C0ABHT0SWA2
text: "⏳ Stable release awaiting approval"
blocks:
- type: "header"
text:
type: "plain_text"
text: "⏳ Stable release awaiting approval"
- type: "section"
text:
type: "mrkdwn"
text: "*Release SHA:* `${{ needs.build-package-artifacts.outputs.release_sha }}`\n\n*Packages:*\n${{ needs.build-package-artifacts.outputs.markdown }}\n\n<${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}|View Run>"

publish-stable:
needs:
- build-package-artifacts
- request-approval
if: |
always() &&
needs.build-package-artifacts.outputs.has_work == 'true' &&
needs.request-approval.result == 'success'
runs-on: ubuntu-latest
timeout-minutes: 30
permissions:
contents: write
issues: write
id-token: write
pull-requests: read
attestations: read
environment: npm-publish
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
fetch-depth: 0
ref: ${{ needs.build-package-artifacts.outputs.release_sha }}
- name: Download release package artifacts
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
name: stable-release-packages
path: release-artifacts
- name: Publish stable package tarballs
uses: braintrustdata/sdk-actions/actions/release/lang/js/publish-tarballs@release_custom_build_actions
with:
manifest: release-artifacts/.release-manifest.json
tarballs: release-artifacts/artifacts/release-packages/*.tgz
repo: ${{ github.repository }}
slack_token: ${{ secrets.SLACK_BOT_TOKEN }}
slack_channel: C0ABHT0SWA2
- name: Push Changesets release tags
if: steps.detect.outputs.has_work == 'true'
run: |
set -euo pipefail

git config user.name "github-actions[bot]"
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"

target_commit="$(jq -r '.commit // empty' .release-manifest.json)"
manifest="release-artifacts/.release-manifest.json"
target_commit="$(jq -r '.commit // empty' "$manifest")"
mapfile -t tags < <(
jq -r '.packages[]? | .tag // "\(.name)@\(.version)"' .release-manifest.json
jq -r '.packages[]? | .tag // "\(.name)@\(.version)"' "$manifest"
)

if [ "${#tags[@]}" -eq 0 ]; then
Expand Down Expand Up @@ -174,44 +341,37 @@ jobs:

git push origin "${to_push[@]}"
- name: Create GitHub Releases
if: steps.detect.outputs.has_work == 'true'
env:
GITHUB_TOKEN: ${{ github.token }}
run: node scripts/release/create-github-releases.mjs --manifest .release-manifest.json
uses: braintrustdata/sdk-actions/actions/release/create-package-releases@release_custom_build_actions
with:
manifest: release-artifacts/.release-manifest.json
repo: ${{ github.repository }}
- name: Attach package SBOMs to GitHub Releases
uses: braintrustdata/sdk-actions/actions/release/upload-package-sboms@release_custom_build_actions
with:
manifest: release-artifacts/.release-manifest.json
sboms: release-artifacts/artifacts/release-packages/*.sbom.json
repo: ${{ github.repository }}
- name: Comment on issues closed by released PRs
if: steps.detect.outputs.has_work == 'true'
env:
GITHUB_TOKEN: ${{ github.token }}
run: node scripts/release/comment-release-issues.mjs
- name: Post stable release to Slack
if: steps.detect.outputs.has_work == 'true'
uses: slackapi/slack-github-action@af78098f536edbc4de71162a307590698245be95 # v3.0.1
with:
method: chat.postMessage
token: ${{ secrets.SLACK_BOT_TOKEN }}
payload: |
channel: C0ABHT0SWA2
text: "✅ Packages published"
blocks:
- type: "header"
text:
type: "plain_text"
text: "✅ Packages published"
- type: "section"
text:
type: "mrkdwn"
text: "*Release SHA:* `${{ needs.validate-release-sha.outputs.release_sha }}`\n\n*Packages:*\n${{ steps.detect.outputs.markdown }}\n\n<${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}|View Run>"
run: |
cp release-artifacts/.release-manifest.json .release-manifest.json
node scripts/release/comment-release-issues.mjs

notify-failure:
needs:
- validate-release-sha
- print-changelog-links
- build-package-artifacts
- request-approval
- publish-stable
if: |
always() &&
(
needs.validate-release-sha.result == 'failure' ||
needs.print-changelog-links.result == 'failure' ||
needs.build-package-artifacts.result == 'failure' ||
needs.request-approval.result == 'failure' ||
needs.publish-stable.result == 'failure'
)
runs-on: ubuntu-latest
Expand Down
2 changes: 1 addition & 1 deletion .tool-versions
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
nodejs 24.15.0
npm 11.12.1
pnpm 10.33.0
pnpm 11.9.0
deno 2.7.6
60 changes: 44 additions & 16 deletions e2e/helpers/scenario-installer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,13 @@ const PNPM_COMMAND = process.platform === "win32" ? "pnpm.cmd" : "pnpm";
const TEMP_DIR_NAME = ".bt-tmp";
const DEPENDENCY_CACHE_DIR_NAME = "scenario-deps";
const CANARY_MODE_ENV = "BRAINTRUST_E2E_MODE";
const SCENARIO_PNPM_WORKSPACE_CONFIG = `strictDepBuilds: true
allowBuilds:
"@google/genai": false
"@openrouter/sdk": false
esbuild: false
protobufjs: false
`;
const INSTALL_SECRET_ENV_VARS = [
"ANTHROPIC_API_KEY",
"BRAINTRUST_API_KEY",
Expand Down Expand Up @@ -161,6 +168,27 @@ function installEnv(): NodeJS.ProcessEnv {
return env;
}

function isTempScenarioDir(scenarioDir: string): boolean {
const relativeToTemp = path.relative(
path.join(E2E_ROOT, TEMP_DIR_NAME),
path.resolve(scenarioDir),
);
return !relativeToTemp.startsWith("..") && !path.isAbsolute(relativeToTemp);
}

async function writeScenarioPnpmWorkspaceConfig(
scenarioDir: string,
): Promise<void> {
if (!isTempScenarioDir(scenarioDir)) {
return;
}

await fs.writeFile(
path.join(scenarioDir, "pnpm-workspace.yaml"),
SCENARIO_PNPM_WORKSPACE_CONFIG,
);
}

function scenarioNameForPath(scenarioDir: string): string {
return path.basename(scenarioDir);
}
Expand Down Expand Up @@ -238,20 +266,18 @@ async function rewriteManifestForCanary(scenarioDir: string): Promise<void> {

manifest.dependencies = dependencies;
await fs.writeFile(manifestPath, `${JSON.stringify(manifest, null, 2)}\n`);
await writeScenarioPnpmWorkspaceConfig(scenarioDir);

await spawnOrThrow(
PNPM_COMMAND,
[
"install",
"--dir",
scenarioDir,
"--ignore-workspace",
"--lockfile-only",
"--strict-peer-dependencies=false",
],
scenarioDir,
installEnv(),
);
const installArgs = [
"install",
"--lockfile-only",
"--strict-peer-dependencies=false",
];
if (!isTempScenarioDir(scenarioDir)) {
installArgs.push("--dir", scenarioDir, "--ignore-workspace");
}

await spawnOrThrow(PNPM_COMMAND, installArgs, scenarioDir, installEnv());
}

function findWorkspaceSpecs(
Expand Down Expand Up @@ -377,6 +403,7 @@ async function installCachedScenarioDependencies({
path.join(stagingDir, "pnpm-lock.yaml"),
inputs.lockfileRaw,
);
await writeScenarioPnpmWorkspaceConfig(stagingDir);
await installScenarioDependencies({
mode,
preferOffline,
Expand Down Expand Up @@ -465,16 +492,17 @@ export async function installScenarioDependencies({
if (mode === "canary") {
await rewriteManifestForCanary(scenarioDir);
}
await writeScenarioPnpmWorkspaceConfig(scenarioDir);

const installArgs = [
"install",
"--dir",
scenarioDir,
"--ignore-workspace",
"--frozen-lockfile",
"--ignore-scripts=false",
"--strict-peer-dependencies=false",
];
if (!isTempScenarioDir(scenarioDir)) {
installArgs.push("--dir", scenarioDir, "--ignore-workspace");
}
if (preferOffline) {
installArgs.push("--prefer-offline");
}
Expand Down
Loading
Loading