From 4f3d517f4526e9024338022c314229aee12870c4 Mon Sep 17 00:00:00 2001 From: Luca Forstner Date: Thu, 9 Jul 2026 15:25:29 +0200 Subject: [PATCH 1/2] deps: Update pnpm to v11 --- .tool-versions | 2 +- package.json | 29 +---------------------------- pnpm-lock.yaml | 22 +++++++++++----------- pnpm-workspace.yaml | 24 ++++++++++++++++++++++++ 4 files changed, 37 insertions(+), 40 deletions(-) diff --git a/.tool-versions b/.tool-versions index 1d471a553..ebcb7cd34 100644 --- a/.tool-versions +++ b/.tool-versions @@ -1,4 +1,4 @@ nodejs 24.15.0 npm 11.12.1 -pnpm 10.33.0 +pnpm 11.9.0 deno 2.7.6 diff --git a/package.json b/package.json index a915e4d8a..aa6c82ca5 100644 --- a/package.json +++ b/package.json @@ -56,32 +56,5 @@ "lint-staged": { "**/*.{js,jsx,ts,tsx,mjs,cjs,css,md,mdx,html,yaml,yml,json}": "prettier --write" }, - "packageManager": "pnpm@10.33.0", - "pnpm": { - "overrides": { - "handlebars": "^4.7.9", - "protobufjs": "^7.5.8", - "esbuild": "0.27.4", - "uuid": "11.1.1", - "router@2.2.0>path-to-regexp": "8.4.0", - "express@5.2.1>qs": "6.15.2", - "qs@>=6.7.0 <6.15.2": "^6.15.2", - "tar@>=7.0.0 <7.5.11": "^7.5.11", - "tar@>=6.0.0 <7.0.0": "^6.2.1", - "hono@>=4.0.0 <4.12.18": "^4.12.18", - "flatted@<3.4.2": "^3.4.2", - "markdown-it@>=13.0.0 <14.1.1": "^14.1.1", - "ip-address@<10.1.1": "^10.1.1", - "js-yaml@>=4.0.0 <4.1.1": "^4.1.1", - "js-yaml@<3.14.2": "^3.14.2", - "@mikro-orm/knex@>=6.0.0 <6.6.14": "^6.6.14", - "glob@>=10.2.0 <10.5.0": "^10.5.0" - }, - "ignoredBuiltDependencies": [ - "@swc/core", - "esbuild", - "msw", - "protobufjs" - ] - } + "packageManager": "pnpm@11.9.0" } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 01a1c1df4..ff65bc288 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -190,7 +190,7 @@ importers: devDependencies: '@openai/agents': specifier: ^0.0.15 - version: 0.0.15(ws@8.20.1)(zod@3.25.76) + version: 0.0.15(ws@8.20.1)(zod@3.25.67) '@types/node': specifier: ^20.10.5 version: 20.19.40 @@ -5029,22 +5029,22 @@ snapshots: '@open-draft/until@2.1.0': {} - '@openai/agents-core@0.0.15(ws@8.20.1)(zod@3.25.76)': + '@openai/agents-core@0.0.15(ws@8.20.1)(zod@3.25.67)': dependencies: '@openai/zod': zod@3.25.67 debug: 4.4.3 openai: 5.11.0(ws@8.20.1)(zod@3.25.76) optionalDependencies: '@modelcontextprotocol/sdk': 1.29.0(zod@3.25.76) - zod: 3.25.76 + zod: 3.25.67 transitivePeerDependencies: - '@cfworker/json-schema' - supports-color - ws - '@openai/agents-openai@0.0.15(ws@8.20.1)(zod@3.25.76)': + '@openai/agents-openai@0.0.15(ws@8.20.1)(zod@3.25.67)': dependencies: - '@openai/agents-core': 0.0.15(ws@8.20.1)(zod@3.25.76) + '@openai/agents-core': 0.0.15(ws@8.20.1)(zod@3.25.67) '@openai/zod': zod@3.25.67 debug: 4.4.3 openai: 5.11.0(ws@8.20.1)(zod@3.25.76) @@ -5054,9 +5054,9 @@ snapshots: - ws - zod - '@openai/agents-realtime@0.0.15(zod@3.25.76)': + '@openai/agents-realtime@0.0.15(zod@3.25.67)': dependencies: - '@openai/agents-core': 0.0.15(ws@8.20.1)(zod@3.25.76) + '@openai/agents-core': 0.0.15(ws@8.20.1)(zod@3.25.67) '@openai/zod': zod@3.25.67 '@types/ws': 8.18.1 debug: 4.4.3 @@ -5068,11 +5068,11 @@ snapshots: - utf-8-validate - zod - '@openai/agents@0.0.15(ws@8.20.1)(zod@3.25.76)': + '@openai/agents@0.0.15(ws@8.20.1)(zod@3.25.67)': dependencies: - '@openai/agents-core': 0.0.15(ws@8.20.1)(zod@3.25.76) - '@openai/agents-openai': 0.0.15(ws@8.20.1)(zod@3.25.76) - '@openai/agents-realtime': 0.0.15(zod@3.25.76) + '@openai/agents-core': 0.0.15(ws@8.20.1)(zod@3.25.67) + '@openai/agents-openai': 0.0.15(ws@8.20.1)(zod@3.25.67) + '@openai/agents-realtime': 0.0.15(zod@3.25.67) debug: 4.4.3 openai: 5.11.0(ws@8.20.1)(zod@3.25.76) transitivePeerDependencies: diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index dd63d0ea2..d90563922 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -14,3 +14,27 @@ trustPolicy: no-downgrade # Ignore the check for packages published more than 30 days ago (pnpm 10.27+) # Useful for older packages that pre-date provenance support trustPolicyIgnoreAfter: 43200 # minutes (30 days) +strictDepBuilds: true +overrides: + handlebars: ^4.7.9 + protobufjs: ^7.5.8 + esbuild: 0.27.4 + uuid: 11.1.1 + router@2.2.0>path-to-regexp: 8.4.0 + express@5.2.1>qs: 6.15.2 + qs@>=6.7.0 <6.15.2: ^6.15.2 + tar@>=7.0.0 <7.5.11: ^7.5.11 + tar@>=6.0.0 <7.0.0: ^6.2.1 + hono@>=4.0.0 <4.12.18: ^4.12.18 + flatted@<3.4.2: ^3.4.2 + markdown-it@>=13.0.0 <14.1.1: ^14.1.1 + ip-address@<10.1.1: ^10.1.1 + js-yaml@>=4.0.0 <4.1.1: ^4.1.1 + js-yaml@<3.14.2: ^3.14.2 + "@mikro-orm/knex@>=6.0.0 <6.6.14": ^6.6.14 + glob@>=10.2.0 <10.5.0: ^10.5.0 +allowBuilds: + "@swc/core": false + esbuild: false + msw: false + protobufjs: false From 9d2bb470054924f9abe1f92e78c83aa99a183657 Mon Sep 17 00:00:00 2001 From: Luca Forstner Date: Fri, 10 Jul 2026 14:07:31 +0200 Subject: [PATCH 2/2] clean up --- .github/workflows/publish-stable-release.yaml | 232 +++++++++++++++--- scripts/release/_shared.mjs | 93 ++++++- scripts/release/create-github-releases.mjs | 59 ++--- scripts/release/pack-publishable-packages.mjs | 104 ++++++-- scripts/release/publish-release-manifest.mjs | 64 +---- scripts/release/release-manifest.mjs | 76 ++++-- 6 files changed, 439 insertions(+), 189 deletions(-) diff --git a/.github/workflows/publish-stable-release.yaml b/.github/workflows/publish-stable-release.yaml index 3745e2f1b..7418c73ae 100644 --- a/.github/workflows/publish-stable-release.yaml +++ b/.github/workflows/publish-stable-release.yaml @@ -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 @@ -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: @@ -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 @@ -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 diff --git a/scripts/release/_shared.mjs b/scripts/release/_shared.mjs index f9f2186ea..5fe323991 100644 --- a/scripts/release/_shared.mjs +++ b/scripts/release/_shared.mjs @@ -7,7 +7,7 @@ import { fileURLToPath } from "node:url"; const __dirname = path.dirname(fileURLToPath(import.meta.url)); const REPO_ROOT = path.resolve(__dirname, "../.."); -const NPM_REGISTRY = "https://registry.npmjs.org/"; +export const NPM_REGISTRY = "https://registry.npmjs.org/"; export const GITHUB_REPO_URL = "git+https://github.com/braintrustdata/braintrust-sdk-javascript.git"; export const DOCS_HOMEPAGE = "https://www.braintrust.dev/docs"; @@ -191,6 +191,97 @@ export function formatPackageList(packages) { return packages.map((pkg) => `- ${pkg.name}@${pkg.version}`).join("\n"); } +export function extractReleaseNotes(relativeDir, packageName, version) { + const changelogPath = repoPath(relativeDir, "CHANGELOG.md"); + if (!existsSync(changelogPath)) { + return `Published ${packageName}@${version}.`; + } + + const changelog = readFileSync(changelogPath, "utf8"); + const heading = new RegExp(`^##\\s+${escapeRegExp(version)}\\s*$`, "m"); + const match = heading.exec(changelog); + if (!match) { + return `Published ${packageName}@${version}.`; + } + + const start = match.index; + const afterHeading = changelog.slice(start); + const nextHeading = afterHeading.slice(match[0].length).search(/^##\s+/m); + const section = + nextHeading === -1 + ? afterHeading + : afterHeading.slice(0, match[0].length + nextHeading); + + return `# ${packageName}\n\n${section.trim()}`; +} + +export function packageArtifactBase(name, version) { + return `${name.replace(/^@/, "").replace(/[\\/@]/g, "-")}-${version}`; +} + +export function orderPackagesForPublish(packages) { + const packageMap = new Map( + packages.map((pkg) => [ + pkg.name, + { ...pkg, manifest: readPackage(pkg.dir) }, + ]), + ); + const visiting = new Set(); + const visited = new Set(); + const ordered = []; + + for (const pkg of packageMap.values()) { + visit(pkg); + } + + return ordered.map(({ manifest: _manifest, ...pkg }) => pkg); + + function visit(pkg) { + if (visited.has(pkg.name)) { + return; + } + + if (visiting.has(pkg.name)) { + throw new Error( + `Detected a publish dependency cycle involving ${pkg.name}`, + ); + } + + visiting.add(pkg.name); + + for (const dependencyName of getWorkspaceReleaseDependencies( + pkg.manifest, + )) { + const dependency = packageMap.get(dependencyName); + if (dependency) { + visit(dependency); + } + } + + visiting.delete(pkg.name); + visited.add(pkg.name); + ordered.push(pkg); + } +} + +function getWorkspaceReleaseDependencies(manifest) { + const dependencyNames = new Set(); + + for (const field of [ + "dependencies", + "optionalDependencies", + "peerDependencies", + "devDependencies", + ]) { + for (const dependencyName of Object.keys(manifest[field] ?? {})) { + dependencyNames.add(dependencyName); + } + } + + dependencyNames.delete(manifest.name); + return dependencyNames; +} + export function isPublishedToNpm(name, version) { const result = spawnSync( "npm", diff --git a/scripts/release/create-github-releases.mjs b/scripts/release/create-github-releases.mjs index 3975ebcf3..9c6786deb 100644 --- a/scripts/release/create-github-releases.mjs +++ b/scripts/release/create-github-releases.mjs @@ -1,6 +1,6 @@ -import { existsSync, readFileSync } from "node:fs"; +import { readFileSync } from "node:fs"; -import { escapeRegExp, parseArgs } from "./_shared.mjs"; +import { extractReleaseNotes, parseArgs } from "./_shared.mjs"; const args = parseArgs(); const manifestPath = args.manifest ?? ".release-manifest.json"; @@ -27,46 +27,23 @@ for (const pkg of manifest.packages) { if (existing.status === 200) { console.log(`GitHub release already exists for ${tag}`); - continue; + } else { + await fetchGithub(`/repos/${repository}/releases`, token, { + method: "POST", + body: JSON.stringify({ + tag_name: tag, + name: pkg.release_title ?? tag, + body: + pkg.release_body ?? + extractReleaseNotes(pkg.dir, pkg.name, pkg.version), + draft: false, + prerelease: false, + generate_release_notes: false, + }), + }).then((response) => response.json()); + + console.log(`Created GitHub release for ${tag}`); } - - await fetchGithub(`/repos/${repository}/releases`, token, { - method: "POST", - body: JSON.stringify({ - tag_name: tag, - name: tag, - body: extractReleaseNotes(pkg.dir, pkg.name, pkg.version), - draft: false, - prerelease: false, - generate_release_notes: false, - }), - }); - - console.log(`Created GitHub release for ${tag}`); -} - -function extractReleaseNotes(relativeDir, packageName, version) { - const changelogPath = `${relativeDir}/CHANGELOG.md`; - if (!existsSync(changelogPath)) { - return `Published ${packageName}@${version}.`; - } - - const changelog = readFileSync(changelogPath, "utf8"); - const heading = new RegExp(`^##\\s+${escapeRegExp(version)}\\s*$`, "m"); - const match = heading.exec(changelog); - if (!match) { - return `Published ${packageName}@${version}.`; - } - - const start = match.index; - const afterHeading = changelog.slice(start); - const nextHeading = afterHeading.slice(match[0].length).search(/^##\s+/m); - const section = - nextHeading === -1 - ? afterHeading - : afterHeading.slice(0, match[0].length + nextHeading); - - return `# ${packageName}\n\n${section.trim()}`; } async function fetchGithub(endpoint, authToken, options) { diff --git a/scripts/release/pack-publishable-packages.mjs b/scripts/release/pack-publishable-packages.mjs index fe73eeff8..4b5fabb48 100644 --- a/scripts/release/pack-publishable-packages.mjs +++ b/scripts/release/pack-publishable-packages.mjs @@ -1,11 +1,23 @@ import { execFileSync } from "node:child_process"; -import { mkdirSync, readFileSync, writeFileSync } from "node:fs"; +import { + mkdtempSync, + mkdirSync, + readdirSync, + readFileSync, + renameSync, + rmSync, + writeFileSync, +} from "node:fs"; +import os from "node:os"; import path from "node:path"; import { PUBLISHABLE_PACKAGES, appendSummary, + orderPackagesForPublish, + packageArtifactBase, parseArgs, + readPackage, repoPath, } from "./_shared.mjs"; @@ -16,37 +28,76 @@ const reportPath = args.report ?? path.posix.join(outputDir, "pack-report.json"); const targets = getTargets(manifestPath); -mkdirSync(repoPath(outputDir), { recursive: true }); +const absoluteOutputDir = repoPath(outputDir); +mkdirSync(absoluteOutputDir, { recursive: true }); -const tarballs = []; +const artifacts = []; for (const target of targets) { - const relativeOutputDir = path.posix.relative(target.dir, outputDir); - const tarball = execFileSync( - "npm", - ["pack", "--pack-destination", relativeOutputDir], - { + const packDir = mkdtempSync(path.join(os.tmpdir(), "braintrust-pack-")); + try { + execFileSync("pnpm", ["pack", "--pack-destination", packDir], { cwd: repoPath(target.dir), - encoding: "utf8", - }, - ).trim(); + stdio: "inherit", + }); + + const packedTarballs = readdirSync(packDir).filter((file) => + file.endsWith(".tgz"), + ); + if (packedTarballs.length !== 1) { + throw new Error( + `Expected pnpm pack for ${target.name} to create one tarball, found ${packedTarballs.length}`, + ); + } + + const tarballAsset = + target.tarball_asset ?? + `${packageArtifactBase(target.name, target.version)}.tgz`; + const sbomAsset = + target.sbom_asset ?? + `${packageArtifactBase(target.name, target.version)}.sbom.json`; + const tarballPath = path.join(absoluteOutputDir, tarballAsset); + const sbomPath = path.join(absoluteOutputDir, sbomAsset); + + renameSync(path.join(packDir, packedTarballs[0]), tarballPath); + execFileSync( + "pnpm", + [ + "--dir", + repoPath(target.dir), + "sbom", + "--sbom-format", + "cyclonedx", + "--prod", + "--out", + sbomPath, + ], + { stdio: "inherit" }, + ); - tarballs.push({ - name: target.name, - dir: target.dir, - version: target.version, - tarball, - }); + artifacts.push({ + name: target.name, + dir: target.dir, + version: target.version, + tarball: path.relative(repoPath(), tarballPath), + sbom: path.relative(repoPath(), sbomPath), + }); + } finally { + rmSync(packDir, { force: true, recursive: true }); + } } writeFileSync( repoPath(reportPath), - `${JSON.stringify({ tarballs }, null, 2)}\n`, + `${JSON.stringify({ artifacts }, null, 2)}\n`, "utf8", ); -console.log(`Packed ${tarballs.length} package(s) into ${outputDir}`); +console.log(`Packed ${artifacts.length} package(s) into ${outputDir}`); appendSummary( - `## Packed publishable packages\n\n${tarballs - .map((entry) => `- ${entry.name}@${entry.version}: ${entry.tarball}`) + `## Packed publishable packages\n\n${artifacts + .map( + (entry) => + `- ${entry.name}@${entry.version}: ${entry.tarball}, ${entry.sbom}`, + ) .join("\n")}`, ); @@ -60,13 +111,13 @@ function getTargets(maybeManifestPath) { const manifest = JSON.parse( readFileSync(repoPath(maybeManifestPath), "utf8"), ); - return manifest.packages.map((pkg) => readPackageInfo(pkg.dir, pkg.name)); + return orderPackagesForPublish(manifest.packages ?? []).map((pkg) => + readPackageInfo(pkg.dir, pkg.name, pkg), + ); } -function readPackageInfo(relativeDir, expectedName) { - const manifest = JSON.parse( - readFileSync(repoPath(relativeDir, "package.json"), "utf8"), - ); +function readPackageInfo(relativeDir, expectedName, manifestEntry = {}) { + const manifest = readPackage(relativeDir); if (manifest.name !== expectedName) { throw new Error( @@ -75,6 +126,7 @@ function readPackageInfo(relativeDir, expectedName) { } return { + ...manifestEntry, dir: relativeDir, name: manifest.name, version: manifest.version, diff --git a/scripts/release/publish-release-manifest.mjs b/scripts/release/publish-release-manifest.mjs index 22631221a..44500a5dd 100644 --- a/scripts/release/publish-release-manifest.mjs +++ b/scripts/release/publish-release-manifest.mjs @@ -3,6 +3,7 @@ import { readFileSync } from "node:fs"; import { isPublishedToNpm, + orderPackagesForPublish, parseArgs, readPackage, repoPath, @@ -49,66 +50,3 @@ for (const pkg of packages) { stdio: "inherit", }); } - -function orderPackagesForPublish(packages) { - const packageMap = new Map( - packages.map((pkg) => [ - pkg.name, - { ...pkg, manifest: readPackage(pkg.dir) }, - ]), - ); - const visiting = new Set(); - const visited = new Set(); - const ordered = []; - - for (const pkg of packageMap.values()) { - visit(pkg); - } - - return ordered.map(({ manifest: _manifest, ...pkg }) => pkg); - - function visit(pkg) { - if (visited.has(pkg.name)) { - return; - } - - if (visiting.has(pkg.name)) { - throw new Error( - `Detected a publish dependency cycle involving ${pkg.name}`, - ); - } - - visiting.add(pkg.name); - - for (const dependencyName of getWorkspaceReleaseDependencies( - pkg.manifest, - )) { - const dependency = packageMap.get(dependencyName); - if (dependency) { - visit(dependency); - } - } - - visiting.delete(pkg.name); - visited.add(pkg.name); - ordered.push(pkg); - } -} - -function getWorkspaceReleaseDependencies(manifest) { - const dependencyNames = new Set(); - - for (const field of [ - "dependencies", - "optionalDependencies", - "peerDependencies", - "devDependencies", - ]) { - for (const dependencyName of Object.keys(manifest[field] ?? {})) { - dependencyNames.add(dependencyName); - } - } - - dependencyNames.delete(manifest.name); - return dependencyNames; -} diff --git a/scripts/release/release-manifest.mjs b/scripts/release/release-manifest.mjs index 65f6e4a8a..ebc26a6ba 100644 --- a/scripts/release/release-manifest.mjs +++ b/scripts/release/release-manifest.mjs @@ -3,11 +3,14 @@ import { readFileSync, writeFileSync } from "node:fs"; import { PUBLISHABLE_PACKAGES, + extractReleaseNotes, filterPublishableReleases, formatPackageList, getApprovedPackageByName, getReleaseTag, isPublishedToNpm, + orderPackagesForPublish, + packageArtifactBase, parseArgs, readPackage, writeGithubOutput, @@ -49,23 +52,22 @@ function handleStatusManifest({ }) { const status = JSON.parse(readFileSync(currentStatusPath, "utf8")); const releases = filterPublishableReleases(status); - const packages = releases.map((release) => { - const approved = getApprovedPackageByName(release.name); - if (!approved) { - throw new Error( - `Unapproved publishable package in status file: ${release.name}`, - ); - } - - const manifest = readPackage(approved.dir); - return { - dir: approved.dir, - name: manifest.name, - version: manifest.version, - type: release.type, - tag: getReleaseTag(manifest.name, manifest.version), - }; - }); + const packages = orderPackagesForPublish( + releases.map((release) => { + const approved = getApprovedPackageByName(release.name); + if (!approved) { + throw new Error( + `Unapproved publishable package in status file: ${release.name}`, + ); + } + + const packageJson = readPackage(approved.dir); + return { + ...buildPackageManifest(approved.dir, packageJson), + type: release.type, + }; + }), + ); writeManifestFile(currentOutputPath, { mode: currentMode, @@ -75,9 +77,11 @@ function handleStatusManifest({ const markdownList = packagesToMarkdown(packages); const plainList = packagesToPlain(packages); + const packageNamesJson = packagesToNamesJson(packages); writeGithubOutput("has_packages", packages.length > 0); writeGithubOutput("package_count", packages.length); + writeGithubOutput("package_names_json", packageNamesJson); writeGithubOutput("title", currentTitle); writeGithubOutput("markdown", markdownList); writeGithubOutput("plain", plainList); @@ -99,7 +103,7 @@ function handleStableManifest({ outputPath: currentOutputPath, title: currentTitle, }) { - const packages = getUnpublishedPackages(); + const packages = orderPackagesForPublish(getUnpublishedPackages()); const hasWork = packages.length > 0; writeManifestFile(currentOutputPath, { @@ -110,10 +114,12 @@ function handleStableManifest({ const markdownList = packagesToMarkdown(packages); const plainList = packagesToPlain(packages); + const packageNamesJson = packagesToNamesJson(packages); writeGithubOutput("has_work", hasWork); writeGithubOutput("needs_publish", hasWork); writeGithubOutput("package_count", packages.length); + writeGithubOutput("package_names_json", packageNamesJson); writeGithubOutput("title", currentTitle); writeGithubOutput("markdown", markdownList); writeGithubOutput("plain", plainList); @@ -136,15 +142,37 @@ function getUnpublishedPackages() { } return { - dir: approved.dir, - name: manifest.name, - version: manifest.version, - tag: getReleaseTag(manifest.name, manifest.version), + ...buildPackageManifest(approved.dir, manifest), published: false, }; }).filter(Boolean); } +function buildPackageManifest(dir, packageJson) { + const tag = getReleaseTag(packageJson.name, packageJson.version); + const artifactBase = packageArtifactBase( + packageJson.name, + packageJson.version, + ); + + return { + dir, + name: packageJson.name, + version: packageJson.version, + tag, + tarball_asset: `${artifactBase}.tgz`, + sbom_asset: `${artifactBase}.sbom.json`, + release_title: tag, + release_body: extractReleaseNotes( + dir, + packageJson.name, + packageJson.version, + ), + channel: "latest", + provenance: packageJson.publishConfig?.provenance ?? true, + }; +} + function writeManifestFile(currentOutputPath, manifest) { writeFileSync( currentOutputPath, @@ -165,6 +193,10 @@ function packagesToPlain(packages) { : packages.map((pkg) => `${pkg.name}@${pkg.version}`).join(", "); } +function packagesToNamesJson(packages) { + return JSON.stringify(packages.map((pkg) => pkg.name)); +} + function getTitle(currentMode) { return currentMode .split(/[-_\s]+/)