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/.changeset/little-webs-hang.md b/.changeset/little-webs-hang.md new file mode 100644 index 000000000..d0b1d5faa --- /dev/null +++ b/.changeset/little-webs-hang.md @@ -0,0 +1,21 @@ +--- +"braintrust": minor +--- + +- feat: Add instrumentation for @huggingface/inference (#1807) +- feat: Add cohere-ai instrumentation (#1781) +- fix: Capture anthropic server tool use inputs for streaming APIs (#1776) +- feat: Capture grounding metadata for Google GenAI (#1773) +- fix(claude-agent-sdk): Don't drop tool spans for spawning subagents (#1779) +- feat: Track server tool use metrics for anthropic SDK (#1772) +- fix(openai): Collect logprob and refulsals output for streaming APIs (#1774) +- perf: Remove zod from deepCopyEvent (#1796) +- fix(test): Double timeout for slow OpenAI API tests (#1794) +- feat(claude-agent-sdk): Improve task lifecycle and lifecycle details (#1777) +- ci(deps): bump actions/github-script from 8.0.0 to 9.0.0 (#1783) +- ci(deps): bump docker/setup-buildx-action from 3.12.0 to 4.0.0 (#1782) +- chore: Don't use environment (ie. github deployments) for canary tests (#1775) +- chore: Make dependabot less annoying (#1778) +- fix(auto-instrumentation): Upgrade @apm-js-collab/code-transformer to v0.12.0 (#1708) +- fix(auto-instrumentation): Use sync channel for AI SDK CJS streamText/streamObject in v4+ (#1768) +- fix: Give AI SDK top-level api spans type function (#1769) diff --git a/.github/workflows/_run-js-release-mode.yaml b/.github/workflows/_run-js-release-mode.yaml new file mode 100644 index 000000000..e6db0f542 --- /dev/null +++ b/.github/workflows/_run-js-release-mode.yaml @@ -0,0 +1,159 @@ +name: Run JS release mode + +on: + workflow_call: + inputs: + mode: + required: true + type: string + checkout_ref: + required: true + type: string + version_command: + required: true + type: string + publish_enabled: + required: false + default: false + type: boolean + publish_command: + required: false + default: "" + type: string + artifact_dir: + required: false + default: "" + type: string + artifact_name: + required: false + default: "" + type: string + slack_text: + required: false + default: "" + type: string + slack_header: + required: false + default: "" + type: string + slack_details: + required: false + default: "" + type: string + run_canary_check: + required: false + default: false + type: boolean + +jobs: + run: + 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 + ref: ${{ inputs.checkout_ref }} + - 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 release status + id: status_check + run: node scripts/release/check-changeset-status.mjs --mode ${{ inputs.mode }} --status-file .changeset-status.json + - name: Check if canary already published for HEAD + if: inputs.run_canary_check && steps.status_check.outputs.has_packages == 'true' + id: canary_check + env: + GITHUB_TOKEN: ${{ github.token }} + run: node scripts/release/should-publish-canary.mjs --status-file .changeset-status.json + - name: Decide whether to continue + id: plan + env: + HAS_PACKAGES: ${{ steps.status_check.outputs.has_packages }} + RUN_CANARY_CHECK: ${{ inputs.run_canary_check && 'true' || 'false' }} + CANARY_SHOULD_PUBLISH: ${{ steps.canary_check.outputs.should_publish }} + run: | + should_run="$HAS_PACKAGES" + reason="Packages are available." + + if [ "$HAS_PACKAGES" != "true" ]; then + should_run=false + reason="No publishable packages found." + elif [ "$RUN_CANARY_CHECK" = "true" ] && [ "$CANARY_SHOULD_PUBLISH" != "true" ]; then + should_run=false + reason="Canary already published for HEAD." + fi + + echo "should_run=$should_run" >> "$GITHUB_OUTPUT" + + { + echo "## Release mode" + echo + echo '- Mode: `${{ inputs.mode }}`' + echo '- Ref: `${{ inputs.checkout_ref }}`' + echo "- Decision: $reason" + } >> "$GITHUB_STEP_SUMMARY" + - name: Version packages + if: steps.plan.outputs.should_run == 'true' + run: ${{ inputs.version_command }} + - name: Prepare release manifest + if: steps.plan.outputs.should_run == 'true' + run: node scripts/release/prepare-release-manifest.mjs --mode ${{ inputs.mode }} --status-file .changeset-status.json --output .release-manifest.json + - name: Build publishable packages + if: steps.plan.outputs.should_run == 'true' + run: bash scripts/release/build-publishable-packages.sh .release-manifest.json + - name: Publish packages + if: steps.plan.outputs.should_run == 'true' && inputs.publish_enabled + run: ${{ inputs.publish_command }} + env: + NPM_TOKEN: "" + - name: Summarize release + if: steps.plan.outputs.should_run == 'true' + id: summary + run: node scripts/release/summarize-release.mjs --mode ${{ inputs.mode }} --manifest .release-manifest.json + - name: Pack publishable packages + if: steps.plan.outputs.should_run == 'true' && inputs.artifact_dir != '' + run: node scripts/release/pack-publishable-packages.mjs --manifest .release-manifest.json --output-dir ${{ inputs.artifact_dir }} + - name: Upload dry-run artifacts + if: steps.plan.outputs.should_run == 'true' && inputs.artifact_dir != '' && inputs.artifact_name != '' + uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 + with: + name: ${{ inputs.artifact_name }} + path: ${{ inputs.artifact_dir }} + retention-days: 5 + - name: Post release to Slack + if: steps.plan.outputs.should_run == 'true' && inputs.slack_text != '' && inputs.slack_header != '' + uses: slackapi/slack-github-action@af78098f536edbc4de71162a307590698245be95 # v3.0.1 + with: + method: chat.postMessage + token: ${{ secrets.SLACK_BOT_TOKEN }} + payload: | + channel: C0ABHT0SWA2 + text: "${{ inputs.slack_text }}" + blocks: + - type: "header" + text: + type: "plain_text" + text: "${{ inputs.slack_header }}" + - type: "section" + text: + type: "mrkdwn" + text: | + ${{ inputs.slack_details }} + *Packages:* + ${{ steps.summary.outputs.markdown }} + + <${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}|View Run> diff --git a/.github/workflows/checks.yaml b/.github/workflows/checks.yaml index 8806ec372..3adf700fa 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 index 100939580..3edfad4be 100644 --- a/.github/workflows/publish-js-sdk-canary-scheduler.yaml +++ b/.github/workflows/publish-js-sdk-canary-scheduler.yaml @@ -1,47 +1,45 @@ -# 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: Publish JS SDK Canary Scheduler -name: Schedule JavaScript SDK Canary Publish +on: + schedule: + - cron: "0 4 * * *" # Nightly at 04:00 UTC + workflow_dispatch: + +permissions: + actions: write + contents: read concurrency: group: publish-js-sdk-canary-scheduler cancel-in-progress: false -on: - schedule: - - cron: "17 6 * * *" - workflow_dispatch: - jobs: - dispatch-canary-publish: + dispatch-canary: runs-on: ubuntu-latest timeout-minutes: 5 - permissions: - actions: write steps: - - name: Dispatch publish workflow + - name: Dispatch canary publish workflow uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 with: script: | + const { owner, repo } = context.repo; + const ref = context.payload.repository.default_branch; + await github.rest.actions.createWorkflowDispatch({ - owner: context.repo.owner, - repo: context.repo.repo, - workflow_id: "publish-js-sdk.yaml", - ref: "main", + owner, + repo, + workflow_id: 'publish-js-sdk.yaml', + ref, inputs: { - release_type: "canary", - branch: "main", + release_mode: 'canary', }, }); - - 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" + core.summary + .addHeading('Dispatched JS canary publish') + .addList([ + `Workflow: publish-js-sdk.yaml`, + `Mode: canary`, + `Ref: ${ref}`, + ]) + .write(); diff --git a/.github/workflows/publish-js-sdk.yaml b/.github/workflows/publish-js-sdk.yaml index db9834fed..8db2c26e0 100644 --- a/.github/workflows/publish-js-sdk.yaml +++ b/.github/workflows/publish-js-sdk.yaml @@ -1,534 +1,276 @@ -# -# 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 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' && format('stable-{0}', github.ref_name) || format('{0}-{1}', inputs.release_mode, inputs.branch || inputs.ref || github.ref_name) }} + 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@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.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@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.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' - 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" - blocks: - - type: "header" - text: - type: "plain_text" - text: "โœ… JavaScript SDK 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>" - - - name: Post to Slack on success (pre-release) - if: needs.prepare-release.outputs.release_type == 'prerelease' - 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" - blocks: - - type: "header" - text: - type: "plain_text" - text: "๐Ÿงช JavaScript SDK Pre-release 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>" - - - name: Post to Slack on success (canary) - if: needs.prepare-release.outputs.release_type == 'canary' + 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 canary ${{ needs.publish-canary.outputs.version }} published: https://www.npmjs.com/package/braintrust/v/${{ needs.publish-canary.outputs.version }}" + text: "โœ… JavaScript packages published" blocks: - type: "header" text: type: "plain_text" - text: "๐Ÿงช JavaScript SDK Canary Published" + text: "โœ… JavaScript packages 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: "*Packages:*\n${{ steps.summary.outputs.markdown }}\n\n<${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}|View Run>" + + prerelease-snapshot: + if: github.event_name == 'workflow_dispatch' && inputs.release_mode == 'prerelease' + uses: ./.github/workflows/_run-js-release-mode.yaml + secrets: inherit + with: + mode: prerelease + checkout_ref: ${{ inputs.branch || github.ref_name }} + version_command: pnpm exec changeset version --snapshot rc + publish_enabled: true + publish_command: pnpm exec changeset publish --tag rc --no-git-tag + slack_text: "๐Ÿงช JavaScript prerelease snapshots published" + slack_header: "๐Ÿงช JavaScript prerelease snapshots published" + slack_details: | + *Branch:* `${{ inputs.branch || github.ref_name }}` + *Tag:* `rc` + + canary-snapshot: + if: github.event_name == 'workflow_dispatch' && inputs.release_mode == 'canary' + uses: ./.github/workflows/_run-js-release-mode.yaml + secrets: inherit + with: + mode: canary + checkout_ref: ${{ github.ref_name }} + version_command: pnpm exec changeset version --snapshot canary + publish_enabled: true + publish_command: pnpm exec changeset publish --tag canary --no-git-tag + run_canary_check: true + slack_text: "๐Ÿค JavaScript canary snapshots published" + slack_header: "๐Ÿค JavaScript canary snapshots published" + slack_details: | + *Tag:* `canary` + *Commit:* `${{ github.sha }}` + + dry-run-canary: + if: github.event_name == 'workflow_dispatch' && inputs.release_mode == 'dry-run-canary' + uses: ./.github/workflows/_run-js-release-mode.yaml + secrets: inherit + with: + mode: dry-run-canary + checkout_ref: ${{ inputs.ref || 'main' }} + version_command: pnpm exec changeset version --snapshot canary + artifact_dir: artifacts/dry-run-canary + artifact_name: canary-dry-run-${{ github.run_id }} + + dry-run-stable: + if: github.event_name == 'workflow_dispatch' && inputs.release_mode == 'dry-run-stable' + uses: ./.github/workflows/_run-js-release-mode.yaml + secrets: inherit + with: + mode: dry-run-stable + checkout_ref: ${{ inputs.ref || 'main' }} + version_command: pnpm exec changeset version && pnpm install --lockfile-only + artifact_dir: artifacts/dry-run-stable + artifact_name: stable-dry-run-${{ github.run_id }} + + dry-run-prerelease: + if: github.event_name == 'workflow_dispatch' && inputs.release_mode == 'dry-run-prerelease' + uses: ./.github/workflows/_run-js-release-mode.yaml + secrets: inherit + with: + mode: dry-run-prerelease + checkout_ref: ${{ inputs.branch || github.ref_name }} + version_command: pnpm exec changeset version --snapshot rc + artifact_dir: artifacts/dry-run-prerelease + artifact_name: prerelease-dry-run-${{ github.run_id }} 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 +282,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:* `${{ github.event_name == 'push' && 'stable-main' || inputs.release_mode }}`\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..13d84a4f1 100644 --- a/PUBLISHING_JS.md +++ b/PUBLISHING_JS.md @@ -1,107 +1,236 @@ -# JS SDK Release +# Publishing JavaScript SDK Packages -## Recommended Path +This guide explains how to release packages from this repository. -Use the [Publish JavaScript SDK workflow](https://github.com/braintrustdata/braintrust-sdk-javascript/actions/workflows/publish-js-sdk.yaml) in GitHub Actions. +**Important:** all publishing happens in **GitHub Actions**. Do **not** publish from your local machine. -This is the single npm publish entrypoint for stable releases, prereleases, and canaries. +## TL;DR -We keep all npm publishes in one workflow file because npm trusted publishing only allows one configured GitHub Actions publisher per package. +- Run `pnpm changeset` in any PR that contains changes that may in any way be user facing +- Commit the generated `.changeset/*.md` file +- Merging your PR will update or create a "Release PR" +- Merging the "Release PR" will trigger a release +- Release runs need to be approved on GitHub via Deployment Approvals -Stable releases now pause at the `npm-publish` GitHub Actions environment and require approval before the publish job runs. +## Start here -## Release Types +| I want to... | What to do | +| ------------------------------------------------------ | ----------------------------------------------------------------------------------------- | +| Make my PR release something | Run `pnpm changeset` and commit the generated `.changeset/*.md` file | +| Ship a normal stable release to npm `latest` | Merge the auto-created release PR on `main`, then approve the `npm-publish` environment | +| Publish a branch build for testing | Run the **Release Packages** workflow with `release_mode=prerelease` and your branch name | +| Manually trigger a canary publish | Run the **Release Packages** workflow with `release_mode=canary` | +| Preview what would publish without actually publishing | Run the workflow with a `dry-run-*` mode | -### Stable release +## Creating a Changeset -- 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 +If your PR changes a publishable package, it usually needs a changeset. -### Prerelease +```bash +pnpm changeset +``` -- 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 +That command will ask: -### Canary +1. which package(s) changed +2. whether the bump is `patch`, `minor`, or `major` +3. what should appear in the changelog -- 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 +Commit the generated `.changeset/*.md` file with your PR. -## Stable Release Checklist +### How to choose the bump type -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`. +| Bump | Use it for | Examples | +| ------- | -------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------- | +| `patch` | Bug fixes, internal refactors, performance work, dependency updates, docs-only API-neutral changes | Fix a crash, improve retry logic, bump a dependency | +| `minor` | New backwards-compatible functionality | Add a new method, export a new helper, add an optional parameter | +| `major` | Breaking changes | Remove or rename public API, change behavior in a way that may break users, drop Node support | -## Prerelease Steps +### Example changeset -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. +```markdown +--- +"braintrust": patch +--- -If you prerelease from a non-`main` branch, make sure that branch is in the state you intend to publish. +Fix span export when using custom headers +``` -## Nightly Canary +You can edit the generated file by hand before committing it. -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 you do not need a changeset -- 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`. +You usually do **not** need one for docs-only, test-only, or CI-only changes. -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. +If your PR touches a publishable package but does not contain any potentially user-facing or user-impacting changes, bypass the check by using one of these: -## Fallback CLI Trigger +- add the `skip-changeset` label to the PR +- include `#skip-changeset` in the PR title or body -If you do not want to open GitHub Actions manually, you can dispatch the same workflow from the terminal: +--- -```bash -./js/scripts/dispatch-release-workflow.sh +## Stable release (`latest`) + +This is the normal production release flow. + +### The important mental model + +A stable release is a **two-step process**: + +```text +feature PR with changeset +โ†’ merge to main +โ†’ automation opens or updates a release PR +โ†’ merge the release PR +โ†’ approve npm-publish +โ†’ packages publish to npm +``` + +**Merging your feature PR does not publish anything by itself.** +It only feeds changes into the next release PR. + +### What to do + +1. Merge your PR with its changeset into `main`. +2. Wait for GitHub Actions to create or update the release PR. +3. Review the release PR: + - are the package bumps correct? + - do the changelog entries read well? +4. Merge the release PR. +5. Open the workflow run and approve the `npm-publish` environment when prompted. +6. The workflow publishes to npm, pushes release tags, and creates GitHub Releases. + +### What stable release creates + +- npm publishes on the `latest` dist-tag +- Changesets package tags such as `braintrust@3.8.0` +- GitHub Releases + +## Canary release (`canary`) + +Canaries are nightly snapshots from `main`. + +Use them when someone wants the newest merged JS SDK code without waiting for the next stable release. + +### How it works + +```text +nightly scheduler workflow dispatches canary publish on main +โ†’ check for pending changesets +โ†’ check whether HEAD already has a canary +โ†’ check whether latest CI passed +โ†’ publish @canary if needed ``` -To target a different remote branch: +### Behavior + +- runs automatically every night at **04:00 UTC** +- can also be triggered manually with `release_mode=canary` +- skips if there are no pending changesets +- skips if the current `main` commit already has a canary +- skips if the latest required CI run on `main` did not succeed + +### Install a canary ```bash -BRANCH= ./js/scripts/dispatch-release-workflow.sh +npm install braintrust@canary +npm install @braintrust/otel@canary +``` + +### Canary version format + +```text +1.2.3-canary.20260404040000.abc1234 ``` -To dispatch a prerelease or canary instead of a stable release: +That includes: + +- the base version +- a timestamp +- the short commit hash + +### What canaries do not do + +- do not create git tags +- do not create GitHub Releases +- do not commit version changes back to the repo +- do not need manual environment approval + +## Prerelease from a branch (`rc`) + +Use a prerelease when you want to publish a test build from a branch before merging to `main`. + +### What to do + +1. Open **Actions** in GitHub. +2. Open the **Release Packages** workflow. +3. Click **Run workflow**. +4. Set: + - `release_mode=prerelease` + - `branch=` +5. Run the workflow. +6. After it finishes, install from the `rc` dist-tag: ```bash -RELEASE_TYPE=prerelease ./js/scripts/dispatch-release-workflow.sh -RELEASE_TYPE=canary ./js/scripts/dispatch-release-workflow.sh +npm install braintrust@rc ``` -Notes: +### Prerelease version format + +```text +1.2.3-rc.20260414104840.abcdef1234567890abcdef1234567890abcdef12 +``` + +That includes: + +- the next base version +- a timestamp +- the commit SHA + +### What prereleases do not do + +- do not create git tags +- do not create GitHub Releases +- do not commit version changes back to the repo +- do not publish packages that have no releasable changesets on that branch + +## Dry run + +Use a dry run when you want to answer: **what would publish if I ran this for real?** + +Available modes: + +| Mode | Simulates | +| -------------------- | ------------------------- | +| `dry-run-stable` | stable release from a ref | +| `dry-run-prerelease` | prerelease from a branch | +| `dry-run-canary` | canary publish from a ref | + +Dry runs will: + +- compute versions +- build the publishable packages +- create tarball artifacts +- show a summary in the workflow output + +Dry runs will **not**: + +- publish to npm +- create tags +- create GitHub Releases +- commit anything back to the repo + +## FAQ + +### Can I manually trigger a stable release from the workflow dispatch UI? + +No. Stable release publishing happens from the push-to-`main` flow around the release PR. + +### Can I publish a test build from a feature branch? -- 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`. +Yes. Use `release_mode=prerelease` and a specific branch ref. -## Repository Setup +### What is the difference between canary and prerelease? -Configure the `npm-publish` environment in GitHub repository settings with the required reviewers who are allowed to approve stable releases. +- **canary**: automated or manual snapshot from `main`, published to `canary` +- **prerelease**: manual snapshot from any branch, published to `rc` 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 bf935e93d..fb3d2cb34 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", @@ -230,5 +229,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 cc117a190..d7a986915 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(encoding@0.1.13) + '@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) @@ -1099,6 +1105,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'} @@ -1841,6 +1908,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'} @@ -2073,6 +2149,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==} + '@mikro-orm/core@6.6.13': resolution: {integrity: sha512-Zf00ZCUV1/fTCE60jJUDDbFb6dDYaojUWr0yoavNYJaFX+qoLdgKSj3tX6j2v//cGKfb/sLqs72FEEtSwvhviA==} engines: {node: '>= 18.12.0'} @@ -2924,6 +3006,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==} @@ -3329,6 +3414,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'} @@ -3481,6 +3570,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==} @@ -3631,6 +3724,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==} @@ -3851,6 +3947,9 @@ packages: resolution: {integrity: sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==} engines: {node: '>= 12'} + dataloader@1.4.0: + resolution: {integrity: sha512-68s5jYdlvasItOJnCuI2Q9s4q98g0pCyL3HrcKJu8KNugUl8ahgmZYg38ysLTgQjjXX3H8CJLkAvWrclWfcalw==} + dataloader@2.2.3: resolution: {integrity: sha512-y2krtASINtPFS1rSDjacrFgn1dcUuoREVabwlOGOe4SdxenREqwjwjElAdwvbGM7kgZz9a3KVicWR7vcz8rnzA==} @@ -3950,6 +4049,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-libc@2.1.2: resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==} engines: {node: '>=8'} @@ -3981,6 +4084,10 @@ packages: resolution: {integrity: sha512-IO8C/dzEb6O3F9/twg6ZLXz164a2fhTnEWb95H23Dm4OuN+92NmEAlTrupP9VW6Jm3sO26tQlqyvyi4CsnY9GA==} 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'} @@ -4034,6 +4141,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'} @@ -4280,6 +4391,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==} @@ -4439,6 +4553,14 @@ packages: resolution: {integrity: sha512-VWSRii4t0AFm6ixFFmLLx1t7wS1gh+ckoa84aOeapGum0h+EZd1EhEumSB+ZdDLnEPuucsVB9oB7cxJHap6Afg==} engines: {node: '>=14.14'} + 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-minipass@2.1.0: resolution: {integrity: sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==} engines: {node: '>= 8'} @@ -4678,6 +4800,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'} @@ -4826,6 +4952,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'} + is-wsl@3.1.1: resolution: {integrity: sha512-e6rvdUCiQCAuumZslxRJWR/Doq4VpPR82kqclvcS0efgt430SlGIk05vdCN58+VrzgtIcfNODjozVielycD4Sw==} engines: {node: '>=16'} @@ -5080,6 +5214,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==} @@ -5247,6 +5384,9 @@ packages: lodash.sortby@4.7.0: resolution: {integrity: sha512-HDWXG8isMntAyRF5vZ7xKuEvOhT4AhlRt/3czTSjvGUxjYCBVRQY48ViDHyfYz9VIoBkW4TMGQNapx+l3RUwdA==} + lodash.startcase@4.4.0: + resolution: {integrity: sha512-+WKqsK294HMSc2jEbNgpHpd0JfIBhp7rEV4aqXWqFr6AlXov+SlcgB1Fv01y2kGe3Gc8nMW7VA0SrGuSkRfIEg==} + lodash@4.18.1: resolution: {integrity: sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==} @@ -5491,6 +5631,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==} @@ -5732,12 +5876,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'} @@ -5758,6 +5909,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-map@4.0.0: resolution: {integrity: sha512-/bjOqmgETBYB5BoEeGVea8dmvHb2m9GLy1E9W43yeyfP6QQCZGFNa+XRceJEuDB6zqr+gKpIAmlLebMpykw/MQ==} engines: {node: '>=10'} @@ -5781,6 +5936,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'} @@ -5901,6 +6059,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'} @@ -6058,6 +6220,9 @@ packages: resolution: {integrity: sha512-6YHEFRL9mfgcAvql/XhwTvf5jKcOiiupt2FiJxHkiX1z4j7WL8J/jRHYLluORvc1XxB5rV20KoeK00gVJamspg==} 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==} @@ -6099,6 +6264,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'} + readable-stream@3.6.2: resolution: {integrity: sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==} engines: {node: '>= 6'} @@ -6376,6 +6545,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==} + split2@4.2.0: resolution: {integrity: sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==} engines: {node: '>= 10.x'} @@ -6479,6 +6651,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'} @@ -6585,6 +6761,10 @@ packages: resolution: {integrity: sha512-resvxdc6Mgb7YEThw6G6bExlXKkv6+YbuzGg9xuXxSgxJF7Ozs+o8Y9+2R3sArdWdW8nOokoQb1yrpFB0pQK2g==} engines: {node: '>=14'} + 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'} @@ -6921,6 +7101,10 @@ packages: unique-slug@2.0.2: resolution: {integrity: sha512-zoWr9ObaxALD3DOPfjPSqxt4fnZiWblxHIgeWqW8x7UqDzEtHEQLzji2cuJYQFCU6KmoJikOYAZlrTHHebjx2w==} + 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'} @@ -8190,6 +8374,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(encoding@0.1.13)': + dependencies: + '@changesets/get-github-info': 0.8.0(encoding@0.1.13) + '@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(encoding@0.1.13)': + dependencies: + dataloader: 1.4.0 + node-fetch: 2.7.0(encoding@0.1.13) + 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 @@ -8496,7 +8838,7 @@ snapshots: '@eslint/eslintrc@3.3.3': dependencies: - ajv: 6.12.6 + ajv: 6.14.0 debug: 4.4.3 espree: 10.4.0 globals: 14.0.0 @@ -8835,6 +9177,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)': @@ -8860,7 +9209,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 @@ -9210,6 +9559,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 + '@mikro-orm/core@6.6.13': dependencies: dataloader: 2.2.3 @@ -9394,7 +9759,7 @@ snapshots: '@modelcontextprotocol/sdk@1.18.0': dependencies: - ajv: 6.12.6 + ajv: 6.14.0 content-type: 1.0.5 cors: 2.8.5 cross-spawn: 7.0.6 @@ -10241,6 +10606,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 @@ -10798,6 +11165,8 @@ snapshots: dependencies: string-width: 4.2.3 + ansi-colors@4.1.3: {} + ansi-escapes@4.3.2: dependencies: type-fest: 0.21.3 @@ -11018,6 +11387,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: {} @@ -11258,6 +11631,8 @@ snapshots: char-regex@1.0.2: {} + chardet@2.1.1: {} + cheminfo-types@1.8.1: {} chokidar@4.0.3: @@ -11451,6 +11826,8 @@ snapshots: data-uri-to-buffer@4.0.1: {} + dataloader@1.4.0: {} + dataloader@2.2.3: {} dc-browser@1.0.4: {} @@ -11505,6 +11882,8 @@ snapshots: destroy@1.2.0: {} + detect-indent@6.1.0: {} + detect-libc@2.1.2: {} detect-newline@3.1.0: {} @@ -11524,6 +11903,8 @@ snapshots: dotenv@17.3.1: {} + dotenv@8.6.0: {} + dunder-proto@1.0.1: dependencies: call-bind-apply-helpers: 1.0.2 @@ -11575,6 +11956,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: {} env-paths@2.2.1: @@ -12039,6 +12425,8 @@ snapshots: extend@3.0.2: {} + extendable-error@0.1.7: {} + fast-deep-equal@3.1.3: {} fast-glob@3.3.3: @@ -12211,6 +12599,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-minipass@2.1.0: dependencies: minipass: 3.3.6 @@ -12515,6 +12915,8 @@ snapshots: transitivePeerDependencies: - supports-color + human-id@4.1.3: {} + human-signals@2.1.0: {} humanize-ms@1.2.1: @@ -12623,6 +13025,12 @@ snapshots: is-stream@2.0.1: {} + is-subdir@1.2.0: + dependencies: + better-path-resolve: 1.0.0 + + is-windows@1.0.2: {} + is-wsl@3.1.1: dependencies: is-inside-container: 1.0.0 @@ -13088,6 +13496,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 @@ -13327,6 +13739,8 @@ snapshots: lodash.sortby@4.7.0: {} + lodash.startcase@4.4.0: {} + lodash@4.18.1: {} log-update@6.1.0: @@ -13590,6 +14004,8 @@ snapshots: module-details-from-path@1.0.4: {} + mri@1.2.0: {} + ms@2.0.0: {} ms@2.1.2: {} @@ -13982,6 +14398,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: @@ -14007,6 +14425,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: @@ -14025,6 +14447,8 @@ snapshots: dependencies: p-limit: 3.1.0 + p-map@2.1.0: {} + p-map@4.0.0: dependencies: aggregate-error: 3.1.0 @@ -14048,6 +14472,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 @@ -14143,6 +14571,8 @@ snapshots: pidtree@0.6.0: {} + pify@4.0.1: {} + pirates@4.0.6: {} pkce-challenge@5.0.0: {} @@ -14286,6 +14716,8 @@ snapshots: dependencies: side-channel: 1.1.0 + quansync@0.2.11: {} + querystringify@2.2.0: {} queue-microtask@1.2.3: {} @@ -14334,6 +14766,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 + readable-stream@3.6.2: dependencies: inherits: 2.0.4 @@ -14664,6 +15103,11 @@ snapshots: dependencies: whatwg-url: 7.1.0 + spawndamnit@3.0.1: + dependencies: + cross-spawn: 7.0.6 + signal-exit: 4.1.0 + split2@4.2.0: {} sprintf-js@1.0.3: {} @@ -14737,7 +15181,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: @@ -14766,6 +15210,8 @@ snapshots: dependencies: ansi-regex: 6.1.0 + strip-bom@3.0.0: {} + strip-bom@4.0.0: {} strip-final-newline@2.0.0: {} @@ -14906,6 +15352,8 @@ snapshots: - encoding - supports-color + 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)): @@ -15298,6 +15746,8 @@ snapshots: imurmurhash: 0.1.4 optional: true + universalify@0.1.2: {} + universalify@0.2.0: {} universalify@2.0.1: {} @@ -15788,7 +16238,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..2ad2f17bb --- /dev/null +++ b/scripts/release/_shared.mjs @@ -0,0 +1,194 @@ +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)); + +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" }, +]; + +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]), +); + +export function repoPath(...segments) { + return path.join(REPO_ROOT, ...segments); +} + +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 getApprovedPackageByName(name) { + return PUBLISHABLE_PACKAGE_MAP.get(name); +} + +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`); + } +}