From 543140b37dfc4e2b0051757e81975343f1853c27 Mon Sep 17 00:00:00 2001 From: Abhijeet Prasad Date: Fri, 3 Apr 2026 22:42:52 -0400 Subject: [PATCH 1/2] chore: migrate to changesets for release management Replace the custom tag-based release scripts with @changesets/cli for versioning and publishing across all workspace packages. - Add @changesets/cli and @changesets/changelog-github to devDependencies - Add .changeset config and initial changeset entries - Add repository, homepage, and publishConfig to all integration package.json files - Fix repository.directory in js/package.json - Add centralized release scripts under scripts/release/ - Remove legacy js/scripts/ release and validation scripts - Remove publish:validate script from js/package.json - Remove workspaces field from root package.json (pnpm-workspace.yaml is sufficient) - Add changeset convenience scripts to root package.json - Update CI workflows for changeset-based publishing - Generate initial CHANGELOGs for all packages --- .changeset/README.md | 8 + .changeset/config.json | 21 + .github/workflows/checks.yaml | 84 +- .../publish-js-sdk-canary-scheduler.yaml | 47 - .github/workflows/publish-js-sdk.yaml | 854 ++++++++---------- PUBLISHING_JS.md | 253 ++++-- integrations/browser-js/CHANGELOG.md | 9 + integrations/browser-js/package.json | 2 +- integrations/langchain-js/CHANGELOG.md | 8 + integrations/langchain-js/package.json | 10 + integrations/openai-agents-js/CHANGELOG.md | 8 + integrations/openai-agents-js/package.json | 10 + integrations/otel-js/CHANGELOG.md | 8 + integrations/otel-js/package.json | 10 + integrations/templates-nunjucks/CHANGELOG.md | 8 + integrations/templates-nunjucks/package.json | 10 + integrations/temporal-js/CHANGELOG.md | 9 + integrations/temporal-js/package.json | 10 + integrations/vercel-ai-sdk/CHANGELOG.md | 9 + integrations/vercel-ai-sdk/package.json | 10 + js/CHANGELOG.md | 12 +- js/package.json | 7 +- js/scripts/check-remote-tag.sh | 14 - js/scripts/dispatch-release-workflow.sh | 42 - js/scripts/publish-prerelease.sh | 135 --- js/scripts/push-release-tag.sh | 102 --- js/scripts/validate-release-tag.sh | 63 -- js/scripts/validate-release.sh | 28 - package.json | 8 +- pnpm-lock.yaml | 497 +++++++++- scripts/release/_shared.mjs | 207 +++++ scripts/release/build-publishable-packages.sh | 22 + scripts/release/check-changeset-status.mjs | 52 ++ scripts/release/create-github-releases.mjs | 96 ++ scripts/release/enforce-changeset.mjs | 75 ++ scripts/release/pack-publishable-packages.mjs | 82 ++ scripts/release/prepare-release-manifest.mjs | 64 ++ scripts/release/push-release-tags.mjs | 90 ++ scripts/release/should-publish-canary.mjs | 174 ++++ scripts/release/should-publish-stable.mjs | 98 ++ scripts/release/summarize-release.mjs | 46 + .../release/validate-publishable-packages.mjs | 128 +++ 42 files changed, 2395 insertions(+), 1035 deletions(-) create mode 100644 .changeset/README.md create mode 100644 .changeset/config.json delete mode 100644 .github/workflows/publish-js-sdk-canary-scheduler.yaml create mode 100644 integrations/browser-js/CHANGELOG.md create mode 100644 integrations/langchain-js/CHANGELOG.md create mode 100644 integrations/openai-agents-js/CHANGELOG.md create mode 100644 integrations/otel-js/CHANGELOG.md create mode 100644 integrations/templates-nunjucks/CHANGELOG.md create mode 100644 integrations/temporal-js/CHANGELOG.md create mode 100644 integrations/vercel-ai-sdk/CHANGELOG.md delete mode 100755 js/scripts/check-remote-tag.sh delete mode 100755 js/scripts/dispatch-release-workflow.sh delete mode 100755 js/scripts/publish-prerelease.sh delete mode 100755 js/scripts/push-release-tag.sh delete mode 100755 js/scripts/validate-release-tag.sh delete mode 100755 js/scripts/validate-release.sh create mode 100644 scripts/release/_shared.mjs create mode 100755 scripts/release/build-publishable-packages.sh create mode 100644 scripts/release/check-changeset-status.mjs create mode 100644 scripts/release/create-github-releases.mjs create mode 100644 scripts/release/enforce-changeset.mjs create mode 100644 scripts/release/pack-publishable-packages.mjs create mode 100644 scripts/release/prepare-release-manifest.mjs create mode 100644 scripts/release/push-release-tags.mjs create mode 100644 scripts/release/should-publish-canary.mjs create mode 100644 scripts/release/should-publish-stable.mjs create mode 100644 scripts/release/summarize-release.mjs create mode 100644 scripts/release/validate-publishable-packages.mjs diff --git a/.changeset/README.md b/.changeset/README.md new file mode 100644 index 000000000..654c6d475 --- /dev/null +++ b/.changeset/README.md @@ -0,0 +1,8 @@ +# Changesets + +Hello and welcome! This folder has been automatically generated by `@changesets/cli`, a build tool that works +with multi-package repos, or single-package repos to help you version and publish your code. You can +find the full documentation for it [in our repository](https://github.com/changesets/changesets). + +We have a quick list of common questions to get you started engaging with this project in +[our documentation](https://github.com/changesets/changesets/blob/main/docs/common-questions.md). diff --git a/.changeset/config.json b/.changeset/config.json new file mode 100644 index 000000000..e90fdfce8 --- /dev/null +++ b/.changeset/config.json @@ -0,0 +1,21 @@ +{ + "$schema": "https://unpkg.com/@changesets/config/schema.json", + "changelog": [ + "@changesets/changelog-github", + { + "repo": "braintrustdata/braintrust-sdk-javascript" + } + ], + "commit": false, + "fixed": [], + "linked": [], + "access": "public", + "baseBranch": "main", + "updateInternalDependencies": "patch", + "bumpVersionsWithWorkspaceProtocolOnly": true, + "ignore": [], + "snapshot": { + "useCalculatedVersion": true, + "prereleaseTemplate": "{tag}.{datetime}.{commit}" + } +} diff --git a/.github/workflows/checks.yaml b/.github/workflows/checks.yaml index 25e6966d3..244bc6fac 100644 --- a/.github/workflows/checks.yaml +++ b/.github/workflows/checks.yaml @@ -81,6 +81,22 @@ jobs: - name: Ensure SHA pinned actions uses: zgosalvez/github-actions-ensure-sha-pinned-actions@ca46236c6ce584ae24bc6283ba8dcf4b3ec8a066 # v5.0.4 + changeset-required: + if: github.event_name == 'pull_request' + runs-on: ubuntu-latest + timeout-minutes: 10 + steps: + - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1 + with: + fetch-depth: 0 + - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 + with: + node-version-file: .tool-versions + - name: Fetch pull request base ref + run: git fetch origin "${{ github.base_ref }}:refs/remotes/origin/${{ github.base_ref }}" + - name: Enforce changeset requirement for publishable package changes + run: node scripts/release/enforce-changeset.mjs + js-test: runs-on: ${{ matrix.os }} timeout-minutes: 30 @@ -131,72 +147,16 @@ jobs: run: | echo "Artifact: $ARTIFACT_NAME" echo "name=$ARTIFACT_NAME" >> "$GITHUB_OUTPUT" - - name: Prepare artifact - id: prepare_artifact - working-directory: js - shell: bash - run: | - mkdir -p artifacts - PACKED_TARBALL=$(npm pack --pack-destination artifacts) - echo "packed_tarball=$PACKED_TARBALL" >> "$GITHUB_OUTPUT" - - name: Pack @braintrust/browser - id: prepare_browser_artifact - working-directory: integrations/browser-js - shell: bash - run: | - PACKED_BROWSER_TARBALL=$(npm pack --pack-destination ../../js/artifacts) - echo "packed_browser_tarball=$PACKED_BROWSER_TARBALL" >> "$GITHUB_OUTPUT" - - name: Build and pack @braintrust/otel - id: prepare_otel_artifact - shell: bash - run: | - BRAINTRUST_TARBALL=$(ls js/artifacts/braintrust-*.tgz | head -n 1) - if [ -z "$BRAINTRUST_TARBALL" ]; then - echo "Error: braintrust tarball not found" - exit 1 - fi - echo "Using braintrust tarball: $BRAINTRUST_TARBALL" - - cd integrations/otel-js - if ! npm_config_save=false npm_config_lockfile=false pnpm add \ - file:../../$BRAINTRUST_TARBALL \ - @opentelemetry/api@^1.9.0 \ - @opentelemetry/core@^1.9.0 \ - @opentelemetry/exporter-trace-otlp-http@^0.35.0 \ - @opentelemetry/sdk-trace-base@^1.9.0; then - echo "Error: Failed to install dependencies" - exit 1 - fi - - pnpm run build - - PACKED_OTEL_TARBALL=$(npm pack --pack-destination ../../js/artifacts) - echo "packed_otel_tarball=$PACKED_OTEL_TARBALL" >> "$GITHUB_OUTPUT" - - name: Build and pack @braintrust/templates-nunjucks - id: prepare_templates_nunjucks_artifact - shell: bash - run: | - cd integrations/templates-nunjucks - pnpm run build - PACKED_NUNJUCKS_TARBALL=$(npm pack --pack-destination ../../js/artifacts) - echo "packed_nunjucks_tarball=$PACKED_NUNJUCKS_TARBALL" >> "$GITHUB_OUTPUT" + - name: Pack publishable packages + run: node scripts/release/pack-publishable-packages.mjs --output-dir js/artifacts - name: List artifacts before upload shell: bash - run: | - echo "Braintrust tarball: ${{ steps.prepare_artifact.outputs.packed_tarball }}" - echo "Browser tarball: ${{ steps.prepare_browser_artifact.outputs.packed_browser_tarball }}" - echo "Otel tarball: ${{ steps.prepare_otel_artifact.outputs.packed_otel_tarball }}" - echo "Templates-nunjucks tarball: ${{ steps.prepare_templates_nunjucks_artifact.outputs.packed_nunjucks_tarball }}" - ls -la js/artifacts/ + run: ls -la js/artifacts/ - name: Upload build artifacts uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 with: name: ${{ steps.artifact.outputs.name }}-dist - path: | - js/artifacts/${{ steps.prepare_artifact.outputs.packed_tarball }} - js/artifacts/${{ steps.prepare_browser_artifact.outputs.packed_browser_tarball }} - js/artifacts/${{ steps.prepare_otel_artifact.outputs.packed_otel_tarball }} - js/artifacts/${{ steps.prepare_templates_nunjucks_artifact.outputs.packed_nunjucks_tarball }} + path: js/artifacts/*.tgz retention-days: 1 e2e-hermetic: @@ -238,7 +198,7 @@ jobs: id: published-version shell: bash run: | - PUBLISHED_VERSION=$(npm view braintrust version 2>/dev/null || echo "none") + PUBLISHED_VERSION=$(cd /tmp && npm view braintrust version 2>/dev/null || echo "none") echo "version=$PUBLISHED_VERSION" >> "$GITHUB_OUTPUT" echo "Published version: $PUBLISHED_VERSION" - name: Cache API compatibility test tarball @@ -512,6 +472,7 @@ jobs: - check-typings - dead-code - ensure-pinned-actions + - changeset-required - js-test - js-build - e2e-hermetic @@ -545,6 +506,7 @@ jobs: check_result "check-typings" "${{ needs.check-typings.result }}" check_result "dead-code" "${{ needs.dead-code.result }}" check_result "ensure-pinned-actions" "${{ needs.ensure-pinned-actions.result }}" + check_result "changeset-required" "${{ needs.changeset-required.result }}" check_result "js-test" "${{ needs.js-test.result }}" check_result "js-build" "${{ needs.js-build.result }}" check_result "e2e-hermetic" "${{ needs.e2e-hermetic.result }}" diff --git a/.github/workflows/publish-js-sdk-canary-scheduler.yaml b/.github/workflows/publish-js-sdk-canary-scheduler.yaml deleted file mode 100644 index a8f449ec3..000000000 --- a/.github/workflows/publish-js-sdk-canary-scheduler.yaml +++ /dev/null @@ -1,47 +0,0 @@ -# This workflow is responsible for scheduling the JavaScript SDK canary publish workflow on a daily basis. -# The actual publish workflow is defined in `publish-js-sdk.yaml`, which is triggered by this scheduler workflow. -# `publish-js-sdk.yaml` has to be where the publish actually happens so that npm trusted publishing still works. -# npm trusted publishing that only one workflow can publish to npm. - -name: Schedule JavaScript SDK Canary Publish - -concurrency: - group: publish-js-sdk-canary-scheduler - cancel-in-progress: false - -on: - schedule: - - cron: "17 6 * * *" - workflow_dispatch: - -jobs: - dispatch-canary-publish: - runs-on: ubuntu-latest - timeout-minutes: 5 - permissions: - actions: write - steps: - - name: Dispatch publish workflow - uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 - with: - script: | - await github.rest.actions.createWorkflowDispatch({ - owner: context.repo.owner, - repo: context.repo.repo, - workflow_id: "publish-js-sdk.yaml", - ref: "main", - inputs: { - release_type: "canary", - branch: "main", - }, - }); - - - name: Summarize dispatch - run: | - { - echo "## JavaScript SDK Canary Dispatch Queued" - echo - echo "- Workflow: \`publish-js-sdk.yaml\`" - echo "- Release type: \`canary\`" - echo "- Branch: \`main\`" - } >> "$GITHUB_STEP_SUMMARY" diff --git a/.github/workflows/publish-js-sdk.yaml b/.github/workflows/publish-js-sdk.yaml index 34e0daf37..04b10cbf0 100644 --- a/.github/workflows/publish-js-sdk.yaml +++ b/.github/workflows/publish-js-sdk.yaml @@ -1,534 +1,494 @@ -# -# This workflow publishes the JavaScript SDK to npm. -# -# It supports three modes: -# -# 1. STABLE RELEASE: -# - Manually triggered via GitHub Actions UI -# - Releases the exact version already set in js/package.json -# - Fails if a js-sdk-v tag already exists -# - Creates and pushes the git tag from the workflow before publishing -# -# 2. PRE-RELEASE: -# - Manually triggered via GitHub Actions UI -# - Automatically appends "rc.{run_number}" to the current package.json version -# - Publishes with the "rc" dist-tag -# - Does NOT require updating package.json in the repository -# -# 3. CANARY RELEASE: -# - Triggered manually or by the canary scheduler workflow -# - Publishes with the "canary" dist-tag -# - Reuses this workflow file so npm trusted publishing only needs one publisher -# - -name: Publish JavaScript SDK - -concurrency: - group: publish-js-sdk-${{ inputs.release_type }}-${{ inputs.branch }} - cancel-in-progress: false +name: Release Packages on: + push: + branches: + - main + schedule: + - cron: "0 4 * * *" # Nightly at 04:00 UTC workflow_dispatch: inputs: - release_type: - description: Release type + release_mode: + description: Release mode required: true - default: stable type: choice options: - - stable - prerelease - canary + - dry-run-stable + - dry-run-prerelease + - dry-run-canary branch: - description: Branch to release from - required: true + description: Branch to use for prerelease modes + required: false + type: string + ref: + description: Ref to use for stable/canary dry runs + required: false default: main type: string -jobs: - prepare-release: - runs-on: ubuntu-latest - timeout-minutes: 10 - outputs: - version: ${{ steps.release_metadata.outputs.version }} - is_prerelease: ${{ steps.release_metadata.outputs.is_prerelease }} - is_canary: ${{ steps.release_metadata.outputs.is_canary }} - release_tag: ${{ steps.release_metadata.outputs.release_tag }} - branch: ${{ steps.release_metadata.outputs.branch }} - commit: ${{ steps.release_metadata.outputs.commit }} - release_type: ${{ steps.release_metadata.outputs.release_type }} - steps: - - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1 - with: - fetch-depth: 1 - ref: ${{ inputs.branch }} - - name: Set up Node.js - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 - with: - node-version-file: .tool-versions - - name: Setup mise - uses: jdx/mise-action@1648a7812b9aeae629881980618f079932869151 # v4.0.1 - - name: Determine release metadata - id: release_metadata - working-directory: js - env: - RELEASE_TYPE: ${{ inputs.release_type }} - TARGET_BRANCH: ${{ inputs.branch }} - run: | - set -euo pipefail - - CURRENT_VERSION=$(node -p "require('./package.json').version") - RELEASE_COMMIT=$(git rev-parse HEAD) - CURRENT_SHA=$(git rev-parse --short=7 HEAD) - - echo "release_type=${RELEASE_TYPE}" >> "$GITHUB_OUTPUT" - echo "branch=${TARGET_BRANCH}" >> "$GITHUB_OUTPUT" - echo "commit=${RELEASE_COMMIT}" >> "$GITHUB_OUTPUT" - - if [[ "$RELEASE_TYPE" == "stable" ]]; then - RELEASE_TAG="js-sdk-v${CURRENT_VERSION}" - ./scripts/check-remote-tag.sh "${RELEASE_TAG}" - - echo "version=${CURRENT_VERSION}" >> "$GITHUB_OUTPUT" - echo "is_prerelease=false" >> "$GITHUB_OUTPUT" - echo "is_canary=false" >> "$GITHUB_OUTPUT" - echo "release_tag=${RELEASE_TAG}" >> "$GITHUB_OUTPUT" - elif [[ "$RELEASE_TYPE" == "prerelease" ]]; then - VERSION="${CURRENT_VERSION}-rc.${GITHUB_RUN_NUMBER}" - - echo "version=${VERSION}" >> "$GITHUB_OUTPUT" - echo "is_prerelease=true" >> "$GITHUB_OUTPUT" - echo "is_canary=false" >> "$GITHUB_OUTPUT" - echo "release_tag=" >> "$GITHUB_OUTPUT" - else - VERSION="${CURRENT_VERSION}-canary.$(date -u +%Y%m%d).${GITHUB_RUN_NUMBER}.g${CURRENT_SHA}" +concurrency: + group: >- + release-${{ github.event_name == 'push' && 'stable-main' || github.event_name == 'schedule' && 'canary-nightly' || format('{0}-{1}', inputs.release_mode, inputs.branch || inputs.ref || 'main') }} + cancel-in-progress: false - echo "version=${VERSION}" >> "$GITHUB_OUTPUT" - echo "is_prerelease=false" >> "$GITHUB_OUTPUT" - echo "is_canary=true" >> "$GITHUB_OUTPUT" - echo "release_tag=" >> "$GITHUB_OUTPUT" - fi +env: + HUSKY: "0" - build-and-publish-stable: - needs: prepare-release - if: needs.prepare-release.outputs.release_type == 'stable' +jobs: + stable-release-pr: + if: github.event_name == 'push' && github.ref_name == 'main' runs-on: ubuntu-latest - timeout-minutes: 20 + timeout-minutes: 15 permissions: contents: write - id-token: write - environment: npm-publish - env: - VERSION: ${{ needs.prepare-release.outputs.version }} - RELEASE_TAG: ${{ needs.prepare-release.outputs.release_tag }} - TARGET_BRANCH: ${{ needs.prepare-release.outputs.branch }} - RELEASE_COMMIT: ${{ needs.prepare-release.outputs.commit }} + pull-requests: write steps: - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1 with: fetch-depth: 0 - ref: ${{ needs.prepare-release.outputs.branch }} - - name: Set up Node.js - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 + - uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v5.0.0 + - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 with: node-version-file: .tool-versions - registry-url: "https://registry.npmjs.org" - - name: Setup mise - uses: jdx/mise-action@1648a7812b9aeae629881980618f079932869151 # v4.0.1 - - name: Publish to npm (stable release) - working-directory: js - env: - RELEASE_BRANCH: ${{ env.TARGET_BRANCH }} - run: pnpm run publish:validate - - - name: Create and push release tag - run: | - set -euo pipefail - - ./js/scripts/check-remote-tag.sh "${RELEASE_TAG}" - git config user.name "github-actions[bot]" - git config user.email "41898282+github-actions[bot]@users.noreply.github.com" - git tag "${RELEASE_TAG}" "${RELEASE_COMMIT}" - git push origin "${RELEASE_TAG}" - - - name: Upload build artifacts - uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 + cache: pnpm + - name: Install dependencies + run: pnpm install --frozen-lockfile + - name: Validate publishable package metadata + run: node scripts/release/validate-publishable-packages.mjs + - name: Create or update release PR + id: changesets + uses: changesets/action@d4c53c294341eec8a419ec2d1927138bfdeec234 with: - name: javascript-sdk-release-dist - path: js/dist/ - retention-days: 5 - - - name: Generate release notes - id: release_notes - run: | - RELEASE_NOTES=$(.github/scripts/generate-release-notes.sh "${RELEASE_TAG}" "js/") - echo "notes<> "$GITHUB_OUTPUT" - echo "$RELEASE_NOTES" >> "$GITHUB_OUTPUT" - echo "EOF" >> "$GITHUB_OUTPUT" - echo "release_name=JavaScript SDK v${VERSION}" >> "$GITHUB_OUTPUT" - - - name: Create GitHub Release - uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 + version: pnpm exec changeset version && pnpm install --lockfile-only + commit: "[ci] release" + title: "[ci] release" env: - RELEASE_NOTES: ${{ steps.release_notes.outputs.notes }} - RELEASE_NAME: ${{ steps.release_notes.outputs.release_name }} - with: - script: | - await github.rest.repos.createRelease({ - owner: context.repo.owner, - repo: context.repo.repo, - tag_name: process.env.RELEASE_TAG, - name: process.env.RELEASE_NAME, - body: process.env.RELEASE_NOTES, - draft: false, - prerelease: false - }); + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + - name: Summarize stable release PR state + if: always() + run: | + if [ "${{ steps.changesets.outputs.hasChangesets }}" = "true" ]; then + { + echo "## Stable release PR" + echo + echo "Pending changesets were found and the release PR was refreshed." + } >> "$GITHUB_STEP_SUMMARY" + else + { + echo "## Stable release PR" + echo + echo "No pending changesets to release from main." + } >> "$GITHUB_STEP_SUMMARY" + fi - build-and-publish-prerelease: - needs: prepare-release - if: needs.prepare-release.outputs.release_type == 'prerelease' + stable-detect-publish: + if: github.event_name == 'push' && github.ref_name == 'main' + needs: stable-release-pr runs-on: ubuntu-latest - timeout-minutes: 20 + timeout-minutes: 15 permissions: - contents: write - id-token: write - env: - VERSION: ${{ needs.prepare-release.outputs.version }} + contents: read + outputs: + has_work: ${{ steps.detect.outputs.has_work }} + needs_publish: ${{ steps.detect.outputs.needs_publish }} + needs_tags: ${{ steps.detect.outputs.needs_tags }} + needs_github_releases: ${{ steps.detect.outputs.needs_github_releases }} + manifest_path: ${{ steps.detect.outputs.manifest_path }} steps: - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1 with: fetch-depth: 0 - ref: ${{ needs.prepare-release.outputs.branch }} - - name: Set up Node.js - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 + - uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v5.0.0 + - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 with: node-version-file: .tool-versions - registry-url: "https://registry.npmjs.org" - - name: Setup mise - uses: jdx/mise-action@1648a7812b9aeae629881980618f079932869151 # v4.0.1 - - name: Publish pre-release - working-directory: js - run: ./scripts/publish-prerelease.sh "rc" "$VERSION" - - - name: Upload build artifacts - uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 - with: - name: javascript-sdk-prerelease-dist - path: js/dist/ - retention-days: 5 + cache: pnpm + - name: Install dependencies + run: pnpm install --frozen-lockfile + - name: Validate publishable package metadata + run: node scripts/release/validate-publishable-packages.mjs + - name: Detect stable publish work + id: detect + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: node scripts/release/should-publish-stable.mjs --output .release-manifest.json - publish-canary: - needs: prepare-release - if: needs.prepare-release.outputs.release_type == 'canary' + stable-publish: + if: github.event_name == 'push' && github.ref_name == 'main' && needs.stable-detect-publish.outputs.has_work == 'true' + needs: stable-detect-publish runs-on: ubuntu-latest - timeout-minutes: 20 - outputs: - published: ${{ steps.publish_status.outputs.published }} - version: ${{ steps.publish_status.outputs.version }} - package_name: ${{ steps.publish_status.outputs.package_name }} - commit_sha: ${{ steps.publish_status.outputs.commit_sha }} - reason: ${{ steps.publish_status.outputs.reason }} + timeout-minutes: 30 permissions: - actions: read - contents: read + contents: write id-token: write environment: npm-publish - env: - TARGET_BRANCH: ${{ needs.prepare-release.outputs.branch }} - VERSION: ${{ needs.prepare-release.outputs.version }} steps: - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1 with: fetch-depth: 0 - ref: ${{ needs.prepare-release.outputs.branch }} - - - name: Set up Node.js - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 + - uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v5.0.0 + - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 with: node-version-file: .tool-versions - registry-url: "https://registry.npmjs.org" - - name: Setup mise - uses: jdx/mise-action@1648a7812b9aeae629881980618f079932869151 # v4.0.1 - - - name: Check whether a new canary is needed - id: should_publish - run: | - set -euo pipefail - - PACKAGE_NAME="braintrust" - CURRENT_SHA=$(git rev-parse --short=7 HEAD) - PUBLISHED_VERSION=$(npm view "${PACKAGE_NAME}@canary" version --registry=https://registry.npmjs.org 2>/dev/null || true) - - if [ -z "$PUBLISHED_VERSION" ]; then - echo "should_publish=true" >> "$GITHUB_OUTPUT" - echo "reason=No existing canary found on npm." >> "$GITHUB_OUTPUT" - echo "previous_version=" >> "$GITHUB_OUTPUT" - echo "previous_sha=" >> "$GITHUB_OUTPUT" - exit 0 - fi - - PUBLISHED_SHA=$(printf '%s\n' "$PUBLISHED_VERSION" | sed -n 's/.*\.g\([0-9a-f]\{7\}\)$/\1/p') - echo "previous_version=${PUBLISHED_VERSION}" >> "$GITHUB_OUTPUT" - echo "previous_sha=${PUBLISHED_SHA}" >> "$GITHUB_OUTPUT" - - if [ "$PUBLISHED_SHA" = "$CURRENT_SHA" ]; then - echo "should_publish=false" >> "$GITHUB_OUTPUT" - echo "reason=Current HEAD ${CURRENT_SHA} is already published as canary ${PUBLISHED_VERSION}." >> "$GITHUB_OUTPUT" - else - echo "should_publish=true" >> "$GITHUB_OUTPUT" - echo "reason=Published canary ${PUBLISHED_VERSION} does not match HEAD ${CURRENT_SHA}." >> "$GITHUB_OUTPUT" - fi - - - name: Check JS CI status - if: steps.should_publish.outputs.should_publish == 'true' - id: ci_status - uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 - env: - TARGET_BRANCH: ${{ env.TARGET_BRANCH }} - with: - script: | - const { owner, repo } = context.repo; - const response = await github.rest.actions.listWorkflowRuns({ - owner, - repo, - workflow_id: "js.yaml", - branch: process.env.TARGET_BRANCH, - status: "completed", - per_page: 1, - }); - - const run = response.data.workflow_runs[0]; - if (!run) { - core.setOutput("should_publish", "false"); - core.setOutput("reason", `No completed js.yaml run found on ${process.env.TARGET_BRANCH}.`); - return; - } - - if (run.conclusion !== "success") { - core.setOutput("should_publish", "false"); - core.setOutput( - "reason", - `Latest completed js.yaml run on ${process.env.TARGET_BRANCH} concluded with ${run.conclusion} (${run.html_url}).`, - ); - return; - } - - core.setOutput("should_publish", "true"); - core.setOutput( - "reason", - `Latest completed js.yaml run on ${process.env.TARGET_BRANCH} succeeded (${run.html_url}).`, - ); - + cache: pnpm + registry-url: https://registry.npmjs.org - name: Install dependencies - if: steps.should_publish.outputs.should_publish == 'true' && steps.ci_status.outputs.should_publish == 'true' run: pnpm install --frozen-lockfile - - - name: Prepare canary package metadata - if: steps.should_publish.outputs.should_publish == 'true' && steps.ci_status.outputs.should_publish == 'true' - id: metadata - working-directory: js + - name: Validate publishable package metadata + run: node scripts/release/validate-publishable-packages.mjs + - name: Detect stable publish work + id: detect env: - VERSION: ${{ env.VERSION }} + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: node scripts/release/should-publish-stable.mjs --output .release-manifest.json + - name: Configure git user run: | - set -euo pipefail - - CURRENT_SHA=$(git rev-parse --short=7 HEAD) - CANARY_NAME="braintrust" - # Do not use `npm version` or `pnpm version` here — both delegate to - # npm's arborist which crashes when it encounters pnpm's workspace:* - # protocol specifiers in node_modules after `pnpm install`. - node -e " - const fs = require('fs'); - const pkg = JSON.parse(fs.readFileSync('package.json', 'utf8')); - pkg.version = process.env.VERSION; - fs.writeFileSync('package.json', JSON.stringify(pkg, null, 2) + '\n'); - " - - echo "version=${VERSION}" >> "$GITHUB_OUTPUT" - echo "package_name=${CANARY_NAME}" >> "$GITHUB_OUTPUT" - echo "commit_sha=${CURRENT_SHA}" >> "$GITHUB_OUTPUT" - - - name: Build SDK - if: steps.should_publish.outputs.should_publish == 'true' && steps.ci_status.outputs.should_publish == 'true' - working-directory: js - run: pnpm run build - - - name: Publish canary to npm - if: steps.should_publish.outputs.should_publish == 'true' && steps.ci_status.outputs.should_publish == 'true' - working-directory: js - run: npm publish --tag canary - - - name: Upload build artifacts - if: steps.should_publish.outputs.should_publish == 'true' && steps.ci_status.outputs.should_publish == 'true' - uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 - with: - name: javascript-sdk-canary-dist - path: js/dist/ - retention-days: 5 - - - name: Summarize canary publish - if: steps.should_publish.outputs.should_publish == 'true' && steps.ci_status.outputs.should_publish == 'true' - env: - PACKAGE_NAME: ${{ steps.metadata.outputs.package_name }} - VERSION: ${{ steps.metadata.outputs.version }} - COMMIT_SHA: ${{ steps.metadata.outputs.commit_sha }} - PREVIOUS_VERSION: ${{ steps.should_publish.outputs.previous_version }} - PREVIOUS_SHA: ${{ steps.should_publish.outputs.previous_sha }} - run: | - set -euo pipefail - - { - echo "## JavaScript SDK Canary Published" - echo - echo "- Package: \`${PACKAGE_NAME}\`" - echo "- Version: \`${VERSION}\`" - echo "- Commit: \`${COMMIT_SHA}\`" - echo "- Registry: \`https://registry.npmjs.org\`" - echo "- Install: \`npm install ${PACKAGE_NAME}@canary\`" - echo - echo "### Included commits" - if [ -n "$PREVIOUS_SHA" ]; then - echo "- Previous canary: \`${PREVIOUS_VERSION}\`" - git log "${PREVIOUS_SHA}..HEAD" --pretty=format:"- %h %s (%an)" - else - echo "- Previous canary: none" - git log -n 20 --pretty=format:"- %h %s (%an)" - fi - } >> "$GITHUB_STEP_SUMMARY" - - - name: Summarize skipped canary publish - if: steps.should_publish.outputs.should_publish != 'true' || steps.ci_status.outputs.should_publish != 'true' + git config user.name "github-actions[bot]" + git config user.email "41898282+github-actions[bot]@users.noreply.github.com" + - name: Build publishable packages + if: steps.detect.outputs.needs_publish == 'true' + run: bash scripts/release/build-publishable-packages.sh .release-manifest.json + - name: Publish stable packages to npm + if: steps.detect.outputs.needs_publish == 'true' + run: pnpm exec changeset publish env: - SHOULD_PUBLISH_REASON: ${{ steps.should_publish.outputs.reason }} - CI_REASON: ${{ steps.ci_status.outputs.reason }} - run: | - set -euo pipefail - - REASON="${CI_REASON:-$SHOULD_PUBLISH_REASON}" - - { - echo "## JavaScript SDK Canary Skipped" - echo - echo "$REASON" - } >> "$GITHUB_STEP_SUMMARY" - - - name: Set publish status outputs - id: publish_status - if: always() + NPM_TOKEN: "" + - name: Push Changesets release tags + if: steps.detect.outputs.has_work == 'true' + run: node scripts/release/push-release-tags.mjs --manifest .release-manifest.json + - name: Create GitHub Releases + if: steps.detect.outputs.has_work == 'true' env: - SHOULD_PUBLISH: ${{ steps.should_publish.outputs.should_publish }} - CI_SHOULD_PUBLISH: ${{ steps.ci_status.outputs.should_publish }} - VERSION: ${{ steps.metadata.outputs.version }} - PACKAGE_NAME: ${{ steps.metadata.outputs.package_name }} - COMMIT_SHA: ${{ steps.metadata.outputs.commit_sha }} - SHOULD_PUBLISH_REASON: ${{ steps.should_publish.outputs.reason }} - CI_REASON: ${{ steps.ci_status.outputs.reason }} - run: | - set -euo pipefail - - if [ "${SHOULD_PUBLISH}" = "true" ] && [ "${CI_SHOULD_PUBLISH}" = "true" ]; then - echo "published=true" >> "$GITHUB_OUTPUT" - else - echo "published=false" >> "$GITHUB_OUTPUT" - fi - - REASON="${CI_REASON:-$SHOULD_PUBLISH_REASON}" - - echo "version=${VERSION}" >> "$GITHUB_OUTPUT" - echo "package_name=${PACKAGE_NAME}" >> "$GITHUB_OUTPUT" - echo "commit_sha=${COMMIT_SHA}" >> "$GITHUB_OUTPUT" - echo "reason=${REASON}" >> "$GITHUB_OUTPUT" - - notify-success: - needs: - [ - prepare-release, - build-and-publish-stable, - build-and-publish-prerelease, - publish-canary, - ] - if: | - always() && - ( - needs.build-and-publish-stable.result == 'success' || - needs.build-and-publish-prerelease.result == 'success' || - (needs.publish-canary.result == 'success' && needs.publish-canary.outputs.published == 'true') - ) - runs-on: ubuntu-latest - timeout-minutes: 5 - steps: - - name: Post to Slack on success (stable release) - if: needs.prepare-release.outputs.release_type == 'stable' + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: node scripts/release/create-github-releases.mjs --manifest .release-manifest.json + - name: Summarize stable release + id: summary + run: node scripts/release/summarize-release.mjs --mode stable --manifest .release-manifest.json + - name: Post stable release to Slack uses: slackapi/slack-github-action@af78098f536edbc4de71162a307590698245be95 # v3.0.1 with: method: chat.postMessage token: ${{ secrets.SLACK_BOT_TOKEN }} payload: | channel: C0ABHT0SWA2 - text: "✅ JavaScript SDK v${{ needs.prepare-release.outputs.version }} published" + text: "✅ JavaScript packages published" blocks: - type: "header" text: type: "plain_text" - text: "✅ JavaScript SDK Published" + text: "✅ JavaScript packages published" - type: "section" text: type: "mrkdwn" - text: "*Version:* ${{ needs.prepare-release.outputs.version }}\n*Branch:* `${{ needs.prepare-release.outputs.branch }}`\n*Package:* \n\n<${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}|View Run>" + text: "*Packages:*\n${{ steps.summary.outputs.markdown }}\n\n<${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}|View Run>" - - name: Post to Slack on success (pre-release) - if: needs.prepare-release.outputs.release_type == 'prerelease' + prerelease-snapshot: + if: github.event_name == 'workflow_dispatch' && inputs.release_mode == 'prerelease' + runs-on: ubuntu-latest + timeout-minutes: 30 + permissions: + contents: read + id-token: write + steps: + - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1 + with: + fetch-depth: 0 + ref: ${{ inputs.branch || github.ref_name }} + - uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v5.0.0 + - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 + with: + node-version-file: .tool-versions + cache: pnpm + registry-url: https://registry.npmjs.org + - name: Install dependencies + run: pnpm install --frozen-lockfile + - name: Validate publishable package metadata + run: node scripts/release/validate-publishable-packages.mjs + - name: Compute changeset status + run: pnpm exec changeset status --output=.changeset-status.json + - name: Check prerelease status + id: status_check + run: node scripts/release/check-changeset-status.mjs --mode prerelease --status-file .changeset-status.json + - name: Version snapshot prerelease packages + if: steps.status_check.outputs.has_packages == 'true' + run: pnpm exec changeset version --snapshot rc + - name: Prepare prerelease manifest + if: steps.status_check.outputs.has_packages == 'true' + id: manifest + run: node scripts/release/prepare-release-manifest.mjs --mode prerelease --status-file .changeset-status.json --output .release-manifest.json + - name: Build publishable packages + if: steps.status_check.outputs.has_packages == 'true' + run: bash scripts/release/build-publishable-packages.sh .release-manifest.json + - name: Publish snapshot prereleases + if: steps.status_check.outputs.has_packages == 'true' + run: pnpm exec changeset publish --tag rc --no-git-tag + env: + NPM_TOKEN: "" + - name: Summarize prerelease publish + if: steps.status_check.outputs.has_packages == 'true' + id: summary + run: node scripts/release/summarize-release.mjs --mode prerelease --manifest .release-manifest.json + - name: Post prerelease publish to Slack + if: steps.status_check.outputs.has_packages == 'true' uses: slackapi/slack-github-action@af78098f536edbc4de71162a307590698245be95 # v3.0.1 with: method: chat.postMessage token: ${{ secrets.SLACK_BOT_TOKEN }} payload: | channel: C0ABHT0SWA2 - text: "🧪 JavaScript SDK pre-release v${{ needs.prepare-release.outputs.version }} published" + text: "🧪 JavaScript prerelease snapshots published" blocks: - type: "header" text: type: "plain_text" - text: "🧪 JavaScript SDK Pre-release Published" + text: "🧪 JavaScript prerelease snapshots published" - type: "section" text: type: "mrkdwn" - text: "*Version:* ${{ needs.prepare-release.outputs.version }}\n*Branch:* `${{ needs.prepare-release.outputs.branch }}`\n*npm tag:* `rc` (install with `npm install braintrust@rc`)\n*Package:* \n\n<${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}|View Run>" + text: "*Branch:* `${{ inputs.branch || github.ref_name }}`\n*Tag:* `rc`\n*Packages:*\n${{ steps.summary.outputs.markdown }}\n\n<${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}|View Run>" - - name: Post to Slack on success (canary) - if: needs.prepare-release.outputs.release_type == 'canary' + canary-snapshot: + if: >- + github.event_name == 'schedule' || + (github.event_name == 'workflow_dispatch' && inputs.release_mode == 'canary') + runs-on: ubuntu-latest + timeout-minutes: 30 + permissions: + actions: read + contents: read + id-token: write + steps: + - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1 + with: + fetch-depth: 0 + - uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v5.0.0 + - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 + with: + node-version-file: .tool-versions + cache: pnpm + registry-url: https://registry.npmjs.org + - name: Install dependencies + run: pnpm install --frozen-lockfile + - name: Validate publishable package metadata + run: node scripts/release/validate-publishable-packages.mjs + - name: Compute changeset status + run: pnpm exec changeset status --output=.changeset-status.json + - name: Check canary status + id: status_check + run: node scripts/release/check-changeset-status.mjs --mode canary --status-file .changeset-status.json + - name: Check if canary already published for HEAD + if: steps.status_check.outputs.has_packages == 'true' + id: canary_check + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: node scripts/release/should-publish-canary.mjs --status-file .changeset-status.json + - name: Version canary snapshot + if: steps.status_check.outputs.has_packages == 'true' && steps.canary_check.outputs.should_publish == 'true' + run: pnpm exec changeset version --snapshot canary + - name: Prepare canary manifest + if: steps.status_check.outputs.has_packages == 'true' && steps.canary_check.outputs.should_publish == 'true' + id: manifest + run: node scripts/release/prepare-release-manifest.mjs --mode canary --status-file .changeset-status.json --output .release-manifest.json + - name: Build publishable packages + if: steps.status_check.outputs.has_packages == 'true' && steps.canary_check.outputs.should_publish == 'true' + run: bash scripts/release/build-publishable-packages.sh .release-manifest.json + - name: Publish canary snapshots + if: steps.status_check.outputs.has_packages == 'true' && steps.canary_check.outputs.should_publish == 'true' + run: pnpm exec changeset publish --tag canary --no-git-tag + env: + NPM_TOKEN: "" + - name: Summarize canary publish + if: steps.status_check.outputs.has_packages == 'true' && steps.canary_check.outputs.should_publish == 'true' + id: summary + run: node scripts/release/summarize-release.mjs --mode canary --manifest .release-manifest.json + - name: Post canary publish to Slack + if: steps.status_check.outputs.has_packages == 'true' && steps.canary_check.outputs.should_publish == 'true' uses: slackapi/slack-github-action@af78098f536edbc4de71162a307590698245be95 # v3.0.1 with: method: chat.postMessage token: ${{ secrets.SLACK_BOT_TOKEN }} payload: | channel: C0ABHT0SWA2 - text: "🧪 JavaScript SDK canary ${{ needs.publish-canary.outputs.version }} published: https://www.npmjs.com/package/braintrust/v/${{ needs.publish-canary.outputs.version }}" + text: "🐤 JavaScript canary snapshots published" blocks: - type: "header" text: type: "plain_text" - text: "🧪 JavaScript SDK Canary Published" + text: "🐤 JavaScript canary snapshots published" - type: "section" text: type: "mrkdwn" - text: "*Version:* ${{ needs.publish-canary.outputs.version }}\n*Branch:* `${{ needs.prepare-release.outputs.branch }}`\n*Commit:* `${{ needs.publish-canary.outputs.commit_sha }}`\n*Package:* \n*Install:* `npm install ${{ needs.publish-canary.outputs.package_name }}@canary`\n\n<${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}|View Run>" + text: "*Tag:* `canary`\n*Commit:* `${{ github.sha }}`\n*Packages:*\n${{ steps.summary.outputs.markdown }}\n\n<${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}|View Run>" + + dry-run-canary: + if: github.event_name == 'workflow_dispatch' && inputs.release_mode == 'dry-run-canary' + runs-on: ubuntu-latest + timeout-minutes: 30 + permissions: + contents: read + steps: + - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1 + with: + fetch-depth: 0 + ref: ${{ inputs.ref || 'main' }} + - uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v5.0.0 + - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 + with: + node-version-file: .tool-versions + cache: pnpm + - name: Install dependencies + run: pnpm install --frozen-lockfile + - name: Validate publishable package metadata + run: node scripts/release/validate-publishable-packages.mjs + - name: Compute changeset status + run: pnpm exec changeset status --output=.changeset-status.json + - name: Check canary dry-run status + id: status_check + run: node scripts/release/check-changeset-status.mjs --mode dry-run-canary --status-file .changeset-status.json + - name: Version canary dry-run packages + if: steps.status_check.outputs.has_packages == 'true' + run: pnpm exec changeset version --snapshot canary + - name: Prepare canary dry-run manifest + if: steps.status_check.outputs.has_packages == 'true' + run: node scripts/release/prepare-release-manifest.mjs --mode dry-run-canary --status-file .changeset-status.json --output .release-manifest.json + - name: Build publishable packages + if: steps.status_check.outputs.has_packages == 'true' + run: bash scripts/release/build-publishable-packages.sh .release-manifest.json + - name: Pack canary dry-run packages + if: steps.status_check.outputs.has_packages == 'true' + run: node scripts/release/pack-publishable-packages.mjs --manifest .release-manifest.json --output-dir artifacts/dry-run-canary + - name: Summarize canary dry run + if: steps.status_check.outputs.has_packages == 'true' + run: node scripts/release/summarize-release.mjs --mode dry-run-canary --manifest .release-manifest.json + - name: Upload canary dry-run artifacts + if: steps.status_check.outputs.has_packages == 'true' + uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 + with: + name: canary-dry-run-${{ github.run_id }} + path: artifacts/dry-run-canary + retention-days: 5 + + dry-run-stable: + if: github.event_name == 'workflow_dispatch' && inputs.release_mode == 'dry-run-stable' + runs-on: ubuntu-latest + timeout-minutes: 30 + permissions: + contents: read + steps: + - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1 + with: + fetch-depth: 0 + ref: ${{ inputs.ref || 'main' }} + - uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v5.0.0 + - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 + with: + node-version-file: .tool-versions + cache: pnpm + - name: Install dependencies + run: pnpm install --frozen-lockfile + - name: Validate publishable package metadata + run: node scripts/release/validate-publishable-packages.mjs + - name: Compute changeset status + run: pnpm exec changeset status --output=.changeset-status.json + - name: Check stable dry-run status + id: status_check + run: node scripts/release/check-changeset-status.mjs --mode dry-run-stable --status-file .changeset-status.json + - name: Version stable dry-run packages + if: steps.status_check.outputs.has_packages == 'true' + run: pnpm exec changeset version && pnpm install --lockfile-only + - name: Prepare stable dry-run manifest + if: steps.status_check.outputs.has_packages == 'true' + run: node scripts/release/prepare-release-manifest.mjs --mode dry-run-stable --status-file .changeset-status.json --output .release-manifest.json + - name: Build publishable packages + if: steps.status_check.outputs.has_packages == 'true' + run: bash scripts/release/build-publishable-packages.sh .release-manifest.json + - name: Pack stable dry-run packages + if: steps.status_check.outputs.has_packages == 'true' + run: node scripts/release/pack-publishable-packages.mjs --manifest .release-manifest.json --output-dir artifacts/dry-run-stable + - name: Summarize stable dry run + if: steps.status_check.outputs.has_packages == 'true' + run: node scripts/release/summarize-release.mjs --mode dry-run-stable --manifest .release-manifest.json + - name: Upload stable dry-run artifacts + if: steps.status_check.outputs.has_packages == 'true' + uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 + with: + name: stable-dry-run-${{ github.run_id }} + path: artifacts/dry-run-stable + retention-days: 5 + + dry-run-prerelease: + if: github.event_name == 'workflow_dispatch' && inputs.release_mode == 'dry-run-prerelease' + runs-on: ubuntu-latest + timeout-minutes: 30 + permissions: + contents: read + steps: + - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1 + with: + fetch-depth: 0 + ref: ${{ inputs.branch || github.ref_name }} + - uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v5.0.0 + - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 + with: + node-version-file: .tool-versions + cache: pnpm + - name: Install dependencies + run: pnpm install --frozen-lockfile + - name: Validate publishable package metadata + run: node scripts/release/validate-publishable-packages.mjs + - name: Compute changeset status + run: pnpm exec changeset status --output=.changeset-status.json + - name: Check prerelease dry-run status + id: status_check + run: node scripts/release/check-changeset-status.mjs --mode dry-run-prerelease --status-file .changeset-status.json + - name: Version prerelease dry-run packages + if: steps.status_check.outputs.has_packages == 'true' + run: pnpm exec changeset version --snapshot rc + - name: Prepare prerelease dry-run manifest + if: steps.status_check.outputs.has_packages == 'true' + run: node scripts/release/prepare-release-manifest.mjs --mode dry-run-prerelease --status-file .changeset-status.json --output .release-manifest.json + - name: Build publishable packages + if: steps.status_check.outputs.has_packages == 'true' + run: bash scripts/release/build-publishable-packages.sh .release-manifest.json + - name: Pack prerelease dry-run packages + if: steps.status_check.outputs.has_packages == 'true' + run: node scripts/release/pack-publishable-packages.mjs --manifest .release-manifest.json --output-dir artifacts/dry-run-prerelease + - name: Summarize prerelease dry run + if: steps.status_check.outputs.has_packages == 'true' + run: node scripts/release/summarize-release.mjs --mode dry-run-prerelease --manifest .release-manifest.json + - name: Upload prerelease dry-run artifacts + if: steps.status_check.outputs.has_packages == 'true' + uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 + with: + name: prerelease-dry-run-${{ github.run_id }} + path: artifacts/dry-run-prerelease + retention-days: 5 notify-failure: needs: [ - prepare-release, - build-and-publish-stable, - build-and-publish-prerelease, - publish-canary, + stable-release-pr, + stable-detect-publish, + stable-publish, + prerelease-snapshot, + canary-snapshot, + dry-run-stable, + dry-run-prerelease, + dry-run-canary, ] if: | always() && ( - needs.prepare-release.result == 'failure' || - needs.build-and-publish-stable.result == 'failure' || - needs.build-and-publish-prerelease.result == 'failure' || - needs.publish-canary.result == 'failure' + needs.stable-release-pr.result == 'failure' || + needs.stable-detect-publish.result == 'failure' || + needs.stable-publish.result == 'failure' || + needs.prerelease-snapshot.result == 'failure' || + needs.canary-snapshot.result == 'failure' || + needs.dry-run-stable.result == 'failure' || + needs.dry-run-prerelease.result == 'failure' || + needs.dry-run-canary.result == 'failure' ) runs-on: ubuntu-latest timeout-minutes: 5 @@ -540,41 +500,13 @@ jobs: token: ${{ secrets.SLACK_BOT_TOKEN }} payload: | channel: C0ABHT0SWA2 - text: "🚨 JavaScript SDK release failed" - blocks: - - type: "header" - text: - type: "plain_text" - text: "🚨 JavaScript SDK Release Failed" - - type: "section" - text: - type: "mrkdwn" - text: "*Release type:* ${{ needs.prepare-release.outputs.release_type || 'canary' }}\n*Branch:* `${{ needs.prepare-release.outputs.branch || 'main' }}`\n*Commit:* ${{ needs.prepare-release.outputs.commit || github.sha }}\n*Triggered by:* ${{ github.event_name }}\n\n<${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}|View Run>" - - notify-skipped: - needs: [prepare-release, publish-canary] - if: | - always() && - needs.prepare-release.outputs.release_type == 'canary' && - needs.publish-canary.result == 'success' && - needs.publish-canary.outputs.published != 'true' - runs-on: ubuntu-latest - timeout-minutes: 5 - steps: - - name: Post to Slack on intentional skip - uses: slackapi/slack-github-action@af78098f536edbc4de71162a307590698245be95 # v3.0.1 - with: - method: chat.postMessage - token: ${{ secrets.SLACK_BOT_TOKEN }} - payload: | - channel: C0ABHT0SWA2 - text: "⏭️ JavaScript SDK canary publish skipped: ${{ needs.publish-canary.outputs.reason }}" + text: "🚨 JavaScript release workflow failed" blocks: - type: "header" text: type: "plain_text" - text: "⏭️ JavaScript SDK Canary Publish Skipped" + text: "🚨 JavaScript release workflow failed" - type: "section" text: type: "mrkdwn" - text: "*Branch:* `${{ needs.prepare-release.outputs.branch }}`\n*Commit:* `${{ needs.prepare-release.outputs.commit }}`\n*Reason:* ${{ needs.publish-canary.outputs.reason }}\n\n<${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}|View Run>" + text: "*Event:* `${{ github.event_name }}`\n*Ref:* `${{ github.ref_name }}`\n*Mode:* `${{ inputs.release_mode || 'stable-main' }}`\n\n<${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}|View Run>" diff --git a/PUBLISHING_JS.md b/PUBLISHING_JS.md index 3c6358dec..01602aac9 100644 --- a/PUBLISHING_JS.md +++ b/PUBLISHING_JS.md @@ -1,107 +1,218 @@ -# JS SDK Release +# Publishing JavaScript SDK Packages -## Recommended Path +> **TL;DR** — Add a changeset to your PR. For stable releases, merge the auto-created release PR. For prereleases, trigger the workflow manually. Everything publishes via GitHub Actions — never from a laptop. -Use the [Publish JavaScript SDK workflow](https://github.com/braintrustdata/braintrust-sdk-javascript/actions/workflows/publish-js-sdk.yaml) in GitHub Actions. +--- -This is the single npm publish entrypoint for stable releases, prereleases, and canaries. +## Quick Reference -We keep all npm publishes in one workflow file because npm trusted publishing only allows one configured GitHub Actions publisher per package. +| I want to… | Do this | +| ---------------------------------- | --------------------------------------------------------------------------------- | +| Flag my PR for release | Run `pnpm changeset` and commit the generated file | +| Ship a stable release | Merge the release PR on `main` → approve the `npm-publish` environment | +| Publish a prerelease from a branch | Trigger workflow manually with `release_mode=prerelease` | +| Get the latest canary build | `npm install braintrust@canary` (published nightly from `main`) | +| Trigger a canary manually | Trigger workflow manually with `release_mode=canary` | +| Preview what would publish | Trigger workflow with `dry-run-stable`, `dry-run-prerelease`, or `dry-run-canary` | -Stable releases now pause at the `npm-publish` GitHub Actions environment and require approval before the publish job runs. +--- -## Release Types +## Packages -### Stable release +This process covers all public JS SDK packages: -- Publishes the exact version already set in `js/package.json` -- Publishes to npm as the normal latest release -- Creates the `js-sdk-v` git tag from the workflow -- Fails before publishing if that tag already exists on GitHub -- Waits for approval on the `npm-publish` environment before publishing +`braintrust` · `@braintrust/browser` · `@braintrust/langchain-js` · `@braintrust/openai-agents` · `@braintrust/otel` · `@braintrust/templates-nunjucks-js` · `@braintrust/temporal` · `@braintrust/vercel-ai-sdk` -### Prerelease +--- -- Uses the current `js/package.json` version as the base version -- Publishes `-rc.` -- Publishes to the `rc` npm dist-tag -- Does not update `js/package.json` in the repository +## Step 0: Add a Changeset to Your PR -### Canary +**CI enforces this.** If your PR touches files in a publishable package, the `changeset-required` check will fail unless you include a changeset. -- Can be triggered manually by running the same workflow with `release_type=canary` -- Publishes `-canary...g` -- Publishes to the `canary` npm dist-tag -- Does not create a GitHub release -- Skips publishing if the current `HEAD` commit already matches the existing `canary` tag on npm -- Skips publishing unless the latest completed `js.yaml` run on the target branch succeeded +```bash +pnpm changeset +``` -## Stable Release Checklist +You'll be prompted to pick: -1. Bump `js/package.json` according to [SEMVER](https://semver.org/) principles. -2. Make sure tests and integration tests pass on the PR. -3. Merge the change to `main` in `braintrust-sdk-javascript` and `braintrust`. -4. Open the [Publish JavaScript SDK workflow](https://github.com/braintrustdata/braintrust-sdk-javascript/actions/workflows/publish-js-sdk.yaml). -5. Click `Run workflow`. -6. Set `release_type=stable`. -7. Set `branch=main`. -8. Run the workflow. -9. Approve the pending `npm-publish` environment deployment when prompted. -10. Monitor the run at https://github.com/braintrustdata/braintrust-sdk-javascript/actions/workflows/publish-js-sdk.yaml. -11. Spot check the package at https://www.npmjs.com/package/braintrust. -12. Update relevant docs ([internal](https://www.notion.so/braintrustdata/SDK-Release-Process-183f7858028980b8a57ac4a81d74f97c#2f1f78580289807ebf35d5e171832d2a)). -13. Run the test app at https://github.com/braintrustdata/sdk-test-apps (internal) with `make verify-js`. +- **Which packages** are affected +- **Bump type** — major / minor / patch +- **Changelog entry** — a short description of the change -## Prerelease Steps +Commit the generated `.changeset/*.md` file with your PR. -1. Open the [Publish JavaScript SDK workflow](https://github.com/braintrustdata/braintrust-sdk-javascript/actions/workflows/publish-js-sdk.yaml). -2. Click `Run workflow`. -3. Set `release_type=prerelease`. -4. Set `branch` to the branch you want to prerelease from. -5. Run the workflow. +### Choosing the right bump type -If you prerelease from a non-`main` branch, make sure that branch is in the state you intend to publish. +When `pnpm changeset` asks for a bump type, use these guidelines: -## Nightly Canary +| Bump type | When to use | Examples | +| --------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------- | +| **patch** | Bug fixes, internal refactors, performance improvements, docs fixes, or dependency updates that don't change the public API. | Fix a crash in `span.end()`, update an internal retry policy, bump a transitive dep | +| **minor** | New features, new exports, new optional parameters, or new integrations — anything that **adds** functionality without breaking existing usage. | Add a new `flush()` method, expose a new `@braintrust/foo` integration, add an optional `timeout` param to an existing function | +| **major** | Breaking changes — removing or renaming public API surface, changing default behavior, dropping support for a Node version, or anything that could require users to update their code when upgrading. | Rename `logger.log()` → `logger.trace()`, remove a deprecated export, change a function's return type | -Nightly canary scheduling now lives in the separate [Schedule JavaScript SDK Canary Publish workflow](https://github.com/braintrustdata/braintrust-sdk-javascript/actions/workflows/publish-js-sdk-canary-scheduler.yaml). +> **When in doubt, pick `patch`.** It's easy to upgrade a changeset's bump type during review, and most changes are patches. Over-bumping causes unnecessary major versions that make life harder for consumers. -- The scheduler only dispatches [Publish JavaScript SDK](https://github.com/braintrustdata/braintrust-sdk-javascript/actions/workflows/publish-js-sdk.yaml) with `release_type=canary` and `branch=main`. -- The actual npm publish still runs in `publish-js-sdk.yaml`, so npm trusted publishing only needs that one workflow configured as the publisher. -- Manual canary runs still use the publish workflow dispatch form directly. -- Install with `npm install braintrust@canary`. +### What the generated file looks like -The workflow writes a short run summary with the published version and recent commits touching `js/` so there is at least a lightweight change summary even though there is no formal changelog. +Running `pnpm changeset` creates a Markdown file in `.changeset/` (e.g. `.changeset/funny-dogs-dance.md`): -## Fallback CLI Trigger +```markdown +--- +"braintrust": patch +--- -If you do not want to open GitHub Actions manually, you can dispatch the same workflow from the terminal: +Fix span export when using custom headers +``` -```bash -./js/scripts/dispatch-release-workflow.sh +The frontmatter lists each affected package and its bump type. The body becomes the changelog entry. You can edit this file by hand after generation — for example, to adjust the bump type, tweak the description, or add multiple packages. + +### Tips + +- **One changeset per logical change.** If your PR does two unrelated things (e.g. fixes a bug _and_ adds a feature), create two changesets so each gets its own changelog entry. +- **Multiple packages in one changeset is fine.** If a single change affects both `braintrust` and `@braintrust/otel`, pick both when prompted — they'll share one changeset file. +- **You can run `pnpm changeset` multiple times** to add more changeset files to the same PR. + +### When you DON'T need a changeset + +If your PR doesn't touch publishable package code (e.g. docs-only, test-only, CI changes), the check passes automatically. + +If your PR touches a publishable package but **shouldn't trigger a release**, bypass the check with any of: + +- Add the **`skip-changeset`** label to the PR +- Include `#skip-changeset` in the PR title or body + +--- + +## Stable Release (publish to `latest` on npm) + +### How it works + +``` +PR with changeset → merges to main → bot opens release PR → you merge release PR → publish ``` -To target a different remote branch: +### Step-by-step + +1. **Merge your PR** (with changeset) into `main`. +2. **GitHub Actions automatically creates/updates a release PR** that contains version bumps, changelog updates, and lockfile changes. +3. **Review the release PR.** Check: + - Are the version bumps correct (major/minor/patch)? + - Do the changelog entries look right? +4. **Merge the release PR.** +5. **Approve the publish.** The workflow pauses at the `npm-publish` environment gate — click "Approve" in the Actions UI. +6. **Done.** Packages are published to npm, git tags are created (e.g. `braintrust@3.8.0`), and GitHub Releases are generated. + +### No release PR showing up? + +- No unreleased changesets exist on `main`. +- The workflow hasn't run yet (check Actions). +- The previous release PR was already merged and there's nothing new. + +--- + +## Canary (publish to `canary` on npm) + +Canary builds are **automated nightly snapshots** from `main`. They let users test the latest merged changes without waiting for a formal release. + +### How it works -```bash -BRANCH= ./js/scripts/dispatch-release-workflow.sh ``` +Nightly cron (04:00 UTC) → check for pending changesets → check if HEAD already has canary → publish +``` + +- Runs automatically every night at 04:00 UTC. +- **Skips** if there are no pending changesets on `main`. +- **Skips** if the current `HEAD` commit already has a canary published (idempotent). +- Can also be triggered manually via workflow dispatch with `release_mode=canary`. -To dispatch a prerelease or canary instead of a stable release: +### Installing a canary build ```bash -RELEASE_TYPE=prerelease ./js/scripts/dispatch-release-workflow.sh -RELEASE_TYPE=canary ./js/scripts/dispatch-release-workflow.sh +npm install braintrust@canary +npm install @braintrust/otel@canary ``` -Notes: +### Canary version format + +Canary versions include the snapshot tag, a datetime stamp, and the short commit hash: + +``` +1.2.3-canary.20260404040000.abc1234 +``` + +This makes versions monotonically increasing and traceable to a specific commit. + +### What canaries do NOT do + +- ❌ Create git tags +- ❌ Create GitHub Releases +- ❌ Commit version changes back to the repo +- ❌ Require manual approval — canaries publish automatically +- ❌ Affect the release PR or stable release flow + +--- + +## Prerelease (publish to `rc` on npm) + +Use this to publish a testable build from **any branch** — no need to merge to `main` first. + +### Step-by-step + +1. Go to **Actions → "Release Packages"** workflow in GitHub. +2. Click **"Run workflow"** and set: + - **`release_mode`** → `prerelease` + - **`branch`** → the branch you want to publish (e.g. `my-feature`) +3. Click **"Run workflow"**. +4. Once complete, install with the `rc` tag: + ```bash + npm install braintrust@rc + ``` + +### What prereleases do NOT do + +- ❌ Create git tags +- ❌ Create GitHub Releases +- ❌ Commit version changes back to the repo +- ❌ Publish the entire workspace — only packages with releasable changesets on the branch + +--- + +## Dry Run (preview without publishing) + +Trigger the workflow manually with one of: + +| Mode | What it simulates | +| -------------------- | ----------------- | +| `dry-run-stable` | A stable release | +| `dry-run-prerelease` | A prerelease | +| `dry-run-canary` | A canary release | + +You'll see which packages would release, the computed versions, and packaged tarballs — but **nothing is published, tagged, or committed**. + +--- + +## FAQ + +**Do I need to bump versions manually?** +No. Changesets handles it via the release PR. + +**Do conventional commits drive releases?** +No. Only changeset files drive releases. + +**Can I publish a prerelease from a feature branch?** +Yes. That's exactly what prereleases are for. + +**Is canary publishing supported?** +Yes. Canaries are published nightly from `main` to the `canary` npm dist-tag. You can also trigger one manually via workflow dispatch. + +**What's the difference between canary and prerelease?** +Canaries are **automated nightly** from `main` (tagged `canary`). Prereleases are **manual** from any branch (tagged `rc`). -- This is a fallback, not the recommended path. -- It requires `gh` to be installed and authenticated. -- It does not publish from your local checkout. -- It dispatches the same GitHub Actions workflow against the selected branch on GitHub. -- `RELEASE_TYPE` defaults to `stable`. +--- -## Repository Setup +## Reference -Configure the `npm-publish` environment in GitHub repository settings with the required reviewers who are allowed to approve stable releases. +- **Workflow file:** `.github/workflows/publish-js-sdk.yaml` +- **Tag format:** Standard Changesets monorepo tags (e.g. `braintrust@3.8.0`, `@braintrust/otel@0.3.0`). Legacy `js-sdk-v` tags are no longer created. +- **Trusted publishing:** If npm trusted publishing is configured for additional packages, point npm at the workflow file above. diff --git a/integrations/browser-js/CHANGELOG.md b/integrations/browser-js/CHANGELOG.md new file mode 100644 index 000000000..8d03c4bf5 --- /dev/null +++ b/integrations/browser-js/CHANGELOG.md @@ -0,0 +1,9 @@ +# @braintrust/browser + +## 0.0.2-rc.4 + +### Patch Changes + +- Initial prerelease of the dedicated browser package for Braintrust. +- Added browser-specific configuration to avoid pulling in server-only templating behavior. +- Bundled `als-browser` support to provide AsyncLocalStorage-compatible behavior in browser environments. diff --git a/integrations/browser-js/package.json b/integrations/browser-js/package.json index 4d6981643..511171084 100644 --- a/integrations/browser-js/package.json +++ b/integrations/browser-js/package.json @@ -44,7 +44,7 @@ "repository": { "type": "git", "url": "git+https://github.com/braintrustdata/braintrust-sdk-javascript.git", - "directory": "sdk/integrations/browser-js" + "directory": "integrations/browser-js" }, "homepage": "https://www.braintrust.dev/docs", "license": "Apache-2.0", diff --git a/integrations/langchain-js/CHANGELOG.md b/integrations/langchain-js/CHANGELOG.md new file mode 100644 index 000000000..f2be6709b --- /dev/null +++ b/integrations/langchain-js/CHANGELOG.md @@ -0,0 +1,8 @@ +# @braintrust/langchain-js + +## 0.2.3 + +### Patch Changes + +- Added prompt caching token tracking for LangChain usage metadata. +- Mapped nested `input_token_details` metrics into Braintrust's standard cached-token fields, including cache reads and cache creation tokens. diff --git a/integrations/langchain-js/package.json b/integrations/langchain-js/package.json index 95f7bbd6a..ed3e3c3d4 100644 --- a/integrations/langchain-js/package.json +++ b/integrations/langchain-js/package.json @@ -49,5 +49,15 @@ "@langchain/core": { "optional": false } + }, + "repository": { + "type": "git", + "url": "git+https://github.com/braintrustdata/braintrust-sdk-javascript.git", + "directory": "integrations/langchain-js" + }, + "homepage": "https://www.braintrust.dev/docs", + "publishConfig": { + "access": "public", + "registry": "https://registry.npmjs.org/" } } diff --git a/integrations/openai-agents-js/CHANGELOG.md b/integrations/openai-agents-js/CHANGELOG.md new file mode 100644 index 000000000..4fdd749f6 --- /dev/null +++ b/integrations/openai-agents-js/CHANGELOG.md @@ -0,0 +1,8 @@ +# @braintrust/openai-agents + +## 0.1.5 + +### Patch Changes + +- Ensured the root span is flushed during `onTraceEnd()` so traces are marked complete even in short-lived and serverless processes. +- Added test coverage around trace completion and root span flushing behavior. diff --git a/integrations/openai-agents-js/package.json b/integrations/openai-agents-js/package.json index b85716a2b..435852570 100644 --- a/integrations/openai-agents-js/package.json +++ b/integrations/openai-agents-js/package.json @@ -36,5 +36,15 @@ }, "peerDependencies": { "braintrust": ">=0.4.0" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/braintrustdata/braintrust-sdk-javascript.git", + "directory": "integrations/openai-agents-js" + }, + "homepage": "https://www.braintrust.dev/docs", + "publishConfig": { + "access": "public", + "registry": "https://registry.npmjs.org/" } } diff --git a/integrations/otel-js/CHANGELOG.md b/integrations/otel-js/CHANGELOG.md new file mode 100644 index 000000000..7e6dbc11a --- /dev/null +++ b/integrations/otel-js/CHANGELOG.md @@ -0,0 +1,8 @@ +# @braintrust/otel + +## 0.2.0 + +### Minor Changes + +- Updated `AISpanProcessor` filtering so root spans are no longer retained by default. +- Added exported helpers for span filtering, including `isRootSpan`, and made custom filtering behavior easier to control. diff --git a/integrations/otel-js/package.json b/integrations/otel-js/package.json index 427d22b4d..8810d24b5 100644 --- a/integrations/otel-js/package.json +++ b/integrations/otel-js/package.json @@ -55,5 +55,15 @@ "@opentelemetry/core": { "optional": false } + }, + "repository": { + "type": "git", + "url": "git+https://github.com/braintrustdata/braintrust-sdk-javascript.git", + "directory": "integrations/otel-js" + }, + "homepage": "https://www.braintrust.dev/docs", + "publishConfig": { + "access": "public", + "registry": "https://registry.npmjs.org/" } } diff --git a/integrations/templates-nunjucks/CHANGELOG.md b/integrations/templates-nunjucks/CHANGELOG.md new file mode 100644 index 000000000..b5c38bb9c --- /dev/null +++ b/integrations/templates-nunjucks/CHANGELOG.md @@ -0,0 +1,8 @@ +# @braintrust/templates-nunjucks-js + +## 0.0.1 + +### Patch Changes + +- Initial release of the standalone Nunjucks templating package for Braintrust. +- Split Nunjucks support out of the main SDK and added dedicated test and smoke-test coverage for common runtimes. diff --git a/integrations/templates-nunjucks/package.json b/integrations/templates-nunjucks/package.json index 9958847af..36974f54e 100644 --- a/integrations/templates-nunjucks/package.json +++ b/integrations/templates-nunjucks/package.json @@ -39,5 +39,15 @@ "tsup": "^8.5.1", "typescript": "5.5.4", "vitest": "^4.1.2" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/braintrustdata/braintrust-sdk-javascript.git", + "directory": "integrations/templates-nunjucks" + }, + "homepage": "https://www.braintrust.dev/docs", + "publishConfig": { + "access": "public", + "registry": "https://registry.npmjs.org/" } } diff --git a/integrations/temporal-js/CHANGELOG.md b/integrations/temporal-js/CHANGELOG.md new file mode 100644 index 000000000..cb69d9890 --- /dev/null +++ b/integrations/temporal-js/CHANGELOG.md @@ -0,0 +1,9 @@ +# @braintrust/temporal + +## 0.1.1 + +### Patch Changes + +- Improved Temporal integration support for ESM projects. +- Fixed merging behavior for existing `WorkflowClientInterceptors` so Braintrust interceptors compose correctly with older client configurations. +- Added expanded example coverage, including ESM and AI SDK plugin usage examples. diff --git a/integrations/temporal-js/package.json b/integrations/temporal-js/package.json index df0c6acb8..f74536d82 100644 --- a/integrations/temporal-js/package.json +++ b/integrations/temporal-js/package.json @@ -68,5 +68,15 @@ "@temporalio/workflow": { "optional": false } + }, + "repository": { + "type": "git", + "url": "git+https://github.com/braintrustdata/braintrust-sdk-javascript.git", + "directory": "integrations/temporal-js" + }, + "homepage": "https://www.braintrust.dev/docs", + "publishConfig": { + "access": "public", + "registry": "https://registry.npmjs.org/" } } diff --git a/integrations/vercel-ai-sdk/CHANGELOG.md b/integrations/vercel-ai-sdk/CHANGELOG.md new file mode 100644 index 000000000..dd1634a40 --- /dev/null +++ b/integrations/vercel-ai-sdk/CHANGELOG.md @@ -0,0 +1,9 @@ +# @braintrust/vercel-ai-sdk + +## 0.0.5 + +### Patch Changes + +- Added `toDataStreamResponse()` as the preferred response helper for Braintrust streams. +- Kept `toAIStreamResponse()` as a deprecated alias for compatibility. +- Updated the adapter for compatibility with newer Zod versions. diff --git a/integrations/vercel-ai-sdk/package.json b/integrations/vercel-ai-sdk/package.json index 52c0ab310..24e719e46 100644 --- a/integrations/vercel-ai-sdk/package.json +++ b/integrations/vercel-ai-sdk/package.json @@ -36,5 +36,15 @@ }, "peerDependencies": { "braintrust": ">=0.0.141" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/braintrustdata/braintrust-sdk-javascript.git", + "directory": "integrations/vercel-ai-sdk" + }, + "homepage": "https://www.braintrust.dev/docs", + "publishConfig": { + "access": "public", + "registry": "https://registry.npmjs.org/" } } diff --git a/js/CHANGELOG.md b/js/CHANGELOG.md index bd0b3941d..9ae5e8eb9 100644 --- a/js/CHANGELOG.md +++ b/js/CHANGELOG.md @@ -1,3 +1,11 @@ -# Changelog +# braintrust -Release notes can be found [here](https://www.braintrust.dev/docs/reference/release-notes). +## 3.7.1 + +### Patch Changes + +- Preserved all streaming content block types. +- Fixed `wrapOpenAI` so it no longer breaks native private fields on the wrapped client. +- Propagated `templateFormat` in `ScorerBuilder.create()`. +- Rehydrated remote prompt parameters correctly. +- Switched the AI SDK, OpenRouter, Anthropic, Claude Agent SDK, and Google Gen AI wrappers to diagnostics channels. diff --git a/js/package.json b/js/package.json index 0f4d8ae90..5abdbd362 100644 --- a/js/package.json +++ b/js/package.json @@ -5,7 +5,7 @@ "repository": { "type": "git", "url": "git+https://github.com/braintrustdata/braintrust-sdk-javascript.git", - "directory": "blob/main/js" + "directory": "js" }, "homepage": "https://www.braintrust.dev/docs", "main": "./dist/index.js", @@ -143,7 +143,6 @@ "test:vitest": "pnpm --filter @braintrust/vitest-wrapper-tests test", "test:output": "tsx scripts/test-output.ts --with-comparison --with-metrics --with-progress", "bench": "tsx src/queue.bench.ts", - "publish:validate": "./scripts/validate-release.sh && pnpm install --frozen-lockfile && pnpm run build && pnpm publish", "lint": "eslint .", "fix:lint": "eslint --fix .", "playground": "tsx playground.ts", @@ -229,5 +228,9 @@ }, "peerDependencies": { "zod": "^3.25.34 || ^4.0" + }, + "publishConfig": { + "access": "public", + "registry": "https://registry.npmjs.org/" } } diff --git a/js/scripts/check-remote-tag.sh b/js/scripts/check-remote-tag.sh deleted file mode 100755 index 38959094c..000000000 --- a/js/scripts/check-remote-tag.sh +++ /dev/null @@ -1,14 +0,0 @@ -#!/bin/bash -set -euo pipefail - -if [ $# -ne 1 ]; then - echo "Usage: $0 " - exit 1 -fi - -TAG="$1" - -if git ls-remote --tags --exit-code origin "refs/tags/${TAG}" >/dev/null 2>&1; then - echo "ERROR: Release tag ${TAG} already exists on origin" - exit 1 -fi diff --git a/js/scripts/dispatch-release-workflow.sh b/js/scripts/dispatch-release-workflow.sh deleted file mode 100755 index c814ef0b4..000000000 --- a/js/scripts/dispatch-release-workflow.sh +++ /dev/null @@ -1,42 +0,0 @@ -#!/bin/bash -set -euo pipefail - -ROOT_DIR=$(git rev-parse --show-toplevel) -cd "$ROOT_DIR" - -if ! command -v gh >/dev/null 2>&1; then - echo "ERROR: GitHub CLI (gh) is required for this fallback flow" - exit 1 -fi - -if ! gh auth status >/dev/null 2>&1; then - echo "ERROR: gh is not authenticated" - exit 1 -fi - -BRANCH="${BRANCH:-$(git rev-parse --abbrev-ref HEAD)}" -RELEASE_TYPE="${RELEASE_TYPE:-stable}" - -case "$RELEASE_TYPE" in - stable|prerelease|canary) - ;; - *) - echo "ERROR: RELEASE_TYPE must be one of: stable, prerelease, canary" - exit 1 - ;; -esac - -if [ "$BRANCH" = "HEAD" ]; then - echo "ERROR: Could not determine the current branch. Set BRANCH= and retry." - exit 1 -fi - -if ! git ls-remote --exit-code --heads origin "$BRANCH" >/dev/null 2>&1; then - echo "ERROR: Branch '$BRANCH' does not exist on origin" - exit 1 -fi - -echo "Dispatching publish-js-sdk workflow for branch '$BRANCH' with release_type='$RELEASE_TYPE'..." -gh workflow run publish-js-sdk.yaml --ref "$BRANCH" -f release_type="$RELEASE_TYPE" -f branch="$BRANCH" -echo "Workflow dispatched:" -echo "https://github.com/braintrustdata/braintrust-sdk-javascript/actions/workflows/publish-js-sdk.yaml" diff --git a/js/scripts/publish-prerelease.sh b/js/scripts/publish-prerelease.sh deleted file mode 100755 index 0e8aae421..000000000 --- a/js/scripts/publish-prerelease.sh +++ /dev/null @@ -1,135 +0,0 @@ -#!/bin/bash -set -euo pipefail - -# Script to publish a pre-release version to npm -# Can be used both locally and in CI/CD -# -# Usage: ./publish-prerelease.sh -# type: rc -# version: explicit version to publish, e.g., 1.2.3-rc.1 - -# Get directories -ROOT_DIR=$(git rev-parse --show-toplevel) -JS_DIR="$ROOT_DIR/js" - -# Parse arguments -if [ $# -lt 2 ]; then - echo "Usage: $0 " - echo "" - echo "Arguments:" - echo " type: rc" - echo " version: explicit version to publish" - echo "" - echo "Example:" - echo " $0 rc 1.2.3-rc.1" - exit 1 -fi - -PRERELEASE_TYPE="$1" -VERSION="$2" - -# Validate prerelease type -if [ "$PRERELEASE_TYPE" != "rc" ]; then - echo "ERROR: Invalid prerelease type: $PRERELEASE_TYPE" - echo "Must be: rc" - exit 1 -fi - -# Validate version format -if [ -z "$VERSION" ]; then - echo "ERROR: Version cannot be empty" - exit 1 -fi - -# Validate that version contains the correct prerelease suffix -if [[ ! "$VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+-rc(\.[0-9]+)?$ ]]; then - echo "ERROR: Version '$VERSION' must include the prerelease type 'rc'" - echo "Expected format: X.Y.Z-rc.N (e.g., 1.2.3-rc.1)" - exit 1 -fi - -DIST_TAG="rc" - -set_package_version() { - local version="$1" - VERSION_TO_SET="$version" node -e ' - const fs = require("fs"); - const pkg = JSON.parse(fs.readFileSync("package.json", "utf8")); - pkg.version = process.env.VERSION_TO_SET; - fs.writeFileSync("package.json", JSON.stringify(pkg, null, 2) + "\n"); - ' -} - -echo "================================================" -echo " Publishing Pre-release" -echo "================================================" -echo "Type: $PRERELEASE_TYPE" -echo "Version: $VERSION" -echo "Dist-tag: $DIST_TAG" -echo "" - -# Save current version for reference -cd "$JS_DIR" -CURRENT_VERSION=$(node -p "require('./package.json').version") -echo "Current version: $CURRENT_VERSION" - -# Set the explicit version (updates package.json temporarily) -echo "" -echo "Setting version to $VERSION..." -set_package_version "$VERSION" - -NEW_VERSION=$(node -p "require('./package.json').version") -echo "New version: $NEW_VERSION" -echo "" - -# Build the SDK -echo "Building SDK..." -pnpm install --frozen-lockfile -pnpm run build -echo "Build complete." -echo "" - -# Publish to npm with dist-tag -echo "Publishing to npm..." -echo "Command: npm publish --tag $DIST_TAG" -echo "" - -# In CI, just publish. Locally, ask for confirmation -if [ -n "${CI:-}" ] || [ -n "${GITHUB_ACTIONS:-}" ]; then - # Running in CI - publish without confirmation - npm publish --tag "$DIST_TAG" -else - # Running locally - ask for confirmation - read -p "Ready to publish version $NEW_VERSION to npm with tag @$DIST_TAG? (y/N) " -n 1 -r - echo - if [[ $REPLY =~ ^[Yy]$ ]]; then - npm publish --tag "$DIST_TAG" - else - echo "Publish cancelled." - echo "" - echo "Restoring package.json to original version..." - set_package_version "$CURRENT_VERSION" - exit 1 - fi -fi - -echo "" -echo "================================================" -echo " Published Successfully!" -echo "================================================" -echo "Version: $NEW_VERSION" -echo "Dist-tag: $DIST_TAG" -echo "" -echo "Users can install via:" -echo " npm install braintrust@$DIST_TAG" -echo "" -echo "View on npm:" -echo " https://www.npmjs.com/package/braintrust/v/$NEW_VERSION" -echo "" - -# Restore package.json if not in CI (local development) -if [ -z "${CI:-}" ] && [ -z "${GITHUB_ACTIONS:-}" ]; then - echo "Restoring package.json to original version..." - set_package_version "$CURRENT_VERSION" - echo "Done. Local package.json restored to $CURRENT_VERSION" -fi diff --git a/js/scripts/push-release-tag.sh b/js/scripts/push-release-tag.sh deleted file mode 100755 index 3c8d4a712..000000000 --- a/js/scripts/push-release-tag.sh +++ /dev/null @@ -1,102 +0,0 @@ -#!/bin/bash -set -euo pipefail - -ROOT_DIR=$(git rev-parse --show-toplevel) -JS_DIR="$ROOT_DIR/js" - -# Parse command line arguments -DRY_RUN=false - -while [[ $# -gt 0 ]]; do - case "$1" in - --dry-run) - DRY_RUN=true - shift - ;; - *) - echo "Unknown option: $1" - echo "Usage: $0 [--dry-run]" - exit 1 - ;; - esac -done - -# Fetch latest tags -git fetch --tags --prune - -REPO_URL="https://github.com/braintrustdata/braintrust-sdk-javascript" -TAG_PREFIX="js-sdk-v" -COMMIT=$(git rev-parse --short HEAD) - -# Extract version from package.json -VERSION=$(node -p "require('$JS_DIR/package.json').version") -TAG="${TAG_PREFIX}${VERSION}" - -# Validation before pushing -echo "Running pre-push validation..." - -# Check if tag already exists -if git rev-parse "$TAG" >/dev/null 2>&1; then - echo "Error: Tag $TAG already exists" - exit 1 -fi - -# Check working tree is clean -if [ -n "$(git status --porcelain)" ]; then - echo "Error: working tree is not clean" - exit 1 -fi - -# Ensure we're on main branch or commit is on main -CURRENT_BRANCH=$(git rev-parse --abbrev-ref HEAD) -if [ "$CURRENT_BRANCH" != "main" ]; then - git fetch origin main --depth=1000 - if ! git merge-base --is-ancestor "$(git rev-parse HEAD)" origin/main; then - echo "ERROR: Current commit is not on the main branch" - exit 1 - fi -fi - -# Run the existing validate-release.sh script -echo "Running validate-release.sh..." -cd "$JS_DIR" -./scripts/validate-release.sh -cd "$ROOT_DIR" - -# Find the most recent version tag -LAST_RELEASE=$(git tag -l "${TAG_PREFIX}*" --sort=-v:refname | head -n 1) - -echo "================================================" -echo " JavaScript SDK Release" -echo "================================================" -echo "version: ${TAG}" -echo "commit: ${COMMIT}" -echo "code: ${REPO_URL}/commit/${COMMIT}" -echo "changeset: ${REPO_URL}/compare/${LAST_RELEASE}...${COMMIT}" - -if [ "$DRY_RUN" = true ]; then - exit 0 -fi - -echo "" -echo "" -echo "Are you ready to release version ${VERSION}? Type 'YOLO' to continue:" -read -r CONFIRMATION - -if [ "$CONFIRMATION" != "YOLO" ]; then - echo "Release cancelled." - exit 1 -fi - -# Create and push the tag -echo "" -echo "Creating and pushing tag ${TAG}" -echo "" - -git tag "$TAG" "$COMMIT" -git push origin "$TAG" - -echo "" -echo "Tag ${TAG} has been created and pushed to origin. Check GitHub Actions for build progress:" -echo "https://github.com/braintrustdata/braintrust-sdk-javascript/actions/workflows/publish-js-sdk.yaml" -echo "" diff --git a/js/scripts/validate-release-tag.sh b/js/scripts/validate-release-tag.sh deleted file mode 100755 index dafcc13c0..000000000 --- a/js/scripts/validate-release-tag.sh +++ /dev/null @@ -1,63 +0,0 @@ -#!/bin/bash -# Script to validate release requirements for CI -# - Checks if the tag matches the version in package.json -# - Ensures we're releasing from the main branch - -set -e - -# Get the tag from the first command line argument -if [ $# -eq 0 ]; then - echo "ERROR: Release tag argument not provided" - echo "Usage: $0 " - exit 1 -fi - -ROOT_DIR=$(git rev-parse --show-toplevel) -JS_DIR="$ROOT_DIR/js" - -# Fetch the latest tags to ensure we're up to date -git fetch --tags --prune --force - -TAG=$1 - -# Check if tag starts with js-sdk-v -if [[ ! "$TAG" =~ ^js-sdk-v ]]; then - echo "ERROR: Tag must start with 'js-sdk-v'" - exit 1 -fi - -# Extract version without the 'js-sdk-v' prefix -VERSION=${TAG#js-sdk-v} - -# Get version from package.json -PACKAGE_VERSION=$(node -p "require('$JS_DIR/package.json').version") - -# Check if the tag version matches the package version -if [ "$VERSION" != "$PACKAGE_VERSION" ]; then - echo "ERROR: Tag version ($VERSION) does not match package.json version ($PACKAGE_VERSION)" - exit 1 -fi - -CURRENT_BRANCH=$(git rev-parse --abbrev-ref HEAD) -if [ "$CURRENT_BRANCH" != "main" ]; then - # If we're in detached HEAD state (which is likely in GitHub Actions with a tag), - # we need to check if the tag is on the main branch - if ! git rev-parse "$TAG" &>/dev/null; then - echo "ERROR: Tag $TAG does not exist in the repository" - exit 1 - fi - - TAG_COMMIT=$(git rev-parse "$TAG") - - # Ensure we have main branch history - git fetch origin main --depth=1000 - - # Check if tag is on main branch - if ! git merge-base --is-ancestor "$TAG_COMMIT" origin/main; then - echo "ERROR: Tag $TAG is not on the main branch" - exit 1 - fi -fi - -# All checks passed -exit 0 diff --git a/js/scripts/validate-release.sh b/js/scripts/validate-release.sh deleted file mode 100755 index c178337ea..000000000 --- a/js/scripts/validate-release.sh +++ /dev/null @@ -1,28 +0,0 @@ -#!/bin/bash - -# This script attempts to verify the state of the repo is a candidate for -# release and will fail if it is not. -set -e - -RELEASE_BRANCH="${RELEASE_BRANCH:-main}" - -# Ensure the current commit is on the configured release branch -CURRENT_BRANCH=$(git rev-parse --abbrev-ref HEAD) -if [ "$CURRENT_BRANCH" != "$RELEASE_BRANCH" ]; then - git fetch origin "$RELEASE_BRANCH" --depth=1000 - # assert this commit is on the release branch - if ! git merge-base --is-ancestor "$(git rev-parse HEAD)" "origin/$RELEASE_BRANCH"; then - echo "ERROR: Current commit is not on the $RELEASE_BRANCH branch" - exit 1 - fi -fi - - -# Assert we aren't releasing any uncommitted code -if [ -n "$(git status --porcelain)" ]; then - echo "Error: working tree is not clean" - exit 1 -fi - -# All checks passed -exit 0 diff --git a/package.json b/package.json index 3f3e16d46..56669839f 100644 --- a/package.json +++ b/package.json @@ -4,9 +4,6 @@ "repository": "https://github.com/braintrustdata/braintrust-sdk-javascript", "license": "MIT", "private": true, - "workspaces": [ - "js" - ], "scripts": { "preinstall": "node -e \"const userAgent = process.env.npm_config_user_agent || ''; if (process.env.INIT_CWD === process.cwd() && !userAgent.includes('pnpm/')) { console.error('Use pnpm in this repo.'); process.exit(1); }\"", "build": "turbo run build", @@ -21,6 +18,9 @@ "test:e2e:hermetic": "turbo run test:e2e:hermetic", "test:e2e:canary": "turbo run test:e2e:canary", "test:e2e:update": "turbo run test:e2e:update", + "changeset": "changeset", + "changeset:status": "changeset status", + "changeset:version": "changeset version", "playground": "turbo run playground --filter=\"braintrust\"", "playground:cli:push": "turbo run playground:cli:push --filter=\"braintrust\"", "playground:cli:eval": "turbo run playground:cli:eval --filter=\"braintrust\"", @@ -43,6 +43,8 @@ } }, "devDependencies": { + "@changesets/changelog-github": "^0.6.0", + "@changesets/cli": "^2.30.0", "eslint": "^9.39.2", "husky": "^9.1.7", "knip": "^5.85.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 93c8da126..2d1930a1a 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -8,6 +8,12 @@ importers: .: devDependencies: + '@changesets/changelog-github': + specifier: ^0.6.0 + version: 0.6.0 + '@changesets/cli': + specifier: ^2.30.0 + version: 2.30.0(@types/node@22.19.1) eslint: specifier: ^9.39.2 version: 9.39.2(jiti@2.6.1) @@ -1010,6 +1016,67 @@ packages: '@cfworker/json-schema@4.0.3': resolution: {integrity: sha512-ZykIcDTVv5UNmKWSTLAs3VukO6NDJkkSKxrgUTDPBkAlORVT3H9n5DbRjRl8xIotklscHdbLIa0b9+y3mQq73g==} + '@changesets/apply-release-plan@7.1.0': + resolution: {integrity: sha512-yq8ML3YS7koKQ/9bk1PqO0HMzApIFNwjlwCnwFEXMzNe8NpzeeYYKCmnhWJGkN8g7E51MnWaSbqRcTcdIxUgnQ==} + + '@changesets/assemble-release-plan@6.0.9': + resolution: {integrity: sha512-tPgeeqCHIwNo8sypKlS3gOPmsS3wP0zHt67JDuL20P4QcXiw/O4Hl7oXiuLnP9yg+rXLQ2sScdV1Kkzde61iSQ==} + + '@changesets/changelog-git@0.2.1': + resolution: {integrity: sha512-x/xEleCFLH28c3bQeQIyeZf8lFXyDFVn1SgcBiR2Tw/r4IAWlk1fzxCEZ6NxQAjF2Nwtczoen3OA2qR+UawQ8Q==} + + '@changesets/changelog-github@0.6.0': + resolution: {integrity: sha512-wA2/y4hR/A1K411cCT75rz0d46Iezxp1WYRFoFJDIUpkQ6oDBAIUiU7BZkDCmYgz0NBl94X1lgcZO+mHoiHnFg==} + + '@changesets/cli@2.30.0': + resolution: {integrity: sha512-5D3Nk2JPqMI1wK25pEymeWRSlSMdo5QOGlyfrKg0AOufrUcjEE3RQgaCpHoBiM31CSNrtSgdJ0U6zL1rLDDfBA==} + hasBin: true + + '@changesets/config@3.1.3': + resolution: {integrity: sha512-vnXjcey8YgBn2L1OPWd3ORs0bGC4LoYcK/ubpgvzNVr53JXV5GiTVj7fWdMRsoKUH7hhhMAQnsJUqLr21EncNw==} + + '@changesets/errors@0.2.0': + resolution: {integrity: sha512-6BLOQUscTpZeGljvyQXlWOItQyU71kCdGz7Pi8H8zdw6BI0g3m43iL4xKUVPWtG+qrrL9DTjpdn8eYuCQSRpow==} + + '@changesets/get-dependents-graph@2.1.3': + resolution: {integrity: sha512-gphr+v0mv2I3Oxt19VdWRRUxq3sseyUpX9DaHpTUmLj92Y10AGy+XOtV+kbM6L/fDcpx7/ISDFK6T8A/P3lOdQ==} + + '@changesets/get-github-info@0.8.0': + resolution: {integrity: sha512-cRnC+xdF0JIik7coko3iUP9qbnfi1iJQ3sAa6dE+Tx3+ET8bjFEm63PA4WEohgjYcmsOikPHWzPsMWWiZmntOQ==} + + '@changesets/get-release-plan@4.0.15': + resolution: {integrity: sha512-Q04ZaRPuEVZtA+auOYgFaVQQSA98dXiVe/yFaZfY7hoSmQICHGvP0TF4u3EDNHWmmCS4ekA/XSpKlSM2PyTS2g==} + + '@changesets/get-version-range-type@0.4.0': + resolution: {integrity: sha512-hwawtob9DryoGTpixy1D3ZXbGgJu1Rhr+ySH2PvTLHvkZuQ7sRT4oQwMh0hbqZH1weAooedEjRsbrWcGLCeyVQ==} + + '@changesets/git@3.0.4': + resolution: {integrity: sha512-BXANzRFkX+XcC1q/d27NKvlJ1yf7PSAgi8JG6dt8EfbHFHi4neau7mufcSca5zRhwOL8j9s6EqsxmT+s+/E6Sw==} + + '@changesets/logger@0.1.1': + resolution: {integrity: sha512-OQtR36ZlnuTxKqoW4Sv6x5YIhOmClRd5pWsjZsddYxpWs517R0HkyiefQPIytCVh4ZcC5x9XaG8KTdd5iRQUfg==} + + '@changesets/parse@0.4.3': + resolution: {integrity: sha512-ZDmNc53+dXdWEv7fqIUSgRQOLYoUom5Z40gmLgmATmYR9NbL6FJJHwakcCpzaeCy+1D0m0n7mT4jj2B/MQPl7A==} + + '@changesets/pre@2.0.2': + resolution: {integrity: sha512-HaL/gEyFVvkf9KFg6484wR9s0qjAXlZ8qWPDkTyKF6+zqjBe/I2mygg3MbpZ++hdi0ToqNUF8cjj7fBy0dg8Ug==} + + '@changesets/read@0.6.7': + resolution: {integrity: sha512-D1G4AUYGrBEk8vj8MGwf75k9GpN6XL3wg8i42P2jZZwFLXnlr2Pn7r9yuQNbaMCarP7ZQWNJbV6XLeysAIMhTA==} + + '@changesets/should-skip-package@0.1.2': + resolution: {integrity: sha512-qAK/WrqWLNCP22UDdBTMPH5f41elVDlsNyat180A33dWxuUDyNpg6fPi/FyTZwRriVjg0L8gnjJn2F9XAoF0qw==} + + '@changesets/types@4.1.0': + resolution: {integrity: sha512-LDQvVDv5Kb50ny2s25Fhm3d9QSZimsoUGBsUioj6MC3qbMUCuC8GPIvk/M6IvXx3lYhAs0lwWUQLb+VIEUCECw==} + + '@changesets/types@6.1.0': + resolution: {integrity: sha512-rKQcJ+o1nKNgeoYRHKOS07tAMNd3YSN0uHaJOZYjBAgxfV7TUE7JE+z4BzZdQwb5hKaYbayKN5KrYV7ODb2rAA==} + + '@changesets/write@0.4.0': + resolution: {integrity: sha512-CdTLvIOPiCNuH71pyDu3rA+Q0n65cmAbXnwWH84rKGiFumFzkmHNT8KHTMEchcxN+Kl8I54xGUhJ7l3E7X396Q==} + '@colors/colors@1.5.0': resolution: {integrity: sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ==} engines: {node: '>=0.1.90'} @@ -1659,6 +1726,15 @@ packages: resolution: {integrity: sha512-I+ETk2AL+yAVbvuKx5AJpQmoaWhpiTFOg/UJb7ZkMAK4blmtG8ATh5ct+T/8xNld0CZG/2UhtkdMwpgvld92XQ==} engines: {node: '>=18'} + '@inquirer/external-editor@1.0.3': + resolution: {integrity: sha512-RWbSrDiYmO4LbejWY7ttpxczuwQyZLBUyygsA9Nsv95hpzUWwnNTVQmAq3xuh7vNwCp07UTmE5i11XAEExx4RA==} + engines: {node: '>=18'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + '@inquirer/figures@1.0.8': resolution: {integrity: sha512-tKd+jsmhq21AP1LhexC0pPwsCxEhGgAkg28byjJAd+xhmIs8LUX8JbUc3vBf3PhLxWiB5EvyBE5X7JSPAqMAqg==} engines: {node: '>=18'} @@ -1888,6 +1964,12 @@ packages: '@liuli-util/fs-extra@0.1.0': resolution: {integrity: sha512-eaAyDyMGT23QuRGbITVY3SOJff3G9ekAAyGqB9joAnTBmqvFN+9a1FazOdO70G6IUqgpKV451eBHYSRcOJ/FNQ==} + '@manypkg/find-root@1.1.0': + resolution: {integrity: sha512-mki5uBvhHzO8kYYix/WRy2WX8S3B5wdVSc9D6KcU5lQNglP2yt58/VfLuAK49glRXChosY8ap2oJ1qgma3GUVA==} + + '@manypkg/get-packages@1.1.3': + resolution: {integrity: sha512-fo+QhuU3qE/2TQMQmbVMqaQ6EWbMhi4ABWP+O4AM1NqPBuy0OrApV5LO6BrrgnhtAHS2NH6RrVk9OL181tTi8A==} + '@modelcontextprotocol/sdk@1.18.0': resolution: {integrity: sha512-JvKyB6YwS3quM+88JPR0axeRgvdDu3Pv6mdZUy+w4qVkCzGgumb9bXG/TmtDRQv+671yaofVfXSQmFLlWU5qPQ==} engines: {node: '>=18'} @@ -2551,6 +2633,9 @@ packages: '@types/node-fetch@2.6.13': resolution: {integrity: sha512-QGpRVpzSaUs30JBSGPjOg4Uveu384erbHBoT1zeONvyCfwQxIkUshLAOqN/k9EjGviPRmWTTe6aH2qySWKTVSw==} + '@types/node@12.20.55': + resolution: {integrity: sha512-J8xLz7q2OFulZ2cyGTLE1TbbZcjpno7FaN6zdJNrgAdrJ+DZzh/uFR6YrTb4C+nXakvud8Q4+rbhoIWlYQbUFQ==} + '@types/node@18.19.123': resolution: {integrity: sha512-K7DIaHnh0mzVxreCR9qwgNxp3MH9dltPNIEddW9MYUlcKAzm+3grKNSTe2vCJHI1FaLpvpL5JGJrz1UZDKYvDg==} @@ -2921,6 +3006,10 @@ packages: ansi-align@3.0.1: resolution: {integrity: sha512-IOfwwBF5iczOjp/WeY4YxyjqAFMQoZufdQWDd19SEExbVLNXqvpzSJ/M7Za4/sCPmQ0+GRquoA7bGcINcxew6w==} + ansi-colors@4.1.3: + resolution: {integrity: sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==} + engines: {node: '>=6'} + ansi-escapes@4.3.2: resolution: {integrity: sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==} engines: {node: '>=8'} @@ -2979,6 +3068,10 @@ packages: array-flatten@1.1.1: resolution: {integrity: sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==} + array-union@2.1.0: + resolution: {integrity: sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==} + engines: {node: '>=8'} + asap@2.0.6: resolution: {integrity: sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==} @@ -3042,6 +3135,10 @@ packages: resolution: {integrity: sha512-B0xUquLkiGLgHhpPBqvl7GWegWBUNuujQ6kXd/r1U38ElPT6Ok8KZ8e+FpUGEc2ZoRQUzq/aUnaKFc/svWUGSg==} hasBin: true + better-path-resolve@1.0.0: + resolution: {integrity: sha512-pbnl5XzGBdrFU/wT4jqmJVPn2B6UHPBOhzMQkY/SPUPB6QtUXtmBHBIwCbXJol93mOpGMnQyP/+BB19q04xj7g==} + engines: {node: '>=4'} + bignumber.js@9.3.1: resolution: {integrity: sha512-Ko0uX15oIUS7wJ3Rb30Fs6SkVbLmPBAKdlm7q9+ak9bbIeFf0MwuBsQV6z7+X768/cHsfg+WlysDWJcmthjsjQ==} @@ -3161,6 +3258,9 @@ packages: resolution: {integrity: sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw==} engines: {node: '>=10'} + chardet@2.1.1: + resolution: {integrity: sha512-PsezH1rqdV9VvyNhxxOW32/d75r01NY7TQCmOqomRo15ZSOKbpTFVsfjghxo6JloQUCGnH4k1LGu0R4yCLlWQQ==} + cheminfo-types@1.8.1: resolution: {integrity: sha512-FRcpVkox+cRovffgqNdDFQ1eUav+i/Vq/CUd1hcfEl2bevntFlzznL+jE8g4twl6ElB7gZjCko6pYpXyMn+6dA==} @@ -3333,6 +3433,9 @@ packages: csstype@3.2.3: resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==} + dataloader@1.4.0: + resolution: {integrity: sha512-68s5jYdlvasItOJnCuI2Q9s4q98g0pCyL3HrcKJu8KNugUl8ahgmZYg38ysLTgQjjXX3H8CJLkAvWrclWfcalw==} + dc-browser@1.0.3: resolution: {integrity: sha512-B2PJFzWouEPzc+jLo+yTgUXaVz0eZjVsfKX9CnUDzezsluxOKOjllBpw0EJJ1KNJGnNzKpZ4dDYtd+jVp0KRNA==} @@ -3393,6 +3496,10 @@ packages: resolution: {integrity: sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==} engines: {node: '>= 0.8', npm: 1.2.8000 || >= 1.4.16} + detect-indent@6.1.0: + resolution: {integrity: sha512-reYkTUJAZb9gUuZ2RvVCNhVHdg62RHnJ7WJl8ftMi4diZ6NWlciOzQN88pUhSELEwflJht4oQDv0F0BMlwaYtA==} + engines: {node: '>=8'} + detect-newline@3.1.0: resolution: {integrity: sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA==} engines: {node: '>=8'} @@ -3408,10 +3515,18 @@ packages: resolution: {integrity: sha512-X07nttJQkwkfKfvTPG/KSnE2OMdcUCao6+eXF3wmnIQRn2aPAHH3VxDbDOdegkd6JbPsXqShpvEOHfAT+nCNwQ==} engines: {node: '>=0.3.1'} + dir-glob@3.0.1: + resolution: {integrity: sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==} + engines: {node: '>=8'} + dotenv@16.4.5: resolution: {integrity: sha512-ZmdL2rui+eB2YwhsWzjInR8LldtZHGDoQ1ugH85ppHKwpUHL7j7rN0Ti9NCnGiQbhaZ11FpR+7ao1dNsmduNUg==} engines: {node: '>=12'} + dotenv@8.6.0: + resolution: {integrity: sha512-IrPdXQsk2BbzvCBGBOTmmSH5SodmqZNt4ERAZDmW4CT+tL8VtvinqywuANaFu4bOMWki16nqf0e4oC0QIaDr/g==} + engines: {node: '>=10'} + dunder-proto@1.0.1: resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==} engines: {node: '>= 0.4'} @@ -3453,6 +3568,10 @@ packages: resolution: {integrity: sha512-LgQMM4WXU3QI+SYgEc2liRgznaD5ojbmY3sb8LxyguVkIg5FxdpTkvk72te2R38/TGKxH634oLxXRGY6d7AP+Q==} engines: {node: '>=10.13.0'} + enquirer@2.4.1: + resolution: {integrity: sha512-rRqJg/6gd538VHvR3PSrdRBb/1Vy2YfzHqzvbhGIQpDRKIa4FgV/54b5Q1xYSxOOwKvjXweS26E0Q+nAMwp2pQ==} + engines: {node: '>=8.6'} + entities@4.5.0: resolution: {integrity: sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==} engines: {node: '>=0.12'} @@ -3670,6 +3789,9 @@ packages: extend@3.0.2: resolution: {integrity: sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==} + extendable-error@0.1.7: + resolution: {integrity: sha512-UOiS2in6/Q0FK0R0q6UY9vYpQ21mr/Qn1KOnte7vsACuNJf514WvCCUHSRCPcgjPT2bAhNIJdlE6bVap1GKmeg==} + fast-deep-equal@3.1.3: resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} @@ -3794,6 +3916,14 @@ packages: resolution: {integrity: sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==} engines: {node: '>=12'} + fs-extra@7.0.1: + resolution: {integrity: sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw==} + engines: {node: '>=6 <7 || >=8'} + + fs-extra@8.1.0: + resolution: {integrity: sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==} + engines: {node: '>=6 <7 || >=8'} + fs-monkey@1.1.0: resolution: {integrity: sha512-QMUezzXWII9EV5aTFXW1UBVUO77wYPpjqIF8/AviUCThNeSYZykpoTixUeaNNBwmCev0AMDWMAni+f8Hxb1IFw==} @@ -3885,6 +4015,10 @@ packages: resolution: {integrity: sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==} engines: {node: '>=18'} + globby@11.1.0: + resolution: {integrity: sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==} + engines: {node: '>=10'} + globrex@0.1.2: resolution: {integrity: sha512-uHJgbwAMwNFf5mLst7IWLNg14x1CkeqglJb/K3doi4dw6q2IvAAmM/Y81kevy83wP+Sst+nutFTYOGg3d1lsxg==} @@ -3954,6 +4088,10 @@ packages: resolution: {integrity: sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==} engines: {node: '>= 14'} + human-id@4.1.3: + resolution: {integrity: sha512-tsYlhAYpjCKa//8rXZ9DqKEawhPoSytweBC2eNvcaDK+57RZLHGqNs3PZTQO6yekLFSuvA6AlnAfrw1uBvtb+Q==} + hasBin: true + human-signals@2.1.0: resolution: {integrity: sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==} engines: {node: '>=10.17.0'} @@ -3978,6 +4116,10 @@ packages: resolution: {integrity: sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==} engines: {node: '>=0.10.0'} + iconv-lite@0.7.2: + resolution: {integrity: sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==} + engines: {node: '>=0.10.0'} + ignore@5.3.2: resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==} engines: {node: '>= 4'} @@ -4061,6 +4203,14 @@ packages: resolution: {integrity: sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==} engines: {node: '>=8'} + is-subdir@1.2.0: + resolution: {integrity: sha512-2AT6j+gXe/1ueqbW6fLZJiIw3F8iXGJtt0yDrZaBhAZEG1raiTxKWU+IPqMCzQAXOUCKdA4UDMgacKH25XG2Cw==} + engines: {node: '>=4'} + + is-windows@1.0.2: + resolution: {integrity: sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA==} + engines: {node: '>=0.10.0'} + isexe@2.0.0: resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} @@ -4302,6 +4452,9 @@ packages: engines: {node: ^18.0.0 || >=20.0.0} hasBin: true + jsonfile@4.0.0: + resolution: {integrity: sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==} + jsonfile@6.2.0: resolution: {integrity: sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==} @@ -4404,6 +4557,9 @@ packages: lodash.sortby@4.7.0: resolution: {integrity: sha512-HDWXG8isMntAyRF5vZ7xKuEvOhT4AhlRt/3czTSjvGUxjYCBVRQY48ViDHyfYz9VIoBkW4TMGQNapx+l3RUwdA==} + lodash.startcase@4.4.0: + resolution: {integrity: sha512-+WKqsK294HMSc2jEbNgpHpd0JfIBhp7rEV4aqXWqFr6AlXov+SlcgB1Fv01y2kGe3Gc8nMW7VA0SrGuSkRfIEg==} + log-update@6.1.0: resolution: {integrity: sha512-9ie8ItPR6tjY5uYJh8K/Zrv/RMZ5VOlOWvtZdEHYSTFKZfIBPQa9tOAEeAWhd+AnIneLJ22w5fjOYtoutpWq5w==} engines: {node: '>=18'} @@ -4564,6 +4720,10 @@ packages: module-details-from-path@1.0.4: resolution: {integrity: sha512-EGWKgxALGMgzvxYF1UyGTy0HXX/2vHLkw6+NvDKW2jypWbHpjQuj4UMcqQWXHERJhVGKikolT06G3bcKe4fi7w==} + mri@1.2.0: + resolution: {integrity: sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==} + engines: {node: '>=4'} + ms@2.0.0: resolution: {integrity: sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==} @@ -4753,12 +4913,19 @@ packages: resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==} engines: {node: '>= 0.8.0'} + outdent@0.5.0: + resolution: {integrity: sha512-/jHxFIzoMXdqPzTaCpFzAAWhpkSjZPF4Vsn6jAfNpmbH/ymsmd7Qc6VE9BGn0L6YMj6uwpQLxCECpus4ukKS9Q==} + outvariant@1.4.3: resolution: {integrity: sha512-+Sl2UErvtsoajRDKCE5/dBz4DIvHXQQnAxtQTF04OJxY0+DyZXSo5P5Bb7XYWOh81syohlYL24hbDwxedPUJCA==} oxc-resolver@11.19.1: resolution: {integrity: sha512-qE/CIg/spwrTBFt5aKmwe3ifeDdLfA2NESN30E42X/lII5ClF8V7Wt6WIJhcGZjp0/Q+nQ+9vgxGk//xZNX2hg==} + p-filter@2.1.0: + resolution: {integrity: sha512-ZBxxZ5sL2HghephhpGAQdoskxplTwr7ICaehZwLIlfL6acuVgZPm8yBNuRAFBGEqtD/hmUeq9eqLg2ys9Xr/yw==} + engines: {node: '>=8'} + p-finally@1.0.0: resolution: {integrity: sha512-LICb2p9CB7FS+0eR1oqWnHhp0FljGLZCWBE9aix0Uye9W8LTQPwMTYVGWQWIw9RdQiDg4+epXQODwIYJtSJaow==} engines: {node: '>=4'} @@ -4779,6 +4946,10 @@ packages: resolution: {integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==} engines: {node: '>=10'} + p-map@2.1.0: + resolution: {integrity: sha512-y3b8Kpd8OAN444hxfBbFfj1FY/RjtTd8tzYwhUqNYXx0fXx2iX4maP4Qr6qhIKbQXI02wTLAda4fYUbDagTUFw==} + engines: {node: '>=6'} + p-queue@6.6.2: resolution: {integrity: sha512-RwFpb72c/BhQLEXIZ5K2e+AhgNVmIejGlTgiB9MzZ0e93GRvqZ7uSi0dvRF7/XIXDeNkra2fNHBxTyPDGySpjQ==} engines: {node: '>=8'} @@ -4798,6 +4969,9 @@ packages: package-json-from-dist@1.0.1: resolution: {integrity: sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==} + package-manager-detector@0.2.11: + resolution: {integrity: sha512-BEnLolu+yuz22S56CU1SUKq3XC3PkwD5wv4ikR4MfGvnRVcmzXR9DwSlW2fEamyTPyXHomBJRzgapeuBvRNzJQ==} + parent-module@1.0.1: resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==} engines: {node: '>=6'} @@ -4851,6 +5025,10 @@ packages: resolution: {integrity: sha512-TdrF7fW9Rphjq4RjrW0Kp2AW0Ahwu9sRGTkS6bvDi0SCwZlEZYmcfDbEsTz8RVk0EHIS/Vd1bv3JhG+1xZuAyQ==} engines: {node: '>=16'} + path-type@4.0.0: + resolution: {integrity: sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==} + engines: {node: '>=8'} + pathe@2.0.3: resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==} @@ -4870,6 +5048,10 @@ packages: engines: {node: '>=0.10'} hasBin: true + pify@4.0.1: + resolution: {integrity: sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==} + engines: {node: '>=6'} + pirates@4.0.6: resolution: {integrity: sha512-saLsH7WeYYPiD25LDuLRRY/i+6HaPYr6G1OUlN39otzkSTxKnubR9RTxS3/Kk50s1g2JTgFwWQDQyplC5/SHZg==} engines: {node: '>= 6'} @@ -4970,6 +5152,9 @@ packages: resolution: {integrity: sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==} engines: {node: '>=0.6'} + quansync@0.2.11: + resolution: {integrity: sha512-AifT7QEbW9Nri4tAwR5M/uzpBuqfZf+zwaEM/QkzEjj7NBuFD2rBuy0K3dE+8wltbezDV7JMA0WfnCPYRSYbXA==} + querystringify@2.2.0: resolution: {integrity: sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==} @@ -5003,6 +5188,10 @@ packages: resolution: {integrity: sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ==} engines: {node: '>=0.10.0'} + read-yaml-file@1.1.0: + resolution: {integrity: sha512-VIMnQi/Z4HT2Fxuwg5KrY174U1VdUIASQVWXXyqtNRtxSr9IYkn1rsI6Tb6HsrHCmB7gVpNwX6JxPTHcH6IoTA==} + engines: {node: '>=6'} + readdirp@4.1.2: resolution: {integrity: sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==} engines: {node: '>= 14.18.0'} @@ -5220,6 +5409,9 @@ packages: engines: {node: '>= 8'} deprecated: The work that was done in this beta branch won't be included in future versions + spawndamnit@3.0.1: + resolution: {integrity: sha512-MmnduQUuHCoFckZoWnXsTg7JaiLBJrKFj9UI2MbRPGaJeVpsLcVBu6P/IGZovziM/YBsellCmsprgNA+w0CzVg==} + sprintf-js@1.0.3: resolution: {integrity: sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==} @@ -5281,6 +5473,10 @@ packages: resolution: {integrity: sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==} engines: {node: '>=12'} + strip-bom@3.0.0: + resolution: {integrity: sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==} + engines: {node: '>=4'} + strip-bom@4.0.0: resolution: {integrity: sha512-3xurFv5tEgii33Zi8Jtp55wEIILR9eh34FAW00PZf+JnSsTmV/ioewSgQl97JHvgjoRGwPShsWm+IdrxB35d0w==} engines: {node: '>=8'} @@ -5353,6 +5549,10 @@ packages: engines: {node: '>=18'} deprecated: Old versions of tar are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me + term-size@2.2.1: + resolution: {integrity: sha512-wK0Ri4fOGjv/XPy8SBHZChl8CM7uMc5VML7SqiQ0zG7+J5Vr+RMQDoHa2CNT6KHUnTGIXH34UDMkPzAUyapBZg==} + engines: {node: '>=8'} + termi-link@1.1.0: resolution: {integrity: sha512-2qSN6TnomHgVLtk+htSWbaYs4Rd2MH/RU7VpHTy6MBstyNyWbM4yKd1DCYpE3fDg8dmGWojXCngNi/MHCzGuAA==} engines: {node: '>=12'} @@ -5659,6 +5859,10 @@ packages: unionfs@4.6.0: resolution: {integrity: sha512-fJAy3gTHjFi5S3TP5EGdjs/OUMFFvI/ady3T8qVuZfkv8Qi8prV/Q8BuFEgODJslhZTT2z2qdD2lGdee9qjEnA==} + universalify@0.1.2: + resolution: {integrity: sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==} + engines: {node: '>= 4.0.0'} + universalify@0.2.0: resolution: {integrity: sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg==} engines: {node: '>= 4.0.0'} @@ -6734,6 +6938,164 @@ snapshots: '@cfworker/json-schema@4.0.3': {} + '@changesets/apply-release-plan@7.1.0': + dependencies: + '@changesets/config': 3.1.3 + '@changesets/get-version-range-type': 0.4.0 + '@changesets/git': 3.0.4 + '@changesets/should-skip-package': 0.1.2 + '@changesets/types': 6.1.0 + '@manypkg/get-packages': 1.1.3 + detect-indent: 6.1.0 + fs-extra: 7.0.1 + lodash.startcase: 4.4.0 + outdent: 0.5.0 + prettier: 2.8.8 + resolve-from: 5.0.0 + semver: 7.7.4 + + '@changesets/assemble-release-plan@6.0.9': + dependencies: + '@changesets/errors': 0.2.0 + '@changesets/get-dependents-graph': 2.1.3 + '@changesets/should-skip-package': 0.1.2 + '@changesets/types': 6.1.0 + '@manypkg/get-packages': 1.1.3 + semver: 7.7.4 + + '@changesets/changelog-git@0.2.1': + dependencies: + '@changesets/types': 6.1.0 + + '@changesets/changelog-github@0.6.0': + dependencies: + '@changesets/get-github-info': 0.8.0 + '@changesets/types': 6.1.0 + dotenv: 8.6.0 + transitivePeerDependencies: + - encoding + + '@changesets/cli@2.30.0(@types/node@22.19.1)': + dependencies: + '@changesets/apply-release-plan': 7.1.0 + '@changesets/assemble-release-plan': 6.0.9 + '@changesets/changelog-git': 0.2.1 + '@changesets/config': 3.1.3 + '@changesets/errors': 0.2.0 + '@changesets/get-dependents-graph': 2.1.3 + '@changesets/get-release-plan': 4.0.15 + '@changesets/git': 3.0.4 + '@changesets/logger': 0.1.1 + '@changesets/pre': 2.0.2 + '@changesets/read': 0.6.7 + '@changesets/should-skip-package': 0.1.2 + '@changesets/types': 6.1.0 + '@changesets/write': 0.4.0 + '@inquirer/external-editor': 1.0.3(@types/node@22.19.1) + '@manypkg/get-packages': 1.1.3 + ansi-colors: 4.1.3 + enquirer: 2.4.1 + fs-extra: 7.0.1 + mri: 1.2.0 + package-manager-detector: 0.2.11 + picocolors: 1.1.1 + resolve-from: 5.0.0 + semver: 7.7.4 + spawndamnit: 3.0.1 + term-size: 2.2.1 + transitivePeerDependencies: + - '@types/node' + + '@changesets/config@3.1.3': + dependencies: + '@changesets/errors': 0.2.0 + '@changesets/get-dependents-graph': 2.1.3 + '@changesets/logger': 0.1.1 + '@changesets/should-skip-package': 0.1.2 + '@changesets/types': 6.1.0 + '@manypkg/get-packages': 1.1.3 + fs-extra: 7.0.1 + micromatch: 4.0.8 + + '@changesets/errors@0.2.0': + dependencies: + extendable-error: 0.1.7 + + '@changesets/get-dependents-graph@2.1.3': + dependencies: + '@changesets/types': 6.1.0 + '@manypkg/get-packages': 1.1.3 + picocolors: 1.1.1 + semver: 7.7.4 + + '@changesets/get-github-info@0.8.0': + dependencies: + dataloader: 1.4.0 + node-fetch: 2.7.0 + transitivePeerDependencies: + - encoding + + '@changesets/get-release-plan@4.0.15': + dependencies: + '@changesets/assemble-release-plan': 6.0.9 + '@changesets/config': 3.1.3 + '@changesets/pre': 2.0.2 + '@changesets/read': 0.6.7 + '@changesets/types': 6.1.0 + '@manypkg/get-packages': 1.1.3 + + '@changesets/get-version-range-type@0.4.0': {} + + '@changesets/git@3.0.4': + dependencies: + '@changesets/errors': 0.2.0 + '@manypkg/get-packages': 1.1.3 + is-subdir: 1.2.0 + micromatch: 4.0.8 + spawndamnit: 3.0.1 + + '@changesets/logger@0.1.1': + dependencies: + picocolors: 1.1.1 + + '@changesets/parse@0.4.3': + dependencies: + '@changesets/types': 6.1.0 + js-yaml: 4.1.1 + + '@changesets/pre@2.0.2': + dependencies: + '@changesets/errors': 0.2.0 + '@changesets/types': 6.1.0 + '@manypkg/get-packages': 1.1.3 + fs-extra: 7.0.1 + + '@changesets/read@0.6.7': + dependencies: + '@changesets/git': 3.0.4 + '@changesets/logger': 0.1.1 + '@changesets/parse': 0.4.3 + '@changesets/types': 6.1.0 + fs-extra: 7.0.1 + p-filter: 2.1.0 + picocolors: 1.1.1 + + '@changesets/should-skip-package@0.1.2': + dependencies: + '@changesets/types': 6.1.0 + '@manypkg/get-packages': 1.1.3 + + '@changesets/types@4.1.0': {} + + '@changesets/types@6.1.0': {} + + '@changesets/write@0.4.0': + dependencies: + '@changesets/types': 6.1.0 + fs-extra: 7.0.1 + human-id: 4.1.3 + prettier: 2.8.8 + '@colors/colors@1.5.0': optional: true @@ -7241,6 +7603,13 @@ snapshots: - '@types/node' optional: true + '@inquirer/external-editor@1.0.3(@types/node@22.19.1)': + dependencies: + chardet: 2.1.1 + iconv-lite: 0.7.2 + optionalDependencies: + '@types/node': 22.19.1 + '@inquirer/figures@1.0.8': {} '@inquirer/type@3.0.1(@types/node@20.10.5)': @@ -7266,7 +7635,7 @@ snapshots: dependencies: string-width: 5.1.2 string-width-cjs: string-width@4.2.3 - strip-ansi: 7.1.0 + strip-ansi: 7.1.2 strip-ansi-cjs: strip-ansi@6.0.1 wrap-ansi: 8.1.0 wrap-ansi-cjs: wrap-ansi@7.0.0 @@ -7614,6 +7983,22 @@ snapshots: '@types/fs-extra': 9.0.13 fs-extra: 10.1.0 + '@manypkg/find-root@1.1.0': + dependencies: + '@babel/runtime': 7.28.4 + '@types/node': 12.20.55 + find-up: 4.1.0 + fs-extra: 8.1.0 + + '@manypkg/get-packages@1.1.3': + dependencies: + '@babel/runtime': 7.28.4 + '@changesets/types': 4.1.0 + '@manypkg/find-root': 1.1.0 + fs-extra: 8.1.0 + globby: 11.1.0 + read-yaml-file: 1.1.0 + '@modelcontextprotocol/sdk@1.18.0': dependencies: ajv: 6.12.6 @@ -8300,6 +8685,8 @@ snapshots: '@types/node': 22.19.1 form-data: 4.0.4 + '@types/node@12.20.55': {} + '@types/node@18.19.123': dependencies: undici-types: 5.26.5 @@ -8814,6 +9201,8 @@ snapshots: dependencies: string-width: 4.2.3 + ansi-colors@4.1.3: {} + ansi-escapes@4.3.2: dependencies: type-fest: 0.21.3 @@ -8860,6 +9249,8 @@ snapshots: array-flatten@1.1.1: {} + array-union@2.1.0: {} + asap@2.0.6: {} assertion-error@2.0.1: {} @@ -9011,6 +9402,10 @@ snapshots: baseline-browser-mapping@2.9.14: {} + better-path-resolve@1.0.0: + dependencies: + is-windows: 1.0.2 + bignumber.js@9.3.1: {} binary-search@1.3.6: {} @@ -9178,6 +9573,8 @@ snapshots: char-regex@1.0.2: {} + chardet@2.1.1: {} + cheminfo-types@1.8.1: {} chokidar@4.0.3: @@ -9337,6 +9734,8 @@ snapshots: csstype@3.2.3: {} + dataloader@1.4.0: {} + dc-browser@1.0.3: {} debug@2.6.9: @@ -9365,6 +9764,8 @@ snapshots: destroy@1.2.0: {} + detect-indent@6.1.0: {} + detect-newline@3.1.0: {} diff-match-patch@1.0.5: {} @@ -9374,8 +9775,14 @@ snapshots: diff@4.0.4: optional: true + dir-glob@3.0.1: + dependencies: + path-type: 4.0.0 + dotenv@16.4.5: {} + dotenv@8.6.0: {} + dunder-proto@1.0.1: dependencies: call-bind-apply-helpers: 1.0.2 @@ -9409,6 +9816,11 @@ snapshots: graceful-fs: 4.2.11 tapable: 2.3.0 + enquirer@2.4.1: + dependencies: + ansi-colors: 4.1.3 + strip-ansi: 6.0.1 + entities@4.5.0: {} environment@1.1.0: {} @@ -9790,6 +10202,8 @@ snapshots: extend@3.0.2: {} + extendable-error@0.1.7: {} + fast-deep-equal@3.1.3: {} fast-glob@3.3.3: @@ -9922,6 +10336,18 @@ snapshots: jsonfile: 6.2.0 universalify: 2.0.1 + fs-extra@7.0.1: + dependencies: + graceful-fs: 4.2.11 + jsonfile: 4.0.0 + universalify: 0.1.2 + + fs-extra@8.1.0: + dependencies: + graceful-fs: 4.2.11 + jsonfile: 4.0.0 + universalify: 0.1.2 + fs-monkey@1.1.0: {} fs.realpath@1.0.0: {} @@ -10021,6 +10447,15 @@ snapshots: globals@14.0.0: {} + globby@11.1.0: + dependencies: + array-union: 2.1.0 + dir-glob: 3.0.1 + fast-glob: 3.3.3 + ignore: 5.3.2 + merge2: 1.4.1 + slash: 3.0.0 + globrex@0.1.2: {} google-auth-library@9.15.1: @@ -10095,6 +10530,8 @@ snapshots: transitivePeerDependencies: - supports-color + human-id@4.1.3: {} + human-signals@2.1.0: {} humanize-ms@1.2.1: @@ -10113,6 +10550,10 @@ snapshots: dependencies: safer-buffer: 2.1.2 + iconv-lite@0.7.2: + dependencies: + safer-buffer: 2.1.2 + ignore@5.3.2: {} ignore@7.0.5: {} @@ -10175,6 +10616,12 @@ snapshots: is-stream@2.0.1: {} + is-subdir@1.2.0: + dependencies: + better-path-resolve: 1.0.0 + + is-windows@1.0.2: {} + isexe@2.0.0: {} istanbul-lib-coverage@3.2.2: {} @@ -10630,6 +11077,10 @@ snapshots: chalk: 5.3.0 diff-match-patch: 1.0.5 + jsonfile@4.0.0: + optionalDependencies: + graceful-fs: 4.2.11 + jsonfile@6.2.0: dependencies: universalify: 2.0.1 @@ -10746,6 +11197,8 @@ snapshots: lodash.sortby@4.7.0: {} + lodash.startcase@4.4.0: {} + log-update@6.1.0: dependencies: ansi-escapes: 7.3.0 @@ -10908,6 +11361,8 @@ snapshots: module-details-from-path@1.0.4: {} + mri@1.2.0: {} + ms@2.0.0: {} ms@2.1.3: {} @@ -11224,6 +11679,8 @@ snapshots: type-check: 0.4.0 word-wrap: 1.2.5 + outdent@0.5.0: {} + outvariant@1.4.3: {} oxc-resolver@11.19.1: @@ -11249,6 +11706,10 @@ snapshots: '@oxc-resolver/binding-win32-ia32-msvc': 11.19.1 '@oxc-resolver/binding-win32-x64-msvc': 11.19.1 + p-filter@2.1.0: + dependencies: + p-map: 2.1.0 + p-finally@1.0.0: {} p-limit@2.3.0: @@ -11267,6 +11728,8 @@ snapshots: dependencies: p-limit: 3.1.0 + p-map@2.1.0: {} + p-queue@6.6.2: dependencies: eventemitter3: 4.0.7 @@ -11285,6 +11748,10 @@ snapshots: package-json-from-dist@1.0.1: {} + package-manager-detector@0.2.11: + dependencies: + quansync: 0.2.11 + parent-module@1.0.1: dependencies: callsites: 3.1.0 @@ -11328,6 +11795,8 @@ snapshots: path-to-regexp@8.2.0: optional: true + path-type@4.0.0: {} + pathe@2.0.3: {} picocolors@1.1.1: {} @@ -11338,6 +11807,8 @@ snapshots: pidtree@0.6.0: {} + pify@4.0.1: {} + pirates@4.0.6: {} pkce-challenge@5.0.0: @@ -11432,6 +11903,8 @@ snapshots: side-channel: 1.1.0 optional: true + quansync@0.2.11: {} + querystringify@2.2.0: {} queue-microtask@1.2.3: {} @@ -11467,6 +11940,13 @@ snapshots: react@19.2.4: {} + read-yaml-file@1.1.0: + dependencies: + graceful-fs: 4.2.11 + js-yaml: 3.14.1 + pify: 4.0.1 + strip-bom: 3.0.0 + readdirp@4.1.2: {} require-directory@2.1.1: {} @@ -11728,6 +12208,11 @@ snapshots: dependencies: whatwg-url: 7.1.0 + spawndamnit@3.0.1: + dependencies: + cross-spawn: 7.0.6 + signal-exit: 4.1.0 + sprintf-js@1.0.3: {} sswr@2.1.0(svelte@5.39.0): @@ -11764,7 +12249,7 @@ snapshots: dependencies: eastasianwidth: 0.2.0 emoji-regex: 9.2.2 - strip-ansi: 7.1.0 + strip-ansi: 7.1.2 string-width@7.2.0: dependencies: @@ -11789,6 +12274,8 @@ snapshots: dependencies: ansi-regex: 6.1.0 + strip-bom@3.0.0: {} + strip-bom@4.0.0: {} strip-final-newline@2.0.0: {} @@ -11870,6 +12357,8 @@ snapshots: minizlib: 3.1.0 yallist: 5.0.0 + term-size@2.2.1: {} + termi-link@1.1.0: {} terser-webpack-plugin@5.3.16(@swc/core@1.15.8)(esbuild@0.27.0)(webpack@5.104.1(@swc/core@1.15.8)(esbuild@0.27.0)): @@ -12234,6 +12723,8 @@ snapshots: dependencies: fs-monkey: 1.1.0 + universalify@0.1.2: {} + universalify@0.2.0: {} universalify@2.0.1: {} @@ -12691,7 +13182,7 @@ snapshots: dependencies: ansi-styles: 6.2.1 string-width: 5.1.2 - strip-ansi: 7.1.0 + strip-ansi: 7.1.2 wrap-ansi@9.0.2: dependencies: diff --git a/scripts/release/_shared.mjs b/scripts/release/_shared.mjs new file mode 100644 index 000000000..2ab83769b --- /dev/null +++ b/scripts/release/_shared.mjs @@ -0,0 +1,207 @@ +import { appendFileSync, existsSync, readFileSync, readdirSync } from "node:fs"; +import path from "node:path"; +import { fileURLToPath } from "node:url"; + +const __dirname = path.dirname(fileURLToPath(import.meta.url)); + +export const REPO_ROOT = path.resolve(__dirname, "../.."); +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"; + +export const PUBLISHABLE_PACKAGES = [ + { dir: "js", name: "braintrust" }, + { dir: "integrations/browser-js", name: "@braintrust/browser" }, + { dir: "integrations/langchain-js", name: "@braintrust/langchain-js" }, + { dir: "integrations/openai-agents-js", name: "@braintrust/openai-agents" }, + { dir: "integrations/otel-js", name: "@braintrust/otel" }, + { + dir: "integrations/templates-nunjucks", + name: "@braintrust/templates-nunjucks-js", + }, + { dir: "integrations/temporal-js", name: "@braintrust/temporal" }, + { + dir: "integrations/vercel-ai-sdk", + name: "@braintrust/vercel-ai-sdk", + }, +]; + +export const PRIVATE_WORKSPACE_PACKAGES = [ + { + dir: "js/src/wrappers/vitest", + name: "@braintrust/vitest-wrapper-tests", + }, + { + dir: "js/src/wrappers/claude-agent-sdk", + name: "@braintrust/claude-agent-sdk-tests", + }, + { dir: "e2e", name: "@braintrust/js-e2e-tests" }, +]; + +export const PUBLISHABLE_PACKAGE_NAMES = PUBLISHABLE_PACKAGES.map( + (pkg) => pkg.name, +); +export const PUBLISHABLE_PACKAGE_DIRS = new Set( + PUBLISHABLE_PACKAGES.map((pkg) => pkg.dir), +); +export const PUBLISHABLE_PACKAGE_NAME_SET = new Set(PUBLISHABLE_PACKAGE_NAMES); +export const PUBLISHABLE_PACKAGE_MAP = new Map( + PUBLISHABLE_PACKAGES.map((pkg) => [pkg.name, pkg]), +); + +export function repoPath(...segments) { + return path.join(REPO_ROOT, ...segments); +} + +export function readJson(relativePath) { + return JSON.parse(readFileSync(repoPath(relativePath), "utf8")); +} + +export function readPackage(relativeDir) { + const manifest = readJson(path.posix.join(relativeDir, "package.json")); + return { + ...manifest, + dir: relativeDir, + packageJsonPath: repoPath(relativeDir, "package.json"), + changelogPath: repoPath(relativeDir, "CHANGELOG.md"), + }; +} + +export function getApprovedPackage(relativeDir) { + return PUBLISHABLE_PACKAGES.find((pkg) => pkg.dir === relativeDir); +} + +export function getApprovedPackageByName(name) { + return PUBLISHABLE_PACKAGE_MAP.get(name); +} + +export function toPosixPath(filePath) { + return filePath.split(path.sep).join(path.posix.sep); +} + +export function escapeRegExp(value) { + return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); +} + +export function writeGithubOutput( + key, + value, + outputPath = process.env.GITHUB_OUTPUT, +) { + if (!outputPath) { + return; + } + + const serialized = String(value ?? ""); + if (serialized.includes("\n")) { + appendFileSync(outputPath, `${key}< entry.isDirectory()) + .map((entry) => path.posix.join(relativeBaseDir, entry.name)); +} + +export function listWorkspacePackageDirs() { + const workspaceYaml = readFileSync(repoPath("pnpm-workspace.yaml"), "utf8"); + const patterns = [ + ...workspaceYaml.matchAll(/^\s*-\s+"?([^"\n]+)"?\s*$/gm), + ].map((match) => match[1]); + + const includePatterns = patterns.filter( + (pattern) => !pattern.startsWith("!"), + ); + const ignorePatterns = patterns + .filter((pattern) => pattern.startsWith("!")) + .map((pattern) => pattern.slice(1)); + + const discovered = new Set(); + + for (const pattern of includePatterns) { + if (pattern.endsWith("/*")) { + for (const dir of listImmediateChildDirectories(pattern.slice(0, -2))) { + if (existsSync(repoPath(dir, "package.json"))) { + discovered.add(dir); + } + } + continue; + } + + if (existsSync(repoPath(pattern, "package.json"))) { + discovered.add(pattern); + } + } + + return [...discovered].filter( + (dir) => + !ignorePatterns.some((pattern) => matchesIgnorePattern(dir, pattern)), + ); +} + +function matchesIgnorePattern(relativeDir, pattern) { + if (pattern.endsWith("/**")) { + const base = pattern.slice(0, -3); + return relativeDir === base || relativeDir.startsWith(`${base}/`); + } + return relativeDir === pattern; +} + +export function filterPublishableReleases(status) { + return (status.releases ?? []).filter((release) => + PUBLISHABLE_PACKAGE_NAME_SET.has(release.name), + ); +} + +export function getReleaseTag(name, version) { + return `${name}@${version}`; +} + +export function formatPackageList(packages) { + return packages.map((pkg) => `- ${pkg.name}@${pkg.version}`).join("\n"); +} diff --git a/scripts/release/build-publishable-packages.sh b/scripts/release/build-publishable-packages.sh new file mode 100755 index 000000000..49e285f23 --- /dev/null +++ b/scripts/release/build-publishable-packages.sh @@ -0,0 +1,22 @@ +#!/usr/bin/env bash +set -euo pipefail + +RELEASE_MANIFEST="${1:-${RELEASE_MANIFEST:-}}" + +if [[ -n "$RELEASE_MANIFEST" && -f "$RELEASE_MANIFEST" ]]; then + mapfile -t PACKAGES < <(node -e 'const fs = require("fs"); const manifest = JSON.parse(fs.readFileSync(process.argv[1], "utf8")); for (const pkg of manifest.packages || []) console.log(pkg.name);' "$RELEASE_MANIFEST") +else + mapfile -t PACKAGES < <(node -e 'import("./_shared.mjs").then(m => m.PUBLISHABLE_PACKAGE_NAMES.forEach(n => console.log(n)));') +fi + +if [[ ${#PACKAGES[@]} -eq 0 ]]; then + echo "No publishable packages selected for build." + exit 0 +fi + +ARGS=() +for package in "${PACKAGES[@]}"; do + ARGS+=("--filter=${package}") +done + +pnpm exec turbo run build "${ARGS[@]}" diff --git a/scripts/release/check-changeset-status.mjs b/scripts/release/check-changeset-status.mjs new file mode 100644 index 000000000..98b43e3bd --- /dev/null +++ b/scripts/release/check-changeset-status.mjs @@ -0,0 +1,52 @@ +import { readFileSync } from "node:fs"; + +import { + appendSummary, + filterPublishableReleases, + formatPackageList, + parseArgs, + writeGithubOutput, +} from "./_shared.mjs"; + +const args = parseArgs(); +const statusPath = args["status-file"] ?? ".changeset-status.json"; +const mode = args.mode ?? "release"; +const status = JSON.parse(readFileSync(statusPath, "utf8")); + +const releases = filterPublishableReleases(status); + +writeGithubOutput("has_packages", releases.length > 0); +writeGithubOutput("package_count", releases.length); +writeGithubOutput( + "package_names", + releases.map((release) => release.name).join(","), +); + +if (releases.length === 0) { + const message = `No publishable packages would be released for ${mode}.`; + console.log(message); + appendSummary(`## ${titleCase(mode)}\n\n${message}`); + process.exit(0); +} + +const packageList = formatPackageList( + releases.map((release) => ({ + name: release.name, + version: release.newVersion ?? release.type, + })), +); + +console.log( + `${releases.length} publishable package(s) would be released for ${mode}:\n${packageList}`, +); +appendSummary( + `## ${titleCase(mode)}\n\n${releases.length} publishable package(s) have pending release intent:\n${packageList}`, +); + +function titleCase(value) { + return value + .split(/[-_\s]+/) + .filter(Boolean) + .map((part) => part[0].toUpperCase() + part.slice(1)) + .join(" "); +} diff --git a/scripts/release/create-github-releases.mjs b/scripts/release/create-github-releases.mjs new file mode 100644 index 000000000..3975ebcf3 --- /dev/null +++ b/scripts/release/create-github-releases.mjs @@ -0,0 +1,96 @@ +import { existsSync, readFileSync } from "node:fs"; + +import { escapeRegExp, parseArgs } from "./_shared.mjs"; + +const args = parseArgs(); +const manifestPath = args.manifest ?? ".release-manifest.json"; +const manifest = JSON.parse(readFileSync(manifestPath, "utf8")); +const token = process.env.GITHUB_TOKEN; +const repository = process.env.GITHUB_REPOSITORY; + +if (!token || !repository) { + throw new Error("GITHUB_TOKEN and GITHUB_REPOSITORY must be set"); +} + +if ((manifest.packages ?? []).length === 0) { + console.log("No GitHub releases to create."); + process.exit(0); +} + +for (const pkg of manifest.packages) { + const tag = pkg.tag ?? `${pkg.name}@${pkg.version}`; + const existing = await fetchGithub( + `/repos/${repository}/releases/tags/${encodeURIComponent(tag)}`, + token, + { method: "GET", allow404: true }, + ); + + if (existing.status === 200) { + console.log(`GitHub release already exists for ${tag}`); + continue; + } + + 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) { + const response = await fetch(`https://api.github.com${endpoint}`, { + method: options.method, + headers: { + Accept: "application/vnd.github+json", + Authorization: `Bearer ${authToken}`, + "Content-Type": "application/json", + "X-GitHub-Api-Version": "2022-11-28", + }, + body: options.body, + }); + + if (options.allow404 && response.status === 404) { + return response; + } + + if (!response.ok) { + const body = await response.text(); + throw new Error( + `${options.method} ${endpoint} failed: ${response.status} ${body}`, + ); + } + + return response; +} diff --git a/scripts/release/enforce-changeset.mjs b/scripts/release/enforce-changeset.mjs new file mode 100644 index 000000000..77b28a934 --- /dev/null +++ b/scripts/release/enforce-changeset.mjs @@ -0,0 +1,75 @@ +import { execFileSync } from "node:child_process"; +import { readFileSync } from "node:fs"; + +import { PUBLISHABLE_PACKAGES } from "./_shared.mjs"; + +if (process.env.GITHUB_EVENT_NAME !== "pull_request") { + console.log("Changeset enforcement only runs on pull_request events."); + process.exit(0); +} + +const eventPath = process.env.GITHUB_EVENT_PATH; +if (!eventPath) { + throw new Error("GITHUB_EVENT_PATH is required"); +} + +const event = JSON.parse(readFileSync(eventPath, "utf8")); +const pullRequest = event.pull_request; +const labels = new Set((pullRequest.labels ?? []).map((label) => label.name)); +const title = pullRequest.title ?? ""; +const body = pullRequest.body ?? ""; + +if ( + title.trim() === "[ci] release" || + labels.has("skip-changeset") || + /#skip-changeset/i.test(title) || + /#skip-changeset/i.test(body) +) { + console.log("Changeset requirement bypassed for this pull request."); + process.exit(0); +} + +const baseRef = process.env.GITHUB_BASE_REF; +if (!baseRef) { + throw new Error("GITHUB_BASE_REF is required for pull_request checks"); +} + +const changedFiles = execFileSync( + "git", + ["diff", "--name-only", `origin/${baseRef}...HEAD`], + { encoding: "utf8" }, +) + .split("\n") + .map((file) => file.trim()) + .filter(Boolean); + +const publishableDirs = PUBLISHABLE_PACKAGES.map((pkg) => `${pkg.dir}/`); +const touchedPublishableFiles = changedFiles.filter((file) => + publishableDirs.some((dir) => file.startsWith(dir)), +); + +if (touchedPublishableFiles.length === 0) { + console.log("No publishable package paths changed; no changeset required."); + process.exit(0); +} + +const hasChangeset = changedFiles.some( + (file) => file.startsWith(".changeset/") && file.endsWith(".md"), +); + +if (hasChangeset) { + console.log( + "Found at least one changeset file for publishable package changes.", + ); + process.exit(0); +} + +console.error("Missing changeset for publishable package changes."); +console.error("Touched publishable files:"); +for (const file of touchedPublishableFiles) { + console.error(`- ${file}`); +} +console.error( + "Add a .changeset/*.md file with `pnpm changeset`, or apply the skip-changeset label or add #skip-changeset to the PR title/body when no release is intended.", +); +process.exit(1); diff --git a/scripts/release/pack-publishable-packages.mjs b/scripts/release/pack-publishable-packages.mjs new file mode 100644 index 000000000..fe73eeff8 --- /dev/null +++ b/scripts/release/pack-publishable-packages.mjs @@ -0,0 +1,82 @@ +import { execFileSync } from "node:child_process"; +import { mkdirSync, readFileSync, writeFileSync } from "node:fs"; +import path from "node:path"; + +import { + PUBLISHABLE_PACKAGES, + appendSummary, + parseArgs, + repoPath, +} from "./_shared.mjs"; + +const args = parseArgs(); +const outputDir = args["output-dir"] ?? "artifacts/release-packages"; +const manifestPath = args.manifest; +const reportPath = + args.report ?? path.posix.join(outputDir, "pack-report.json"); + +const targets = getTargets(manifestPath); +mkdirSync(repoPath(outputDir), { recursive: true }); + +const tarballs = []; +for (const target of targets) { + const relativeOutputDir = path.posix.relative(target.dir, outputDir); + const tarball = execFileSync( + "npm", + ["pack", "--pack-destination", relativeOutputDir], + { + cwd: repoPath(target.dir), + encoding: "utf8", + }, + ).trim(); + + tarballs.push({ + name: target.name, + dir: target.dir, + version: target.version, + tarball, + }); +} + +writeFileSync( + repoPath(reportPath), + `${JSON.stringify({ tarballs }, null, 2)}\n`, + "utf8", +); +console.log(`Packed ${tarballs.length} package(s) into ${outputDir}`); +appendSummary( + `## Packed publishable packages\n\n${tarballs + .map((entry) => `- ${entry.name}@${entry.version}: ${entry.tarball}`) + .join("\n")}`, +); + +function getTargets(maybeManifestPath) { + if (!maybeManifestPath) { + return PUBLISHABLE_PACKAGES.map((pkg) => + readPackageInfo(pkg.dir, pkg.name), + ); + } + + const manifest = JSON.parse( + readFileSync(repoPath(maybeManifestPath), "utf8"), + ); + return manifest.packages.map((pkg) => readPackageInfo(pkg.dir, pkg.name)); +} + +function readPackageInfo(relativeDir, expectedName) { + const manifest = JSON.parse( + readFileSync(repoPath(relativeDir, "package.json"), "utf8"), + ); + + if (manifest.name !== expectedName) { + throw new Error( + `Expected ${relativeDir} to be ${expectedName}, found ${manifest.name}`, + ); + } + + return { + dir: relativeDir, + name: manifest.name, + version: manifest.version, + }; +} diff --git a/scripts/release/prepare-release-manifest.mjs b/scripts/release/prepare-release-manifest.mjs new file mode 100644 index 000000000..a8112bbf0 --- /dev/null +++ b/scripts/release/prepare-release-manifest.mjs @@ -0,0 +1,64 @@ +import { readFileSync, writeFileSync } from "node:fs"; +import { execFileSync } from "node:child_process"; + +import { + appendSummary, + filterPublishableReleases, + getApprovedPackageByName, + getReleaseTag, + parseArgs, + readPackage, + writeGithubOutput, +} from "./_shared.mjs"; + +const args = parseArgs(); +const statusPath = args["status-file"] ?? ".changeset-status.json"; +const outputPath = args.output ?? ".release-manifest.json"; +const mode = args.mode ?? "release"; + +const status = JSON.parse(readFileSync(statusPath, "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 manifest = { + mode, + commit: execFileSync("git", ["rev-parse", "HEAD"], { + encoding: "utf8", + }).trim(), + packages, +}; + +writeFileSync(outputPath, `${JSON.stringify(manifest, null, 2)}\n`, "utf8"); + +writeGithubOutput("has_packages", packages.length > 0); +writeGithubOutput("package_count", packages.length); +writeGithubOutput("manifest_path", outputPath); + +if (packages.length === 0) { + appendSummary(`## ${mode}\n\nNo publishable packages are queued.`); + console.log("No publishable packages found in status file."); + process.exit(0); +} + +const packageLines = packages + .map((pkg) => `- ${pkg.name}@${pkg.version} (${pkg.type})`) + .join("\n"); +appendSummary(`## ${mode}\n\nPrepared release manifest:\n${packageLines}`); +console.log(`Prepared release manifest at ${outputPath}:\n${packageLines}`); diff --git a/scripts/release/push-release-tags.mjs b/scripts/release/push-release-tags.mjs new file mode 100644 index 000000000..acecb531b --- /dev/null +++ b/scripts/release/push-release-tags.mjs @@ -0,0 +1,90 @@ +import { execFileSync, spawnSync } from "node:child_process"; +import { readFileSync } from "node:fs"; + +import { appendSummary, parseArgs } from "./_shared.mjs"; + +const args = parseArgs(); +const manifestPath = args.manifest ?? ".release-manifest.json"; +const manifest = JSON.parse(readFileSync(manifestPath, "utf8")); + +if ((manifest.packages ?? []).length === 0) { + console.log("No release tags to push."); + process.exit(0); +} + +const tags = manifest.packages.map( + (pkg) => pkg.tag ?? `${pkg.name}@${pkg.version}`, +); + +const existingRemoteTags = fetchRemoteTags(tags); + +const toCreate = []; +const toPush = []; + +for (const tag of tags) { + if (existingRemoteTags.has(tag)) { + continue; + } + + if (!localTagExists(tag)) { + toCreate.push(tag); + } + + toPush.push(tag); +} + +for (const tag of toCreate) { + execFileSync("git", ["tag", tag], { stdio: "inherit" }); +} + +if (toPush.length > 0) { + execFileSync( + "git", + ["push", "origin", ...toPush.map((tag) => `refs/tags/${tag}`)], + { stdio: "inherit" }, + ); +} + +if (toPush.length === 0) { + console.log("All release tags already exist on origin."); + appendSummary( + "## Release tags\n\nAll release tags already existed on origin.", + ); + process.exit(0); +} + +const list = toPush.map((tag) => `- ${tag}`).join("\n"); +console.log(`Pushed release tags:\n${list}`); +appendSummary(`## Release tags\n\nPushed:\n${list}`); + +function localTagExists(tag) { + return ( + spawnSync("git", ["rev-parse", "-q", "--verify", `refs/tags/${tag}`], { + stdio: "ignore", + }).status === 0 + ); +} + +function fetchRemoteTags(tagsToCheck) { + const result = spawnSync( + "git", + [ + "ls-remote", + "--tags", + "origin", + ...tagsToCheck.map((t) => `refs/tags/${t}`), + ], + { encoding: "utf8" }, + ); + + const existing = new Set(); + if (result.status === 0 && result.stdout) { + for (const line of result.stdout.trim().split("\n")) { + const ref = line.split("\t")[1]; + if (ref) { + existing.add(ref.replace("refs/tags/", "")); + } + } + } + return existing; +} diff --git a/scripts/release/should-publish-canary.mjs b/scripts/release/should-publish-canary.mjs new file mode 100644 index 000000000..e3ad9a198 --- /dev/null +++ b/scripts/release/should-publish-canary.mjs @@ -0,0 +1,174 @@ +import { execFileSync } from "node:child_process"; +import { readFileSync } from "node:fs"; + +import { + NPM_REGISTRY, + appendSummary, + filterPublishableReleases, + parseArgs, + writeGithubOutput, +} from "./_shared.mjs"; + +/** + * Checks whether a canary publish is needed for the current HEAD commit. + * + * 1. Filters publishable packages with pending changesets. + * 2. For each, queries the npm registry for the `canary` dist-tag version. + * If that version already ends with the current short commit hash, the + * package is considered already published. + * 3. If any package still needs publishing, verifies the latest CI run + * on the target branch succeeded before allowing the publish. + * + * Outputs `should_publish=false` when no publish is needed or CI has not + * passed, so the workflow can skip downstream steps. + */ + +const CI_WORKFLOW_FILE = "checks.yaml"; + +const args = parseArgs(); +const statusPath = args["status-file"] ?? ".changeset-status.json"; +const branch = args.branch ?? "main"; + +const commitHash = execFileSync("git", ["rev-parse", "--short", "HEAD"], { + encoding: "utf8", +}).trim(); + +console.log(`Current HEAD commit: ${commitHash}`); + +const status = JSON.parse(readFileSync(statusPath, "utf8")); +const releases = filterPublishableReleases(status); + +if (releases.length === 0) { + console.log("No publishable packages have pending changesets."); + writeGithubOutput("should_publish", "false"); + appendSummary( + "## Canary check\n\nNo pending changesets — nothing to publish.", + ); + process.exit(0); +} + +console.log(`Checking canary dist-tags for ${releases.length} package(s)...\n`); + +let allAlreadyPublished = true; +const results = []; + +for (const release of releases) { + let canaryVersion = null; + try { + canaryVersion = execFileSync( + "npm", + ["view", release.name, "dist-tags.canary", "--registry", NPM_REGISTRY], + { encoding: "utf8", stdio: ["pipe", "pipe", "pipe"] }, + ).trim(); + } catch { + // Package has no canary dist-tag or doesn't exist on npm yet. + } + + const alreadyPublished = + canaryVersion != null && + canaryVersion !== "" && + canaryVersion.endsWith(`.${commitHash}`); + + if (!alreadyPublished) { + allAlreadyPublished = false; + } + + results.push({ + name: release.name, + canaryVersion: canaryVersion || "(none)", + alreadyPublished, + }); +} + +for (const r of results) { + const label = r.alreadyPublished ? "✓ already published" : "✗ needs publish"; + console.log(` ${r.name}: ${label} (canary: ${r.canaryVersion})`); +} + +if (allAlreadyPublished) { + writeGithubOutput("should_publish", "false"); + const list = results.map((r) => `- ${r.name}@${r.canaryVersion}`).join("\n"); + appendSummary( + `## Canary check\n\nAll packages already have canary for commit \`${commitHash}\`:\n${list}\n\nSkipping publish.`, + ); + console.log( + `\nAll packages already have canary for commit ${commitHash}. Skipping.`, + ); + process.exit(0); +} + +// Verify the latest CI run on the target branch succeeded before publishing. +const ciResult = await checkCiStatus(branch); + +if (!ciResult.passed) { + writeGithubOutput("should_publish", "false"); + appendSummary( + `## Canary check\n\nCanary publish skipped — CI gate failed.\n\n${ciResult.reason}`, + ); + console.log(`\nCanary publish skipped: ${ciResult.reason}`); + process.exit(0); +} + +console.log(`\nCI gate passed: ${ciResult.reason}`); + +writeGithubOutput("should_publish", "true"); +const list = results + .filter((r) => !r.alreadyPublished) + .map((r) => `- ${r.name} (current canary: ${r.canaryVersion})`) + .join("\n"); +appendSummary( + `## Canary check\n\nNew canary needed for commit \`${commitHash}\`:\n${list}`, +); +console.log(`\nCanary publish needed for commit ${commitHash}.`); + +async function checkCiStatus(targetBranch) { + const token = process.env.GITHUB_TOKEN; + const repository = process.env.GITHUB_REPOSITORY; + + if (!token || !repository) { + console.log( + "GITHUB_TOKEN or GITHUB_REPOSITORY not set — skipping CI status check.", + ); + return { passed: true, reason: "CI check skipped (no credentials)." }; + } + + const url = `https://api.github.com/repos/${repository}/actions/workflows/${CI_WORKFLOW_FILE}/runs?branch=${encodeURIComponent(targetBranch)}&status=completed&per_page=1`; + + const response = await fetch(url, { + headers: { + Accept: "application/vnd.github+json", + Authorization: `Bearer ${token}`, + "X-GitHub-Api-Version": "2022-11-28", + }, + }); + + if (!response.ok) { + const body = await response.text(); + return { + passed: false, + reason: `Failed to query CI status: ${response.status} ${body}`, + }; + } + + const data = await response.json(); + const run = data.workflow_runs?.[0]; + + if (!run) { + return { + passed: false, + reason: `No completed ${CI_WORKFLOW_FILE} run found on \`${targetBranch}\`.`, + }; + } + + if (run.conclusion !== "success") { + return { + passed: false, + reason: `Latest ${CI_WORKFLOW_FILE} run on \`${targetBranch}\` concluded with \`${run.conclusion}\` (${run.html_url}).`, + }; + } + + return { + passed: true, + reason: `Latest ${CI_WORKFLOW_FILE} run on \`${targetBranch}\` succeeded (${run.html_url}).`, + }; +} diff --git a/scripts/release/should-publish-stable.mjs b/scripts/release/should-publish-stable.mjs new file mode 100644 index 000000000..fe7a94670 --- /dev/null +++ b/scripts/release/should-publish-stable.mjs @@ -0,0 +1,98 @@ +import { execFileSync, spawnSync } from "node:child_process"; +import { writeFileSync } from "node:fs"; +import os from "node:os"; + +import { + NPM_REGISTRY, + PUBLISHABLE_PACKAGES, + appendSummary, + getReleaseTag, + parseArgs, + readPackage, + writeGithubOutput, +} from "./_shared.mjs"; + +const args = parseArgs(); +const outputPath = args.output ?? ".release-manifest.json"; +const packages = PUBLISHABLE_PACKAGES.map((approved) => { + const manifest = readPackage(approved.dir); + const publishedToNpm = isPublishedToNpm(manifest.name, manifest.version); + + return { + dir: approved.dir, + name: manifest.name, + version: manifest.version, + tag: getReleaseTag(manifest.name, manifest.version), + publishedToNpm, + needsPublish: !publishedToNpm, + needsTagPush: !publishedToNpm, + needsGithubRelease: !publishedToNpm, + }; +}); + +const actionablePackages = packages.filter((pkg) => pkg.needsPublish); +const commit = execFileSync("git", ["rev-parse", "HEAD"], { + encoding: "utf8", +}).trim(); +const commitMessage = execFileSync("git", ["log", "-1", "--pretty=%B"], { + encoding: "utf8", +}).trim(); +const isReleaseCommit = /\[ci\] release/i.test(commitMessage); +const hasWork = actionablePackages.length > 0 && isReleaseCommit; +const releasePackages = hasWork ? actionablePackages : []; + +const manifest = { + mode: "stable", + commit, + packages: releasePackages, +}; + +writeFileSync(outputPath, `${JSON.stringify(manifest, null, 2)}\n`, "utf8"); + +const needsPublish = releasePackages.some((pkg) => pkg.needsPublish); +const needsTags = releasePackages.some((pkg) => pkg.needsTagPush); +const needsGithubReleases = releasePackages.some( + (pkg) => pkg.needsGithubRelease, +); + +writeGithubOutput("has_work", hasWork); +writeGithubOutput("needs_publish", needsPublish); +writeGithubOutput("needs_tags", needsTags); +writeGithubOutput("needs_github_releases", needsGithubReleases); +writeGithubOutput("package_count", releasePackages.length); +writeGithubOutput("manifest_path", outputPath); + +if (!hasWork) { + const message = actionablePackages.length + ? "Unpublished package versions exist, but HEAD is not a merged Changesets release commit, so stable publish is skipped." + : "No stable publish work is required on this main commit."; + console.log(message); + appendSummary(`## Stable publish\n\n${message}`); + process.exit(0); +} + +const list = releasePackages + .map((pkg) => `- ${pkg.name}@${pkg.version}`) + .join("\n"); + +console.log( + `Stable release work detected for ${releasePackages.length} package(s):\n${list}`, +); +appendSummary(`## Stable publish work detected\n\n${list}`); + +function isPublishedToNpm(name, version) { + const result = spawnSync( + "npm", + ["view", `${name}@${version}`, "version", "--registry", NPM_REGISTRY], + { + cwd: os.tmpdir(), + encoding: "utf8", + }, + ); + + if (result.status !== 0) { + return false; + } + + return result.stdout.trim() === version; +} diff --git a/scripts/release/summarize-release.mjs b/scripts/release/summarize-release.mjs new file mode 100644 index 000000000..061462bd0 --- /dev/null +++ b/scripts/release/summarize-release.mjs @@ -0,0 +1,46 @@ +import { readFileSync } from "node:fs"; + +import { appendSummary, parseArgs, writeGithubOutput } from "./_shared.mjs"; + +const args = parseArgs(); +const mode = args.mode ?? "release"; +const manifestPath = args.manifest ?? ".release-manifest.json"; +const manifest = JSON.parse(readFileSync(manifestPath, "utf8")); +const packages = manifest.packages ?? []; + +const title = getTitle(mode); +const markdownList = + packages.length === 0 + ? "- none" + : packages.map((pkg) => `- ${pkg.name}@${pkg.version}`).join("\n"); +const plainList = + packages.length === 0 + ? "none" + : packages.map((pkg) => `${pkg.name}@${pkg.version}`).join(", "); + +appendSummary(`## ${title}\n\n${markdownList}`); +writeGithubOutput("count", packages.length); +writeGithubOutput("markdown", markdownList); +writeGithubOutput("plain", plainList); +writeGithubOutput("title", title); + +console.log(`${title}: ${plainList}`); + +function getTitle(currentMode) { + switch (currentMode) { + case "stable": + return "Stable release"; + case "prerelease": + return "Prerelease snapshot"; + case "canary": + return "Canary snapshot"; + case "dry-run-stable": + return "Stable dry run"; + case "dry-run-prerelease": + return "Prerelease dry run"; + case "dry-run-canary": + return "Canary dry run"; + default: + return "Release"; + } +} diff --git a/scripts/release/validate-publishable-packages.mjs b/scripts/release/validate-publishable-packages.mjs new file mode 100644 index 000000000..5af8661df --- /dev/null +++ b/scripts/release/validate-publishable-packages.mjs @@ -0,0 +1,128 @@ +import { + DOCS_HOMEPAGE, + GITHUB_REPO_URL, + PRIVATE_WORKSPACE_PACKAGES, + PUBLISHABLE_PACKAGES, + listWorkspacePackageDirs, + readPackage, +} from "./_shared.mjs"; + +const errors = []; +const warnings = []; + +const workspaceDirs = new Set(listWorkspacePackageDirs()); +const approvedPublishableDirs = new Set( + PUBLISHABLE_PACKAGES.map((pkg) => pkg.dir), +); +const approvedPrivateDirs = new Set( + PRIVATE_WORKSPACE_PACKAGES.map((pkg) => pkg.dir), +); + +for (const expected of [ + ...PUBLISHABLE_PACKAGES, + ...PRIVATE_WORKSPACE_PACKAGES, +]) { + if (!workspaceDirs.has(expected.dir)) { + errors.push( + `${expected.dir} is missing from the pnpm workspace discovery set derived from pnpm-workspace.yaml`, + ); + } +} + +for (const workspaceDir of workspaceDirs) { + const manifest = readPackage(workspaceDir); + + if (approvedPublishableDirs.has(workspaceDir)) { + validatePublishablePackage(workspaceDir, manifest); + continue; + } + + if (approvedPrivateDirs.has(workspaceDir)) { + if (manifest.private !== true) { + errors.push(`${workspaceDir} (${manifest.name}) must remain private`); + } + continue; + } + + if (manifest.private !== true) { + errors.push( + `${workspaceDir} (${manifest.name}) is a workspace package but is not on the publish allowlist and is not private`, + ); + } +} + +if (errors.length > 0) { + console.error("Publishable package validation failed:\n"); + for (const error of errors) { + console.error(`- ${error}`); + } + if (warnings.length > 0) { + console.error("\nWarnings:"); + for (const warning of warnings) { + console.error(`- ${warning}`); + } + } + process.exit(1); +} + +console.log( + `Validated ${PUBLISHABLE_PACKAGES.length} publishable packages and ${PRIVATE_WORKSPACE_PACKAGES.length} private workspace packages.`, +); +if (warnings.length > 0) { + console.log("Warnings:"); + for (const warning of warnings) { + console.log(`- ${warning}`); + } +} + +function validatePublishablePackage(workspaceDir, manifest) { + const approved = PUBLISHABLE_PACKAGES.find((pkg) => pkg.dir === workspaceDir); + if (!approved) { + errors.push(`No publishable package mapping found for ${workspaceDir}`); + return; + } + + if (manifest.name !== approved.name) { + errors.push( + `${workspaceDir} has package name ${manifest.name}, expected ${approved.name}`, + ); + } + + if (manifest.private === true) { + errors.push(`${workspaceDir} (${manifest.name}) must not be private`); + } + + if (manifest.publishConfig?.access !== "public") { + errors.push( + `${workspaceDir} (${manifest.name}) must set publishConfig.access to public`, + ); + } + + if (manifest.publishConfig?.registry !== "https://registry.npmjs.org/") { + errors.push( + `${workspaceDir} (${manifest.name}) must set publishConfig.registry to https://registry.npmjs.org/`, + ); + } + + if (manifest.repository?.url !== GITHUB_REPO_URL) { + errors.push( + `${workspaceDir} (${manifest.name}) must point repository.url at ${GITHUB_REPO_URL}`, + ); + } + + if (manifest.repository?.directory !== workspaceDir) { + errors.push( + `${workspaceDir} (${manifest.name}) must set repository.directory to ${workspaceDir}`, + ); + } + + if (manifest.homepage !== DOCS_HOMEPAGE) { + warnings.push( + `${workspaceDir} (${manifest.name}) homepage should be ${DOCS_HOMEPAGE}`, + ); + } + + if (!manifest.license) { + errors.push(`${workspaceDir} (${manifest.name}) must declare a license`); + } +} From 2f56f8f586226c0ef6e083ee7db9b91881a6e45e Mon Sep 17 00:00:00 2001 From: Abhijeet Prasad Date: Fri, 3 Apr 2026 22:53:56 -0400 Subject: [PATCH 2/2] fix: remove unused exports from _shared.mjs to fix knip dead-code check --- scripts/release/_shared.mjs | 23 +++++------------------ 1 file changed, 5 insertions(+), 18 deletions(-) diff --git a/scripts/release/_shared.mjs b/scripts/release/_shared.mjs index 2ab83769b..2ad2f17bb 100644 --- a/scripts/release/_shared.mjs +++ b/scripts/release/_shared.mjs @@ -4,7 +4,7 @@ import { fileURLToPath } from "node:url"; const __dirname = path.dirname(fileURLToPath(import.meta.url)); -export const REPO_ROOT = path.resolve(__dirname, "../.."); +const REPO_ROOT = path.resolve(__dirname, "../.."); export const NPM_REGISTRY = "https://registry.npmjs.org/"; export const GITHUB_REPO_URL = "git+https://github.com/braintrustdata/braintrust-sdk-javascript.git"; @@ -39,14 +39,9 @@ export const PRIVATE_WORKSPACE_PACKAGES = [ { dir: "e2e", name: "@braintrust/js-e2e-tests" }, ]; -export const PUBLISHABLE_PACKAGE_NAMES = PUBLISHABLE_PACKAGES.map( - (pkg) => pkg.name, -); -export const PUBLISHABLE_PACKAGE_DIRS = new Set( - PUBLISHABLE_PACKAGES.map((pkg) => pkg.dir), -); -export const PUBLISHABLE_PACKAGE_NAME_SET = new Set(PUBLISHABLE_PACKAGE_NAMES); -export const PUBLISHABLE_PACKAGE_MAP = new Map( +const PUBLISHABLE_PACKAGE_NAMES = PUBLISHABLE_PACKAGES.map((pkg) => pkg.name); +const PUBLISHABLE_PACKAGE_NAME_SET = new Set(PUBLISHABLE_PACKAGE_NAMES); +const PUBLISHABLE_PACKAGE_MAP = new Map( PUBLISHABLE_PACKAGES.map((pkg) => [pkg.name, pkg]), ); @@ -54,7 +49,7 @@ export function repoPath(...segments) { return path.join(REPO_ROOT, ...segments); } -export function readJson(relativePath) { +function readJson(relativePath) { return JSON.parse(readFileSync(repoPath(relativePath), "utf8")); } @@ -68,18 +63,10 @@ export function readPackage(relativeDir) { }; } -export function getApprovedPackage(relativeDir) { - return PUBLISHABLE_PACKAGES.find((pkg) => pkg.dir === relativeDir); -} - export function getApprovedPackageByName(name) { return PUBLISHABLE_PACKAGE_MAP.get(name); } -export function toPosixPath(filePath) { - return filePath.split(path.sep).join(path.posix.sep); -} - export function escapeRegExp(value) { return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); }