From 60e4030ad0a9fc72e33c580b1490e65162d0fe1b Mon Sep 17 00:00:00 2001 From: Tejas Kashinath Date: Thu, 3 Sep 2026 17:14:14 -0400 Subject: [PATCH 01/11] ci: add release workflow for rc and stable npm publishing - release.yml: workflow_dispatch (bump, channel, dry_run) publishes -rc.N under the rc dist-tag after approval, or opens a release PR whose merge publishes under latest. rc numbers derive from tags, never commits. - package.json: publish as @aws/agentcore, version tracks the last stable release (0.28.1), repository field required for npm provenance. - update.test.ts: read the version from constants instead of a literal. --- .github/workflows/README.md | 14 +- .github/workflows/release.yml | 224 +++++++++++++++++++++++++++++ package.json | 8 +- src/handlers/update/update.test.ts | 9 +- 4 files changed, 240 insertions(+), 15 deletions(-) create mode 100644 .github/workflows/release.yml diff --git a/.github/workflows/README.md b/.github/workflows/README.md index ab4b6550c..b76297bfb 100644 --- a/.github/workflows/README.md +++ b/.github/workflows/README.md @@ -19,22 +19,18 @@ care what triggered it. An orchestrator calls jobs via `workflow_call`. -### Current example +### Current examples ``` ci.yml |-- check.yml (lint, format, typecheck, audit, secret scan) |-- build.yml (bundle, package, compile, smoke test) `-- unit-test.yml (tests on Linux, Windows, macOS) -``` - -### Future examples -``` -release.yml - ├── unit-test.yml - ├── build.yml - └── publish-npm.yml +release.yml (version, release PR, approval, npm publish, GitHub release) + |-- check.yml + |-- build.yml + `-- unit-test.yml ``` Jobs like `unit-test.yml` and `build.yml` appear in multiple orchestrators. This diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 000000000..23ab6545c --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,224 @@ +# Publishes @aws/agentcore to npm and creates the matching GitHub release. +# +# rc: dispatch -> version -> check/build/unit-test -> approval -> publish -rc.N under the `rc` dist-tag. +# stable: dispatch -> version -> release PR bumping package.json. Merging that PR re-enters this +# workflow, which runs check/build/unit-test and publishes under `latest`. +# +# package.json always holds the last published stable version. rc numbers come from git tags, never commits. +name: release +on: + workflow_dispatch: + inputs: + bump: + description: Semver component to bump from the last stable release + required: true + type: choice + options: [major, minor, patch] + channel: + description: rc publishes after approval, stable opens a release PR + required: true + type: choice + options: [rc, stable] + dry_run: + description: Build and pack, but skip npm publish and the GitHub release + type: boolean + default: false + pull_request: + types: [closed] + branches: [refactor] + +concurrency: + group: ${{ github.workflow }} + cancel-in-progress: false + +env: + AGENTCORE_TELEMETRY_DISABLED: "1" + +jobs: + version: + if: github.event_name == 'workflow_dispatch' || (github.event.pull_request.merged && startsWith(github.head_ref, 'release/')) + runs-on: codebuild-agentcore-e2e-${{ github.run_id }}-${{ github.run_attempt }} + permissions: + contents: read + outputs: + ref: ${{ steps.resolve.outputs.ref }} + version: ${{ steps.resolve.outputs.version }} + previous_tag: ${{ steps.resolve.outputs.previous_tag }} + channel: ${{ steps.resolve.outputs.channel }} + action: ${{ steps.resolve.outputs.action }} + steps: + - uses: actions/checkout@v7 + with: + ref: ${{ github.event.pull_request.merge_commit_sha || github.sha }} + fetch-tags: true + persist-credentials: false + - uses: oven-sh/setup-bun@v2 + - uses: actions/setup-node@v6 + with: + node-version: 20.x + - run: bun install --frozen-lockfile + + - name: Resolve version + id: resolve + env: + BUMP: ${{ inputs.bump }} + CHANNEL: ${{ inputs.channel || 'stable' }} + ACTION: ${{ github.event_name == 'workflow_dispatch' && inputs.channel == 'stable' && 'pull-request' || 'publish' }} + REF: ${{ github.event.pull_request.merge_commit_sha || github.sha }} + run: | + set -euo pipefail + if [ -n "$BUMP" ]; then + BASE=$(npm version "$BUMP" --no-git-tag-version) + else + BASE="v$(npm pkg get version | tr -d '"')" + fi + if git rev-parse -q --verify "refs/tags/$BASE" >/dev/null; then + echo "::error::$BASE is already released" + exit 1 + fi + + if [ "$CHANNEL" = rc ]; then + LAST=$(git tag -l "$BASE-rc.*" | sed 's/.*-rc\.//' | sort -n | tail -1) + VERSION="${BASE#v}-rc.$(( ${LAST:--1} + 1 ))" + else + VERSION="${BASE#v}" + fi + + # rc.1 and later diff against the previous rc. Stable and rc.0 diff against the previous stable. + SEMVER_FLAGS=() + [[ "$VERSION" == *-rc.[1-9]* ]] && SEMVER_FLAGS=(--include-prerelease) + PREVIOUS=$(git tag -l 'v*' | xargs bunx semver "${SEMVER_FLAGS[@]}" --range "<$VERSION" | tail -1) + + { + echo "ref=$REF" + echo "version=$VERSION" + echo "previous_tag=v$PREVIOUS" + echo "channel=$CHANNEL" + echo "action=$ACTION" + } >> "$GITHUB_OUTPUT" + echo "$ACTION $VERSION (notes since v$PREVIOUS)" >> "$GITHUB_STEP_SUMMARY" + + release-pr: + needs: version + if: needs.version.outputs.action == 'pull-request' + runs-on: codebuild-agentcore-e2e-${{ github.run_id }}-${{ github.run_attempt }} + permissions: + contents: read + steps: + - uses: actions/checkout@v7 + with: + ref: ${{ needs.version.outputs.ref }} + persist-credentials: false + - uses: actions/setup-node@v6 + with: + node-version: 20.x + - uses: actions/create-github-app-token@v3 + id: app-token + with: + app-id: ${{ vars.APP_ID }} + private-key: ${{ secrets.APP_PRIVATE_KEY }} + + - name: Open release PR + env: + GH_TOKEN: ${{ steps.app-token.outputs.token }} + VERSION: ${{ needs.version.outputs.version }} + PREVIOUS_TAG: ${{ needs.version.outputs.previous_tag }} + REF: ${{ needs.version.outputs.ref }} + run: | + set -euo pipefail + BRANCH="release/v$VERSION" + npm version "$VERSION" --no-git-tag-version + git config user.name "github-actions[bot]" + git config user.email "github-actions[bot]@users.noreply.github.com" + git commit -am "chore(release): v$VERSION" + git push --force "https://x-access-token:$GH_TOKEN@github.com/$GITHUB_REPOSITORY.git" "HEAD:refs/heads/$BRANCH" + + NOTES=$(gh api "repos/$GITHUB_REPOSITORY/releases/generate-notes" \ + -f tag_name="v$VERSION" -f target_commitish="$REF" -f previous_tag_name="$PREVIOUS_TAG" --jq .body) + gh pr create --base refactor --head "$BRANCH" --title "chore(release): v$VERSION" --body "Merging publishes \`@aws/agentcore@$VERSION\` to npm under \`latest\` and creates the \`v$VERSION\` release. + + $NOTES" + + check: + needs: version + if: needs.version.outputs.action == 'publish' + uses: ./.github/workflows/check.yml + permissions: + contents: read + with: + ref: ${{ needs.version.outputs.ref }} + build: + needs: version + if: needs.version.outputs.action == 'publish' + uses: ./.github/workflows/build.yml + permissions: + contents: read + with: + ref: ${{ needs.version.outputs.ref }} + unit-test: + needs: version + if: needs.version.outputs.action == 'publish' + uses: ./.github/workflows/unit-test.yml + permissions: + contents: read + with: + ref: ${{ needs.version.outputs.ref }} + + approve: + needs: [version, check, build, unit-test] + if: needs.version.outputs.channel == 'rc' + runs-on: ubuntu-latest + environment: npm-publish-approval + steps: + - run: echo "Approved ${{ needs.version.outputs.version }}" + + publish: + needs: [version, check, build, unit-test, approve] + # approve is skipped on the stable path, so a plain `needs` would skip this job too. + if: ${{ !cancelled() && needs.check.result == 'success' && needs.build.result == 'success' && needs.unit-test.result == 'success' && needs.approve.result != 'failure' }} + runs-on: codebuild-agentcore-e2e-${{ github.run_id }}-${{ github.run_attempt }} + permissions: + contents: write + id-token: write + env: + VERSION: ${{ needs.version.outputs.version }} + steps: + - uses: actions/checkout@v7 + with: + ref: ${{ needs.version.outputs.ref }} + persist-credentials: false + - uses: oven-sh/setup-bun@v2 + - uses: actions/setup-node@v6 + with: + node-version: 20.x + registry-url: https://registry.npmjs.org + - run: bun install --frozen-lockfile + + - run: npm version "$VERSION" --no-git-tag-version --allow-same-version + - run: bun run build + - id: pack + run: echo "tarball=$(bun pm pack --quiet --ignore-scripts)" >> "$GITHUB_OUTPUT" + - run: bun run compile + + - if: ${{ !inputs.dry_run }} + env: + NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} + DIST_TAG: ${{ needs.version.outputs.channel == 'rc' && 'rc' || 'latest' }} + run: npm publish "${{ steps.pack.outputs.tarball }}" --provenance --access public --tag "$DIST_TAG" + + - if: ${{ !inputs.dry_run }} + env: + GH_TOKEN: ${{ github.token }} + run: | + gh release create "v$VERSION" --target "${{ needs.version.outputs.ref }}" --title "v$VERSION" \ + --generate-notes --notes-start-tag "${{ needs.version.outputs.previous_tag }}" \ + ${{ needs.version.outputs.channel == 'rc' && '--prerelease' || '--latest' }} \ + "${{ steps.pack.outputs.tarball }}" dist/bin/* + + - if: ${{ inputs.dry_run }} + uses: actions/upload-artifact@v7 + with: + name: release-${{ env.VERSION }} + path: | + ${{ steps.pack.outputs.tarball }} + dist/bin/* diff --git a/package.json b/package.json index 3a6fac2f4..44dfd9a98 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,10 @@ { - "name": "agentcore", - "version": "1.0.0", + "name": "@aws/agentcore", + "version": "0.28.1", + "repository": { + "type": "git", + "url": "https://github.com/aws/agentcore-cli.git" + }, "module": "src/index.ts", "type": "module", "bin": { diff --git a/src/handlers/update/update.test.ts b/src/handlers/update/update.test.ts index f5126e75a..7ce889e9d 100644 --- a/src/handlers/update/update.test.ts +++ b/src/handlers/update/update.test.ts @@ -2,6 +2,7 @@ import { afterEach, describe, expect, mock, spyOn, test } from "bun:test"; import { fetchLatestVersion, handleUpdate } from "./index"; import { NetworkingError } from "../../errors"; import type { ProcessRunner } from "../../io"; +import { PACKAGE_VERSION } from "../../constants"; // No golden/fixture tests here: the repo's *.fixture.test.tsx harness records and // replays AWS SDK responses through CoreClient, but `update` makes no AWS calls — @@ -50,12 +51,12 @@ describe("handleUpdate", () => { }); test("up-to-date when versions match, without invoking the runner", async () => { - mockLatest("1.0.0"); + mockLatest(PACKAGE_VERSION); const runner: ProcessRunner = mock(async () => {}); expect(await handleUpdate(false, { runner })).toEqual({ status: "up-to-date", - currentVersion: "1.0.0", - latestVersion: "1.0.0", + currentVersion: PACKAGE_VERSION, + latestVersion: PACKAGE_VERSION, }); expect(runner).not.toHaveBeenCalled(); }); @@ -70,7 +71,7 @@ describe("handleUpdate", () => { const runner: ProcessRunner = mock(async () => {}); expect(await handleUpdate(true, { runner })).toEqual({ status: "update-available", - currentVersion: "1.0.0", + currentVersion: PACKAGE_VERSION, latestVersion: "2.0.0", }); expect(runner).not.toHaveBeenCalled(); From aadd9a69f879a5819f95530530a72e76fb540437 Mon Sep 17 00:00:00 2001 From: Tejas Kashinath Date: Thu, 3 Sep 2026 17:47:26 -0400 Subject: [PATCH 02/11] ci(release): run the approval job on the CodeBuild runner like every other job --- .github/workflows/release.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 23ab6545c..9afb9fd49 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -167,7 +167,7 @@ jobs: approve: needs: [version, check, build, unit-test] if: needs.version.outputs.channel == 'rc' - runs-on: ubuntu-latest + runs-on: codebuild-agentcore-e2e-${{ github.run_id }}-${{ github.run_attempt }} environment: npm-publish-approval steps: - run: echo "Approved ${{ needs.version.outputs.version }}" From 126ddf9864cfe8f012122a72ff74b0b172356ea4 Mon Sep 17 00:00:00 2001 From: Tejas Kashinath Date: Thu, 3 Sep 2026 17:53:58 -0400 Subject: [PATCH 03/11] ci(release): gate publish with the environment directly, use bun pm version, drop redundant setup --- .github/workflows/release.yml | 82 ++++++++++++++--------------------- 1 file changed, 33 insertions(+), 49 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 9afb9fd49..9ec6dee82 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -1,6 +1,7 @@ # Publishes @aws/agentcore to npm and creates the matching GitHub release. # -# rc: dispatch -> version -> check/build/unit-test -> approval -> publish -rc.N under the `rc` dist-tag. +# rc: dispatch -> version -> check/build/unit-test -> publish -rc.N under the `rc` dist-tag, +# gated by the npm-publish-approval environment. # stable: dispatch -> version -> release PR bumping package.json. Merging that PR re-enters this # workflow, which runs check/build/unit-test and publishes under `latest`. # @@ -25,15 +26,13 @@ on: default: false pull_request: types: [closed] + # TODO: switch to main once the refactor lands there. branches: [refactor] concurrency: group: ${{ github.workflow }} cancel-in-progress: false -env: - AGENTCORE_TELEMETRY_DISABLED: "1" - jobs: version: if: github.event_name == 'workflow_dispatch' || (github.event.pull_request.merged && startsWith(github.head_ref, 'release/')) @@ -41,11 +40,11 @@ jobs: permissions: contents: read outputs: - ref: ${{ steps.resolve.outputs.ref }} + ref: ${{ github.event.pull_request.merge_commit_sha || github.sha }} + channel: ${{ inputs.channel || 'stable' }} + action: ${{ github.event_name == 'workflow_dispatch' && inputs.channel == 'stable' && 'pull-request' || 'publish' }} version: ${{ steps.resolve.outputs.version }} previous_tag: ${{ steps.resolve.outputs.previous_tag }} - channel: ${{ steps.resolve.outputs.channel }} - action: ${{ steps.resolve.outputs.action }} steps: - uses: actions/checkout@v7 with: @@ -53,50 +52,39 @@ jobs: fetch-tags: true persist-credentials: false - uses: oven-sh/setup-bun@v2 - - uses: actions/setup-node@v6 - with: - node-version: 20.x - - run: bun install --frozen-lockfile - name: Resolve version id: resolve env: BUMP: ${{ inputs.bump }} CHANNEL: ${{ inputs.channel || 'stable' }} - ACTION: ${{ github.event_name == 'workflow_dispatch' && inputs.channel == 'stable' && 'pull-request' || 'publish' }} - REF: ${{ github.event.pull_request.merge_commit_sha || github.sha }} run: | set -euo pipefail if [ -n "$BUMP" ]; then - BASE=$(npm version "$BUMP" --no-git-tag-version) + BASE=$(bun pm version "$BUMP" --no-git-tag-version) else - BASE="v$(npm pkg get version | tr -d '"')" + BASE="v$(bun pm pkg get version | tr -d '"')" fi if git rev-parse -q --verify "refs/tags/$BASE" >/dev/null; then echo "::error::$BASE is already released" exit 1 fi + VERSION="${BASE#v}" + PREVIOUS="" if [ "$CHANNEL" = rc ]; then LAST=$(git tag -l "$BASE-rc.*" | sed 's/.*-rc\.//' | sort -n | tail -1) - VERSION="${BASE#v}-rc.$(( ${LAST:--1} + 1 ))" - else - VERSION="${BASE#v}" + VERSION="$VERSION-rc.$(( ${LAST:--1} + 1 ))" + [ -n "$LAST" ] && PREVIOUS="$BASE-rc.$LAST" fi - - # rc.1 and later diff against the previous rc. Stable and rc.0 diff against the previous stable. - SEMVER_FLAGS=() - [[ "$VERSION" == *-rc.[1-9]* ]] && SEMVER_FLAGS=(--include-prerelease) - PREVIOUS=$(git tag -l 'v*' | xargs bunx semver "${SEMVER_FLAGS[@]}" --range "<$VERSION" | tail -1) + # Stable releases and rc.0 take their notes from the previous stable release. + [ -n "$PREVIOUS" ] || PREVIOUS="v$(git tag -l 'v*' | xargs bunx semver@7 --range "<$VERSION" | tail -1)" { - echo "ref=$REF" echo "version=$VERSION" - echo "previous_tag=v$PREVIOUS" - echo "channel=$CHANNEL" - echo "action=$ACTION" + echo "previous_tag=$PREVIOUS" } >> "$GITHUB_OUTPUT" - echo "$ACTION $VERSION (notes since v$PREVIOUS)" >> "$GITHUB_STEP_SUMMARY" + echo "$VERSION (notes since $PREVIOUS)" >> "$GITHUB_STEP_SUMMARY" release-pr: needs: version @@ -109,9 +97,7 @@ jobs: with: ref: ${{ needs.version.outputs.ref }} persist-credentials: false - - uses: actions/setup-node@v6 - with: - node-version: 20.x + - uses: oven-sh/setup-bun@v2 - uses: actions/create-github-app-token@v3 id: app-token with: @@ -127,7 +113,7 @@ jobs: run: | set -euo pipefail BRANCH="release/v$VERSION" - npm version "$VERSION" --no-git-tag-version + bun pm version "$VERSION" --no-git-tag-version git config user.name "github-actions[bot]" git config user.email "github-actions[bot]@users.noreply.github.com" git commit -am "chore(release): v$VERSION" @@ -135,7 +121,7 @@ jobs: NOTES=$(gh api "repos/$GITHUB_REPOSITORY/releases/generate-notes" \ -f tag_name="v$VERSION" -f target_commitish="$REF" -f previous_tag_name="$PREVIOUS_TAG" --jq .body) - gh pr create --base refactor --head "$BRANCH" --title "chore(release): v$VERSION" --body "Merging publishes \`@aws/agentcore@$VERSION\` to npm under \`latest\` and creates the \`v$VERSION\` release. + gh pr create --base "$GITHUB_REF_NAME" --head "$BRANCH" --title "chore(release): v$VERSION" --body "Merging publishes \`@aws/agentcore@$VERSION\` to npm under \`latest\` and creates the \`v$VERSION\` release. $NOTES" @@ -164,24 +150,20 @@ jobs: with: ref: ${{ needs.version.outputs.ref }} - approve: - needs: [version, check, build, unit-test] - if: needs.version.outputs.channel == 'rc' - runs-on: codebuild-agentcore-e2e-${{ github.run_id }}-${{ github.run_attempt }} - environment: npm-publish-approval - steps: - - run: echo "Approved ${{ needs.version.outputs.version }}" - publish: - needs: [version, check, build, unit-test, approve] - # approve is skipped on the stable path, so a plain `needs` would skip this job too. - if: ${{ !cancelled() && needs.check.result == 'success' && needs.build.result == 'success' && needs.unit-test.result == 'success' && needs.approve.result != 'failure' }} + needs: [version, check, build, unit-test] runs-on: codebuild-agentcore-e2e-${{ github.run_id }}-${{ github.run_attempt }} + # npm-publish-approval requires a reviewer. npm-publish does not: merging the release PR was the approval. + environment: + name: ${{ needs.version.outputs.channel == 'rc' && 'npm-publish-approval' || 'npm-publish' }} + url: https://www.npmjs.com/package/@aws/agentcore/v/${{ needs.version.outputs.version }} permissions: contents: write id-token: write env: VERSION: ${{ needs.version.outputs.version }} + REF: ${{ needs.version.outputs.ref }} + PREVIOUS_TAG: ${{ needs.version.outputs.previous_tag }} steps: - uses: actions/checkout@v7 with: @@ -194,7 +176,7 @@ jobs: registry-url: https://registry.npmjs.org - run: bun install --frozen-lockfile - - run: npm version "$VERSION" --no-git-tag-version --allow-same-version + - run: bun pm version "$VERSION" --no-git-tag-version --allow-same-version - run: bun run build - id: pack run: echo "tarball=$(bun pm pack --quiet --ignore-scripts)" >> "$GITHUB_OUTPUT" @@ -203,17 +185,19 @@ jobs: - if: ${{ !inputs.dry_run }} env: NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} + TARBALL: ${{ steps.pack.outputs.tarball }} DIST_TAG: ${{ needs.version.outputs.channel == 'rc' && 'rc' || 'latest' }} - run: npm publish "${{ steps.pack.outputs.tarball }}" --provenance --access public --tag "$DIST_TAG" + run: npm publish "$TARBALL" --provenance --access public --tag "$DIST_TAG" - if: ${{ !inputs.dry_run }} env: GH_TOKEN: ${{ github.token }} + TARBALL: ${{ steps.pack.outputs.tarball }} run: | - gh release create "v$VERSION" --target "${{ needs.version.outputs.ref }}" --title "v$VERSION" \ - --generate-notes --notes-start-tag "${{ needs.version.outputs.previous_tag }}" \ + gh release create "v$VERSION" --target "$REF" --title "v$VERSION" \ + --generate-notes --notes-start-tag "$PREVIOUS_TAG" \ ${{ needs.version.outputs.channel == 'rc' && '--prerelease' || '--latest' }} \ - "${{ steps.pack.outputs.tarball }}" dist/bin/* + "$TARBALL" dist/bin/* - if: ${{ inputs.dry_run }} uses: actions/upload-artifact@v7 From c967ad33945469aca66d66cea0ad3493543c4fed Mon Sep 17 00:00:00 2001 From: Tejas Kashinath Date: Thu, 3 Sep 2026 18:30:19 -0400 Subject: [PATCH 04/11] fix(telemetry): accept prerelease versions in the resource attribute schema The rc dry run's binary printed its version and then exited 1: service.version was validated against ^\d+\.\d+\.\d+$, so any -rc.N build crashed at startup. --- src/telemetry/client.test.tsx | 25 +++++++++++++++++++++++++ src/telemetry/shapes.tsx | 2 +- 2 files changed, 26 insertions(+), 1 deletion(-) diff --git a/src/telemetry/client.test.tsx b/src/telemetry/client.test.tsx index a2fca90bf..c7c2c559d 100644 --- a/src/telemetry/client.test.tsx +++ b/src/telemetry/client.test.tsx @@ -10,6 +10,7 @@ import type { MetricSink } from "./types"; import { FileSystemSink } from "./fileSystemSink"; import { DEFAULT_GLOBAL_CONFIG } from "../globalConfig"; import { PACKAGE_VERSION } from "../constants"; +import { resourceAttributesSchema } from "./shapes"; describe("DefaultTelemetryClient", () => { let tempDir: string; @@ -433,3 +434,27 @@ describe("OtelHistogramSink", () => { await rm(auditFilePath, { force: true }); }); }); + +describe("resourceAttributesSchema", () => { + const attributes = (version: string) => ({ + "service.name": "agentcore-cli", + "service.version": version, + "agentcore-cli.installation_id": "00000000-0000-0000-0000-000000000000", + "agentcore-cli.session_id": "00000000-0000-0000-0000-000000000000", + "os.type": "Darwin", + "os.version": "25.6.0", + "host.arch": "arm64", + "node.version": "v22.0.0", + }); + + test.each(["0.28.1", "1.0.0-rc.0", "1.0.0-preview.29"])( + "accepts service.version %s", + (version) => { + expect(resourceAttributesSchema.safeParse(attributes(version)).success).toBe(true); + }, + ); + + test.each(["v1.0.0", "1.0", "1.0.0-"])("rejects service.version %s", (version) => { + expect(resourceAttributesSchema.safeParse(attributes(version)).success).toBe(false); + }); +}); diff --git a/src/telemetry/shapes.tsx b/src/telemetry/shapes.tsx index 74f8c71e9..378e19d26 100644 --- a/src/telemetry/shapes.tsx +++ b/src/telemetry/shapes.tsx @@ -2,7 +2,7 @@ import z from "zod"; import { ERROR_SOURCE } from "../errors"; const UUID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i; -const SEMVER_PATTERN = /^\d+\.\d+\.\d+$/; +const SEMVER_PATTERN = /^\d+\.\d+\.\d+(-[0-9A-Za-z.-]+)?$/; const NODE_VERSION_PATTERN = /^v\d+\.\d+\.\d+$/; const MAX_ATTR_LENGTH = 64; From 710a8370a9953f6d45dd07f298a71c66955a5667 Mon Sep 17 00:00:00 2001 From: Tejas Kashinath Date: Thu, 3 Sep 2026 19:14:53 -0400 Subject: [PATCH 05/11] ci(release): make publish rerun-safe and scope concurrency to the publish job - concurrency keyed on the resolved version at the publish job, so a closed unrelated PR can no longer cancel a pending stable publish - skip npm publish when the version is already on npm, so a rerun after a failed release step completes instead of failing on the republish - re-dispatching stable edits the existing release PR instead of failing - cap service.version length like every other telemetry attribute --- .github/workflows/release.yml | 25 +++++++++++++++++++------ src/telemetry/shapes.tsx | 2 +- 2 files changed, 20 insertions(+), 7 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 9ec6dee82..25af1c546 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -29,10 +29,6 @@ on: # TODO: switch to main once the refactor lands there. branches: [refactor] -concurrency: - group: ${{ github.workflow }} - cancel-in-progress: false - jobs: version: if: github.event_name == 'workflow_dispatch' || (github.event.pull_request.merged && startsWith(github.head_ref, 'release/')) @@ -121,9 +117,15 @@ jobs: NOTES=$(gh api "repos/$GITHUB_REPOSITORY/releases/generate-notes" \ -f tag_name="v$VERSION" -f target_commitish="$REF" -f previous_tag_name="$PREVIOUS_TAG" --jq .body) - gh pr create --base "$GITHUB_REF_NAME" --head "$BRANCH" --title "chore(release): v$VERSION" --body "Merging publishes \`@aws/agentcore@$VERSION\` to npm under \`latest\` and creates the \`v$VERSION\` release. + BODY="Merging publishes \`@aws/agentcore@$VERSION\` to npm under \`latest\` and creates the \`v$VERSION\` release. $NOTES" + EXISTING=$(gh pr list --base "$GITHUB_REF_NAME" --head "$BRANCH" --json number --jq '.[0].number') + if [ -n "$EXISTING" ]; then + gh pr edit "$EXISTING" --body "$BODY" + else + gh pr create --base "$GITHUB_REF_NAME" --head "$BRANCH" --title "chore(release): v$VERSION" --body "$BODY" + fi check: needs: version @@ -153,6 +155,11 @@ jobs: publish: needs: [version, check, build, unit-test] runs-on: codebuild-agentcore-e2e-${{ github.run_id }}-${{ github.run_attempt }} + # Keyed on the version so unrelated runs never cancel a pending publish, while a duplicate publish of the + # same version waits for the first instead of racing it. + concurrency: + group: publish-${{ needs.version.outputs.version }} + cancel-in-progress: false # npm-publish-approval requires a reviewer. npm-publish does not: merging the release PR was the approval. environment: name: ${{ needs.version.outputs.channel == 'rc' && 'npm-publish-approval' || 'npm-publish' }} @@ -187,7 +194,13 @@ jobs: NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} TARBALL: ${{ steps.pack.outputs.tarball }} DIST_TAG: ${{ needs.version.outputs.channel == 'rc' && 'rc' || 'latest' }} - run: npm publish "$TARBALL" --provenance --access public --tag "$DIST_TAG" + # A rerun after a failed release step must not republish, npm rejects an existing version. + run: | + if npm view "@aws/agentcore@$VERSION" version >/dev/null 2>&1; then + echo "@aws/agentcore@$VERSION is already on npm, skipping publish" + else + npm publish "$TARBALL" --provenance --access public --tag "$DIST_TAG" + fi - if: ${{ !inputs.dry_run }} env: diff --git a/src/telemetry/shapes.tsx b/src/telemetry/shapes.tsx index 378e19d26..825161d81 100644 --- a/src/telemetry/shapes.tsx +++ b/src/telemetry/shapes.tsx @@ -12,7 +12,7 @@ const MAX_ATTR_LENGTH = 64; */ export const resourceAttributesSchema = z.object({ "service.name": z.literal("agentcore-cli"), - "service.version": z.string().regex(SEMVER_PATTERN), + "service.version": z.string().max(MAX_ATTR_LENGTH).regex(SEMVER_PATTERN), "agentcore-cli.installation_id": z.string().regex(UUID_PATTERN), "agentcore-cli.session_id": z.string().regex(UUID_PATTERN), "os.type": z.string().min(1).max(MAX_ATTR_LENGTH), From cef648869d7b49695fa5aebca951b97306aa62c2 Mon Sep 17 00:00:00 2001 From: Tejas Kashinath Date: Thu, 3 Sep 2026 21:22:03 -0400 Subject: [PATCH 06/11] ci(release): split into release-prepare and release-publish, every release goes through a PR - release-prepare.yml: dispatch with bump and channel, bumps package.json with bun pm version (rc series continue with prerelease and graduate by stripping the suffix, since Bun's major would jump an rc to the next major), refreshes the vended @aws/agentcore-cdk pin, opens release/v via the App - release-publish.yml: on merged release PRs, runs check/build/unit-test then publishes with provenance from a GitHub-hosted runner and creates the release. workflow_dispatch with dry_run for verification - scripts/sync-vended-cdk.ts: Bun port of the main-branch pin sync - no environments, no concurrency groups, no tag-derived rc numbering --- .github/workflows/README.md | 4 +- .github/workflows/release-prepare.yml | 73 +++++++++ .github/workflows/release-publish.yml | 117 ++++++++++++++ .github/workflows/release.yml | 221 -------------------------- scripts/sync-vended-cdk.ts | 26 +++ 5 files changed, 219 insertions(+), 222 deletions(-) create mode 100644 .github/workflows/release-prepare.yml create mode 100644 .github/workflows/release-publish.yml delete mode 100644 .github/workflows/release.yml create mode 100644 scripts/sync-vended-cdk.ts diff --git a/.github/workflows/README.md b/.github/workflows/README.md index b76297bfb..38e722c57 100644 --- a/.github/workflows/README.md +++ b/.github/workflows/README.md @@ -27,7 +27,9 @@ ci.yml |-- build.yml (bundle, package, compile, smoke test) `-- unit-test.yml (tests on Linux, Windows, macOS) -release.yml (version, release PR, approval, npm publish, GitHub release) +release-prepare.yml (version bump, vended CDK pin, release PR) + +release-publish.yml (npm publish and GitHub release when a release PR merges) |-- check.yml |-- build.yml `-- unit-test.yml diff --git a/.github/workflows/release-prepare.yml b/.github/workflows/release-prepare.yml new file mode 100644 index 000000000..8eb56d41b --- /dev/null +++ b/.github/workflows/release-prepare.yml @@ -0,0 +1,73 @@ +# Opens the release PR: bumps package.json, refreshes the vended @aws/agentcore-cdk pin, and pushes +# release/v. Merging that PR is the release approval and triggers release-publish.yml. +name: release-prepare +on: + workflow_dispatch: + inputs: + bump: + description: Semver component to release. Ignored while an rc series is open, which continues or graduates instead. + required: true + type: choice + options: [major, minor, patch] + channel: + description: rc publishes under the rc dist-tag, stable under latest + required: true + type: choice + options: [rc, stable] + +jobs: + prepare: + runs-on: ubuntu-latest + permissions: + contents: read + steps: + - uses: actions/checkout@v7 + with: + fetch-tags: true + persist-credentials: false + - uses: oven-sh/setup-bun@v2 + - uses: actions/create-github-app-token@v3 + id: app-token + with: + app-id: ${{ vars.APP_ID }} + private-key: ${{ secrets.APP_PRIVATE_KEY }} + + - name: Bump version + id: version + env: + BUMP: ${{ inputs.bump }} + CHANNEL: ${{ inputs.channel }} + run: | + set -euo pipefail + CURRENT=$(bun pm pkg get version | tr -d '"') + # 0.28.1 -> 1.0.0-rc.0 -> 1.0.0-rc.1 -> 1.0.0. Bun's own "major" would jump an rc to 2.0.0. + if [[ "$CURRENT" == *-rc.* ]]; then + INCREMENT=$([ "$CHANNEL" = rc ] && echo prerelease || echo "${CURRENT%%-*}") + else + INCREMENT=$([ "$CHANNEL" = rc ] && echo "pre$BUMP" || echo "$BUMP") + fi + TAG=$(bun pm version "$INCREMENT" --preid rc --no-git-tag-version) + if git rev-parse -q --verify "refs/tags/$TAG" >/dev/null; then + echo "::error::$TAG is already released" + exit 1 + fi + echo "version=${TAG#v}" >> "$GITHUB_OUTPUT" + + - run: bun scripts/sync-vended-cdk.ts + + - name: Open release PR + env: + GH_TOKEN: ${{ steps.app-token.outputs.token }} + VERSION: ${{ steps.version.outputs.version }} + run: | + set -euo pipefail + BRANCH="release/v$VERSION" + git config user.name "github-actions[bot]" + git config user.email "github-actions[bot]@users.noreply.github.com" + git commit -am "chore(release): v$VERSION" + git push --force "https://x-access-token:$GH_TOKEN@github.com/$GITHUB_REPOSITORY.git" "HEAD:refs/heads/$BRANCH" + + if [ -z "$(gh pr list --base "$GITHUB_REF_NAME" --head "$BRANCH" --json number --jq '.[0].number')" ]; then + gh pr create --base "$GITHUB_REF_NAME" --head "$BRANCH" --title "chore(release): v$VERSION" \ + --body "Merging publishes \`@aws/agentcore@$VERSION\` to npm and creates the \`v$VERSION\` GitHub release with generated notes." + fi diff --git a/.github/workflows/release-publish.yml b/.github/workflows/release-publish.yml new file mode 100644 index 000000000..dfd577c5e --- /dev/null +++ b/.github/workflows/release-publish.yml @@ -0,0 +1,117 @@ +# Publishes @aws/agentcore to npm and creates the GitHub release when a release PR from +# release-prepare.yml merges. The version is whatever the merged package.json says. +name: release-publish +on: + pull_request: + types: [closed] + # TODO: switch to main once the refactor lands there. + branches: [refactor] + workflow_dispatch: + inputs: + dry_run: + description: Build, pack and compile, but skip npm publish and the GitHub release + type: boolean + default: true + +jobs: + check: + if: github.event_name == 'workflow_dispatch' || (github.event.pull_request.merged && startsWith(github.head_ref, 'release/v')) + uses: ./.github/workflows/check.yml + permissions: + contents: read + with: + ref: ${{ github.event.pull_request.merge_commit_sha || github.sha }} + build: + if: github.event_name == 'workflow_dispatch' || (github.event.pull_request.merged && startsWith(github.head_ref, 'release/v')) + uses: ./.github/workflows/build.yml + permissions: + contents: read + with: + ref: ${{ github.event.pull_request.merge_commit_sha || github.sha }} + unit-test: + if: github.event_name == 'workflow_dispatch' || (github.event.pull_request.merged && startsWith(github.head_ref, 'release/v')) + uses: ./.github/workflows/unit-test.yml + permissions: + contents: read + with: + ref: ${{ github.event.pull_request.merge_commit_sha || github.sha }} + + publish: + needs: [check, build, unit-test] + # npm provenance is only issued from GitHub-hosted runners. + runs-on: ubuntu-latest + permissions: + contents: write + id-token: write + env: + REF: ${{ github.event.pull_request.merge_commit_sha || github.sha }} + steps: + - uses: actions/checkout@v7 + with: + ref: ${{ env.REF }} + fetch-tags: true + persist-credentials: false + - uses: oven-sh/setup-bun@v2 + - uses: actions/setup-node@v6 + with: + node-version: 20.x + registry-url: https://registry.npmjs.org + - run: bun install --frozen-lockfile + + - name: Resolve release + id: release + run: | + set -euo pipefail + VERSION=$(bun pm pkg get version | tr -d '"') + # rc notes span from the previous rc, stable notes from the previous stable. + DIST_TAG=latest + SEMVER_FLAGS=() + if [[ "$VERSION" == *-rc.* ]]; then + DIST_TAG=rc + SEMVER_FLAGS=(--include-prerelease) + fi + PREVIOUS=$(git tag -l 'v*' | xargs bunx semver "${SEMVER_FLAGS[@]}" --range "<$VERSION" | tail -1) + { + echo "version=$VERSION" + echo "dist_tag=$DIST_TAG" + echo "previous_tag=v$PREVIOUS" + } >> "$GITHUB_OUTPUT" + + - run: bun run build + - id: pack + run: echo "tarball=$(bun pm pack --quiet --ignore-scripts)" >> "$GITHUB_OUTPUT" + - run: bun run compile + + - if: ${{ !inputs.dry_run }} + env: + NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} + VERSION: ${{ steps.release.outputs.version }} + DIST_TAG: ${{ steps.release.outputs.dist_tag }} + TARBALL: ${{ steps.pack.outputs.tarball }} + # A rerun after a failed release step must not republish, npm rejects an existing version. + run: | + if npm view "@aws/agentcore@$VERSION" version >/dev/null 2>&1; then + echo "@aws/agentcore@$VERSION is already on npm, skipping publish" + else + npm publish "$TARBALL" --provenance --access public --tag "$DIST_TAG" + fi + + - if: ${{ !inputs.dry_run }} + env: + GH_TOKEN: ${{ github.token }} + VERSION: ${{ steps.release.outputs.version }} + PREVIOUS_TAG: ${{ steps.release.outputs.previous_tag }} + TARBALL: ${{ steps.pack.outputs.tarball }} + run: | + gh release create "v$VERSION" --target "$REF" --title "v$VERSION" \ + --generate-notes --notes-start-tag "$PREVIOUS_TAG" \ + ${{ steps.release.outputs.dist_tag == 'rc' && '--prerelease' || '--latest' }} \ + "$TARBALL" dist/bin/* + + - if: ${{ inputs.dry_run }} + uses: actions/upload-artifact@v7 + with: + name: release-${{ steps.release.outputs.version }} + path: | + ${{ steps.pack.outputs.tarball }} + dist/bin/* diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml deleted file mode 100644 index 25af1c546..000000000 --- a/.github/workflows/release.yml +++ /dev/null @@ -1,221 +0,0 @@ -# Publishes @aws/agentcore to npm and creates the matching GitHub release. -# -# rc: dispatch -> version -> check/build/unit-test -> publish -rc.N under the `rc` dist-tag, -# gated by the npm-publish-approval environment. -# stable: dispatch -> version -> release PR bumping package.json. Merging that PR re-enters this -# workflow, which runs check/build/unit-test and publishes under `latest`. -# -# package.json always holds the last published stable version. rc numbers come from git tags, never commits. -name: release -on: - workflow_dispatch: - inputs: - bump: - description: Semver component to bump from the last stable release - required: true - type: choice - options: [major, minor, patch] - channel: - description: rc publishes after approval, stable opens a release PR - required: true - type: choice - options: [rc, stable] - dry_run: - description: Build and pack, but skip npm publish and the GitHub release - type: boolean - default: false - pull_request: - types: [closed] - # TODO: switch to main once the refactor lands there. - branches: [refactor] - -jobs: - version: - if: github.event_name == 'workflow_dispatch' || (github.event.pull_request.merged && startsWith(github.head_ref, 'release/')) - runs-on: codebuild-agentcore-e2e-${{ github.run_id }}-${{ github.run_attempt }} - permissions: - contents: read - outputs: - ref: ${{ github.event.pull_request.merge_commit_sha || github.sha }} - channel: ${{ inputs.channel || 'stable' }} - action: ${{ github.event_name == 'workflow_dispatch' && inputs.channel == 'stable' && 'pull-request' || 'publish' }} - version: ${{ steps.resolve.outputs.version }} - previous_tag: ${{ steps.resolve.outputs.previous_tag }} - steps: - - uses: actions/checkout@v7 - with: - ref: ${{ github.event.pull_request.merge_commit_sha || github.sha }} - fetch-tags: true - persist-credentials: false - - uses: oven-sh/setup-bun@v2 - - - name: Resolve version - id: resolve - env: - BUMP: ${{ inputs.bump }} - CHANNEL: ${{ inputs.channel || 'stable' }} - run: | - set -euo pipefail - if [ -n "$BUMP" ]; then - BASE=$(bun pm version "$BUMP" --no-git-tag-version) - else - BASE="v$(bun pm pkg get version | tr -d '"')" - fi - if git rev-parse -q --verify "refs/tags/$BASE" >/dev/null; then - echo "::error::$BASE is already released" - exit 1 - fi - - VERSION="${BASE#v}" - PREVIOUS="" - if [ "$CHANNEL" = rc ]; then - LAST=$(git tag -l "$BASE-rc.*" | sed 's/.*-rc\.//' | sort -n | tail -1) - VERSION="$VERSION-rc.$(( ${LAST:--1} + 1 ))" - [ -n "$LAST" ] && PREVIOUS="$BASE-rc.$LAST" - fi - # Stable releases and rc.0 take their notes from the previous stable release. - [ -n "$PREVIOUS" ] || PREVIOUS="v$(git tag -l 'v*' | xargs bunx semver@7 --range "<$VERSION" | tail -1)" - - { - echo "version=$VERSION" - echo "previous_tag=$PREVIOUS" - } >> "$GITHUB_OUTPUT" - echo "$VERSION (notes since $PREVIOUS)" >> "$GITHUB_STEP_SUMMARY" - - release-pr: - needs: version - if: needs.version.outputs.action == 'pull-request' - runs-on: codebuild-agentcore-e2e-${{ github.run_id }}-${{ github.run_attempt }} - permissions: - contents: read - steps: - - uses: actions/checkout@v7 - with: - ref: ${{ needs.version.outputs.ref }} - persist-credentials: false - - uses: oven-sh/setup-bun@v2 - - uses: actions/create-github-app-token@v3 - id: app-token - with: - app-id: ${{ vars.APP_ID }} - private-key: ${{ secrets.APP_PRIVATE_KEY }} - - - name: Open release PR - env: - GH_TOKEN: ${{ steps.app-token.outputs.token }} - VERSION: ${{ needs.version.outputs.version }} - PREVIOUS_TAG: ${{ needs.version.outputs.previous_tag }} - REF: ${{ needs.version.outputs.ref }} - run: | - set -euo pipefail - BRANCH="release/v$VERSION" - bun pm version "$VERSION" --no-git-tag-version - git config user.name "github-actions[bot]" - git config user.email "github-actions[bot]@users.noreply.github.com" - git commit -am "chore(release): v$VERSION" - git push --force "https://x-access-token:$GH_TOKEN@github.com/$GITHUB_REPOSITORY.git" "HEAD:refs/heads/$BRANCH" - - NOTES=$(gh api "repos/$GITHUB_REPOSITORY/releases/generate-notes" \ - -f tag_name="v$VERSION" -f target_commitish="$REF" -f previous_tag_name="$PREVIOUS_TAG" --jq .body) - BODY="Merging publishes \`@aws/agentcore@$VERSION\` to npm under \`latest\` and creates the \`v$VERSION\` release. - - $NOTES" - EXISTING=$(gh pr list --base "$GITHUB_REF_NAME" --head "$BRANCH" --json number --jq '.[0].number') - if [ -n "$EXISTING" ]; then - gh pr edit "$EXISTING" --body "$BODY" - else - gh pr create --base "$GITHUB_REF_NAME" --head "$BRANCH" --title "chore(release): v$VERSION" --body "$BODY" - fi - - check: - needs: version - if: needs.version.outputs.action == 'publish' - uses: ./.github/workflows/check.yml - permissions: - contents: read - with: - ref: ${{ needs.version.outputs.ref }} - build: - needs: version - if: needs.version.outputs.action == 'publish' - uses: ./.github/workflows/build.yml - permissions: - contents: read - with: - ref: ${{ needs.version.outputs.ref }} - unit-test: - needs: version - if: needs.version.outputs.action == 'publish' - uses: ./.github/workflows/unit-test.yml - permissions: - contents: read - with: - ref: ${{ needs.version.outputs.ref }} - - publish: - needs: [version, check, build, unit-test] - runs-on: codebuild-agentcore-e2e-${{ github.run_id }}-${{ github.run_attempt }} - # Keyed on the version so unrelated runs never cancel a pending publish, while a duplicate publish of the - # same version waits for the first instead of racing it. - concurrency: - group: publish-${{ needs.version.outputs.version }} - cancel-in-progress: false - # npm-publish-approval requires a reviewer. npm-publish does not: merging the release PR was the approval. - environment: - name: ${{ needs.version.outputs.channel == 'rc' && 'npm-publish-approval' || 'npm-publish' }} - url: https://www.npmjs.com/package/@aws/agentcore/v/${{ needs.version.outputs.version }} - permissions: - contents: write - id-token: write - env: - VERSION: ${{ needs.version.outputs.version }} - REF: ${{ needs.version.outputs.ref }} - PREVIOUS_TAG: ${{ needs.version.outputs.previous_tag }} - steps: - - uses: actions/checkout@v7 - with: - ref: ${{ needs.version.outputs.ref }} - persist-credentials: false - - uses: oven-sh/setup-bun@v2 - - uses: actions/setup-node@v6 - with: - node-version: 20.x - registry-url: https://registry.npmjs.org - - run: bun install --frozen-lockfile - - - run: bun pm version "$VERSION" --no-git-tag-version --allow-same-version - - run: bun run build - - id: pack - run: echo "tarball=$(bun pm pack --quiet --ignore-scripts)" >> "$GITHUB_OUTPUT" - - run: bun run compile - - - if: ${{ !inputs.dry_run }} - env: - NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} - TARBALL: ${{ steps.pack.outputs.tarball }} - DIST_TAG: ${{ needs.version.outputs.channel == 'rc' && 'rc' || 'latest' }} - # A rerun after a failed release step must not republish, npm rejects an existing version. - run: | - if npm view "@aws/agentcore@$VERSION" version >/dev/null 2>&1; then - echo "@aws/agentcore@$VERSION is already on npm, skipping publish" - else - npm publish "$TARBALL" --provenance --access public --tag "$DIST_TAG" - fi - - - if: ${{ !inputs.dry_run }} - env: - GH_TOKEN: ${{ github.token }} - TARBALL: ${{ steps.pack.outputs.tarball }} - run: | - gh release create "v$VERSION" --target "$REF" --title "v$VERSION" \ - --generate-notes --notes-start-tag "$PREVIOUS_TAG" \ - ${{ needs.version.outputs.channel == 'rc' && '--prerelease' || '--latest' }} \ - "$TARBALL" dist/bin/* - - - if: ${{ inputs.dry_run }} - uses: actions/upload-artifact@v7 - with: - name: release-${{ env.VERSION }} - path: | - ${{ steps.pack.outputs.tarball }} - dist/bin/* diff --git a/scripts/sync-vended-cdk.ts b/scripts/sync-vended-cdk.ts new file mode 100644 index 000000000..a191bb93b --- /dev/null +++ b/scripts/sync-vended-cdk.ts @@ -0,0 +1,26 @@ +#!/usr/bin/env bun + +import { $ } from "bun"; +import { join, resolve } from "node:path"; +import { runWithExitCode } from "../src/runnable"; + +const CDK_PACKAGE = "@aws/agentcore-cdk"; +const TEMPLATE = Bun.file( + join(resolve(import.meta.dir, ".."), "src", "assets", "cdk", "package.json"), +); + +process.exit( + await runWithExitCode(async (argv) => { + const tag = argv[2] ?? "latest"; + const target = (await $`bun pm view ${CDK_PACKAGE}@${tag} version`.text()).trim(); + const template = await TEMPLATE.json(); + const current = template.dependencies[CDK_PACKAGE]; + if (current === target) { + console.log(`${CDK_PACKAGE} already pinned to ${target}`); + return; + } + template.dependencies[CDK_PACKAGE] = target; + await Bun.write(TEMPLATE, JSON.stringify(template, null, 2) + "\n"); + console.log(`${CDK_PACKAGE}: ${current} → ${target}`); + }), +); From a9ed7b216d960a8f799b05656820f4a7151979e7 Mon Sep 17 00:00:00 2001 From: Tejas Kashinath Date: Thu, 3 Sep 2026 21:32:17 -0400 Subject: [PATCH 07/11] ci(release): make sync-vended-cdk dependency-free, it runs before bun install --- scripts/sync-vended-cdk.ts | 33 +++++++++++++++++---------------- 1 file changed, 17 insertions(+), 16 deletions(-) diff --git a/scripts/sync-vended-cdk.ts b/scripts/sync-vended-cdk.ts index a191bb93b..5be86b0d5 100644 --- a/scripts/sync-vended-cdk.ts +++ b/scripts/sync-vended-cdk.ts @@ -1,26 +1,27 @@ #!/usr/bin/env bun +/** + Pins the vended CDK template to the published @aws/agentcore-cdk at the given dist-tag (default latest). + Runs before bun install in release-prepare.yml, so it must not import anything from src/. +**/ + import { $ } from "bun"; import { join, resolve } from "node:path"; -import { runWithExitCode } from "../src/runnable"; const CDK_PACKAGE = "@aws/agentcore-cdk"; const TEMPLATE = Bun.file( join(resolve(import.meta.dir, ".."), "src", "assets", "cdk", "package.json"), ); -process.exit( - await runWithExitCode(async (argv) => { - const tag = argv[2] ?? "latest"; - const target = (await $`bun pm view ${CDK_PACKAGE}@${tag} version`.text()).trim(); - const template = await TEMPLATE.json(); - const current = template.dependencies[CDK_PACKAGE]; - if (current === target) { - console.log(`${CDK_PACKAGE} already pinned to ${target}`); - return; - } - template.dependencies[CDK_PACKAGE] = target; - await Bun.write(TEMPLATE, JSON.stringify(template, null, 2) + "\n"); - console.log(`${CDK_PACKAGE}: ${current} → ${target}`); - }), -); +const tag = Bun.argv[2] ?? "latest"; +const target = (await $`bun pm view ${CDK_PACKAGE}@${tag} version`.text()).trim(); +const template = await TEMPLATE.json(); +const current = template.dependencies[CDK_PACKAGE]; + +if (current === target) { + console.log(`${CDK_PACKAGE} already pinned to ${target}`); +} else { + template.dependencies[CDK_PACKAGE] = target; + await Bun.write(TEMPLATE, JSON.stringify(template, null, 2) + "\n"); + console.log(`${CDK_PACKAGE}: ${current} → ${target}`); +} From 59fa397a69eff8b7f3a655b341050ccdd982fd86 Mon Sep 17 00:00:00 2001 From: Tejas Kashinath Date: Thu, 3 Sep 2026 21:46:07 -0400 Subject: [PATCH 08/11] ci(release): scope release notes to this workflow's tags, make release creation rerun-safe Also follow the package rename through: the PR tarball job and the README looked for agentcore-*.tgz, Bun names a scoped tarball aws-agentcore-*.tgz. --- .github/workflows/pr-automation.yml | 2 +- .github/workflows/release-publish.yml | 19 +++++++++++++------ README.md | 4 ++-- 3 files changed, 16 insertions(+), 9 deletions(-) diff --git a/.github/workflows/pr-automation.yml b/.github/workflows/pr-automation.yml index 7950561e2..ee57bd362 100644 --- a/.github/workflows/pr-automation.yml +++ b/.github/workflows/pr-automation.yml @@ -171,7 +171,7 @@ jobs: id: tarball run: | bun pm pack --destination "$RUNNER_TEMP" - tarball="$(find "$RUNNER_TEMP" -maxdepth 1 -type f -name 'agentcore-*.tgz' -print -quit)" + tarball="$(find "$RUNNER_TEMP" -maxdepth 1 -type f -name 'aws-agentcore-*.tgz' -print -quit)" test -f "$tarball" echo "name=$(basename "$tarball")" >> "$GITHUB_OUTPUT" echo "path=$tarball" >> "$GITHUB_OUTPUT" diff --git a/.github/workflows/release-publish.yml b/.github/workflows/release-publish.yml index dfd577c5e..cd4e239ad 100644 --- a/.github/workflows/release-publish.yml +++ b/.github/workflows/release-publish.yml @@ -63,14 +63,16 @@ jobs: run: | set -euo pipefail VERSION=$(bun pm pkg get version | tr -d '"') - # rc notes span from the previous rc, stable notes from the previous stable. + # rc notes span from the previous rc, stable notes from the previous stable. Only this + # workflow's tags count, main's v1.0.0-preview.N tags live in the same repo. DIST_TAG=latest SEMVER_FLAGS=() if [[ "$VERSION" == *-rc.* ]]; then DIST_TAG=rc SEMVER_FLAGS=(--include-prerelease) fi - PREVIOUS=$(git tag -l 'v*' | xargs bunx semver "${SEMVER_FLAGS[@]}" --range "<$VERSION" | tail -1) + PREVIOUS=$(git tag -l 'v*' | grep -E '^v[0-9]+\.[0-9]+\.[0-9]+(-rc\.[0-9]+)?$' \ + | xargs bunx semver "${SEMVER_FLAGS[@]}" --range "<$VERSION" | tail -1) { echo "version=$VERSION" echo "dist_tag=$DIST_TAG" @@ -102,11 +104,16 @@ jobs: VERSION: ${{ steps.release.outputs.version }} PREVIOUS_TAG: ${{ steps.release.outputs.previous_tag }} TARBALL: ${{ steps.pack.outputs.tarball }} + # A rerun after a failed asset upload re-uploads into the existing release instead of failing on the tag. run: | - gh release create "v$VERSION" --target "$REF" --title "v$VERSION" \ - --generate-notes --notes-start-tag "$PREVIOUS_TAG" \ - ${{ steps.release.outputs.dist_tag == 'rc' && '--prerelease' || '--latest' }} \ - "$TARBALL" dist/bin/* + if gh release view "v$VERSION" >/dev/null 2>&1; then + gh release upload "v$VERSION" --clobber "$TARBALL" dist/bin/* + else + gh release create "v$VERSION" --target "$REF" --title "v$VERSION" \ + --generate-notes --notes-start-tag "$PREVIOUS_TAG" \ + ${{ steps.release.outputs.dist_tag == 'rc' && '--prerelease' || '--latest' }} \ + "$TARBALL" dist/bin/* + fi - if: ${{ inputs.dry_run }} uses: actions/upload-artifact@v7 diff --git a/README.md b/README.md index 41a92ba40..f240ff650 100644 --- a/README.md +++ b/README.md @@ -905,14 +905,14 @@ npm link Re-run `bun run build` after changes; the linked command picks it up. Remove with: ```bash -npm unlink -g agentcore +npm unlink -g @aws/agentcore ``` To test the exact published artifact instead: ```bash npm pack # builds via prepublishOnly, creates the .tgz -npm i -g ./agentcore-1.0.0.tgz +npm i -g ./aws-agentcore-0.28.1.tgz ``` # Build From 9b6adb47d6ebdce3d17e71e3748b800e65b33192 Mon Sep 17 00:00:00 2001 From: Tejas Kashinath Date: Fri, 4 Sep 2026 09:35:44 -0400 Subject: [PATCH 09/11] ci: merge check, build and unit-test into one reusable verify workflow ci.yml and release-publish.yml each call verify.yml once instead of wiring three workflows. Job bodies are unchanged. release-publish drops its workflow_dispatch, which only existed for dry-run verification. --- .github/workflows/README.md | 22 +++-- .github/workflows/build.yml | 51 ------------ .github/workflows/check.yml | 31 ------- .github/workflows/ci.yml | 16 +--- .github/workflows/release-publish.yml | 46 ++--------- .github/workflows/unit-test.yml | 50 ------------ .github/workflows/verify.yml | 111 ++++++++++++++++++++++++++ 7 files changed, 131 insertions(+), 196 deletions(-) delete mode 100644 .github/workflows/build.yml delete mode 100644 .github/workflows/check.yml delete mode 100644 .github/workflows/unit-test.yml create mode 100644 .github/workflows/verify.yml diff --git a/.github/workflows/README.md b/.github/workflows/README.md index 38e722c57..fdefc7cb6 100644 --- a/.github/workflows/README.md +++ b/.github/workflows/README.md @@ -5,8 +5,8 @@ Workflows are organized into two roles: ``` -Orchestrators (e.g. ci.yml, release.yml) -Jobs (e.g. build.yml, unit-test.yml, publish-npm.yml) +Orchestrators (ci.yml, release-prepare.yml, release-publish.yml) +Jobs (verify.yml) ``` **Orchestrators** respond to events and coordinate work. They define _when_ and @@ -23,26 +23,24 @@ An orchestrator calls jobs via `workflow_call`. ``` ci.yml - |-- check.yml (lint, format, typecheck, audit, secret scan) - |-- build.yml (bundle, package, compile, smoke test) - `-- unit-test.yml (tests on Linux, Windows, macOS) + `-- verify.yml (check: lint, format, typecheck, audit, secret scan + build: bundle, package, compile, smoke test per platform + unit-test: Linux, Windows, macOS) release-prepare.yml (version bump, vended CDK pin, release PR) release-publish.yml (npm publish and GitHub release when a release PR merges) - |-- check.yml - |-- build.yml - `-- unit-test.yml + `-- verify.yml ``` -Jobs like `unit-test.yml` and `build.yml` appear in multiple orchestrators. This -is the point — write once, compose freely. +`verify.yml` appears in both orchestrators. This is the point — write once, +compose freely. ## Naming Convention -- **Orchestrators** are named for their purpose (e.g. `ci`, `release`). +- **Orchestrators** are named for their purpose (e.g. `ci`, `release-publish`). - **Jobs** are named as verbs or noun-verb pairs describing the work - (e.g. `build`, `unit-test`, `publish-npm`). + (e.g. `verify`). ## Key Choices diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml deleted file mode 100644 index 9194929f1..000000000 --- a/.github/workflows/build.yml +++ /dev/null @@ -1,51 +0,0 @@ -# Builds the CLI and compiles it into executables for each platform for a smoke test. -name: build -on: - workflow_call: - inputs: - ref: - required: true - type: string - -env: - AGENTCORE_TELEMETRY_DISABLED: "1" - -jobs: - build: - name: Build (${{ matrix.name }}) - runs-on: ${{ fromJSON(matrix.runner) }} - permissions: - contents: read - strategy: - fail-fast: false - matrix: - include: - - name: Linux - runner: '["codebuild-agentcore-e2e-${{ github.run_id }}-${{ github.run_attempt }}"]' - target: linux-x64 - binary: ./dist/bin/agentcore-linux-x64 - - name: Windows - runner: '["codebuild-agentcore-e2e-${{ github.run_id }}-${{ github.run_attempt }}", "image:windows-1.0"]' - target: windows-x64 - binary: ./dist/bin/agentcore-windows-x64.exe - # CodeBuild does not support macOS. - - name: macOS - runner: '["macos-latest"]' - target: darwin-arm64 - binary: ./dist/bin/agentcore-darwin-arm64 - steps: - - uses: actions/checkout@v7 - with: - ref: ${{ inputs.ref }} - persist-credentials: false - - uses: oven-sh/setup-bun@v2 - - run: bun install --frozen-lockfile - - - run: bun run build - - - run: bun pm pack - - - run: bun run compile:${{ matrix.target }} - - - name: Smoke test binary - run: ${{ matrix.binary }} --help diff --git a/.github/workflows/check.yml b/.github/workflows/check.yml deleted file mode 100644 index 2609545ca..000000000 --- a/.github/workflows/check.yml +++ /dev/null @@ -1,31 +0,0 @@ -# Verifies lint, formatting, types, audit, and other static checks pass. -name: check -on: - workflow_call: - inputs: - ref: - required: true - type: string - -jobs: - check: - runs-on: codebuild-agentcore-e2e-${{ github.run_id }}-${{ github.run_attempt }} - permissions: - contents: read - steps: - - uses: actions/checkout@v7 - with: - ref: ${{ inputs.ref }} - persist-credentials: false - - uses: oven-sh/setup-bun@v2 - - run: bun install --frozen-lockfile - - run: bun run lint:check - if: always() - - run: bun run format:check - if: always() - - run: bun run typecheck - if: always() - - run: bun audit - if: always() - - run: bun run secrets:check - if: always() diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 7363f1cfc..4660d09b6 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -13,20 +13,8 @@ concurrency: cancel-in-progress: true jobs: - check: - uses: ./.github/workflows/check.yml - permissions: - contents: read - with: - ref: ${{ github.event.pull_request.head.sha || github.sha }} - build: - uses: ./.github/workflows/build.yml - permissions: - contents: read - with: - ref: ${{ github.event.pull_request.head.sha || github.sha }} - unit-test: - uses: ./.github/workflows/unit-test.yml + verify: + uses: ./.github/workflows/verify.yml permissions: contents: read with: diff --git a/.github/workflows/release-publish.yml b/.github/workflows/release-publish.yml index cd4e239ad..a7ed01d05 100644 --- a/.github/workflows/release-publish.yml +++ b/.github/workflows/release-publish.yml @@ -6,45 +6,25 @@ on: types: [closed] # TODO: switch to main once the refactor lands there. branches: [refactor] - workflow_dispatch: - inputs: - dry_run: - description: Build, pack and compile, but skip npm publish and the GitHub release - type: boolean - default: true jobs: - check: - if: github.event_name == 'workflow_dispatch' || (github.event.pull_request.merged && startsWith(github.head_ref, 'release/v')) - uses: ./.github/workflows/check.yml + verify: + if: github.event.pull_request.merged && startsWith(github.head_ref, 'release/v') + uses: ./.github/workflows/verify.yml permissions: contents: read with: - ref: ${{ github.event.pull_request.merge_commit_sha || github.sha }} - build: - if: github.event_name == 'workflow_dispatch' || (github.event.pull_request.merged && startsWith(github.head_ref, 'release/v')) - uses: ./.github/workflows/build.yml - permissions: - contents: read - with: - ref: ${{ github.event.pull_request.merge_commit_sha || github.sha }} - unit-test: - if: github.event_name == 'workflow_dispatch' || (github.event.pull_request.merged && startsWith(github.head_ref, 'release/v')) - uses: ./.github/workflows/unit-test.yml - permissions: - contents: read - with: - ref: ${{ github.event.pull_request.merge_commit_sha || github.sha }} + ref: ${{ github.event.pull_request.merge_commit_sha }} publish: - needs: [check, build, unit-test] + needs: verify # npm provenance is only issued from GitHub-hosted runners. runs-on: ubuntu-latest permissions: contents: write id-token: write env: - REF: ${{ github.event.pull_request.merge_commit_sha || github.sha }} + REF: ${{ github.event.pull_request.merge_commit_sha }} steps: - uses: actions/checkout@v7 with: @@ -84,8 +64,7 @@ jobs: run: echo "tarball=$(bun pm pack --quiet --ignore-scripts)" >> "$GITHUB_OUTPUT" - run: bun run compile - - if: ${{ !inputs.dry_run }} - env: + - env: NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} VERSION: ${{ steps.release.outputs.version }} DIST_TAG: ${{ steps.release.outputs.dist_tag }} @@ -98,8 +77,7 @@ jobs: npm publish "$TARBALL" --provenance --access public --tag "$DIST_TAG" fi - - if: ${{ !inputs.dry_run }} - env: + - env: GH_TOKEN: ${{ github.token }} VERSION: ${{ steps.release.outputs.version }} PREVIOUS_TAG: ${{ steps.release.outputs.previous_tag }} @@ -114,11 +92,3 @@ jobs: ${{ steps.release.outputs.dist_tag == 'rc' && '--prerelease' || '--latest' }} \ "$TARBALL" dist/bin/* fi - - - if: ${{ inputs.dry_run }} - uses: actions/upload-artifact@v7 - with: - name: release-${{ steps.release.outputs.version }} - path: | - ${{ steps.pack.outputs.tarball }} - dist/bin/* diff --git a/.github/workflows/unit-test.yml b/.github/workflows/unit-test.yml deleted file mode 100644 index 40e81b6df..000000000 --- a/.github/workflows/unit-test.yml +++ /dev/null @@ -1,50 +0,0 @@ -# Verifies unit tests pass on all platforms (Linux, Windows, macOS). -name: unit-test -on: - workflow_call: - inputs: - ref: - required: true - type: string - secrets: - CODECOV_TOKEN: - required: false - -env: - AGENTCORE_TELEMETRY_DISABLED: "1" - -jobs: - test: - name: Test (${{ matrix.name }}) - runs-on: ${{ fromJSON(matrix.runner) }} - permissions: - contents: read - strategy: - fail-fast: false - matrix: - include: - - name: Linux - runner: '["codebuild-agentcore-e2e-${{ github.run_id }}-${{ github.run_attempt }}"]' - - name: Windows - runner: '["codebuild-agentcore-e2e-${{ github.run_id }}-${{ github.run_attempt }}", "image:windows-1.0"]' - # CodeBuild does not support macOS. - # https://docs.aws.amazon.com/codebuild/latest/userguide/action-runner-questions.html#action-runner-platform - - name: macOS - runner: '["macos-latest"]' - steps: - - uses: actions/checkout@v7 - with: - ref: ${{ inputs.ref }} - persist-credentials: false - - uses: oven-sh/setup-bun@v2 - - run: bun install --frozen-lockfile - # TODO: investigate sharding as repo grows: https://bun.com/blog/release-notes/bun-v1.3.13#bun-test-shard-m-n-for-splitting-tests-across-ci-jobs - - run: bun test --coverage --coverage-reporter=lcov - - - name: Upload to CodeCov - # only upload on linux to avoid duplicates. - if: always() && runner.os == 'Linux' - uses: codecov/codecov-action@v5 - with: - token: ${{ secrets.CODECOV_TOKEN }} - fail_ci_if_error: false diff --git a/.github/workflows/verify.yml b/.github/workflows/verify.yml new file mode 100644 index 000000000..75e918063 --- /dev/null +++ b/.github/workflows/verify.yml @@ -0,0 +1,111 @@ +# Verifies a commit: static checks, a build with per-platform smoke tests, and unit tests on all platforms. +name: verify +on: + workflow_call: + inputs: + ref: + required: true + type: string + secrets: + CODECOV_TOKEN: + required: false + +env: + AGENTCORE_TELEMETRY_DISABLED: "1" + +jobs: + check: + runs-on: codebuild-agentcore-e2e-${{ github.run_id }}-${{ github.run_attempt }} + permissions: + contents: read + steps: + - uses: actions/checkout@v7 + with: + ref: ${{ inputs.ref }} + persist-credentials: false + - uses: oven-sh/setup-bun@v2 + - run: bun install --frozen-lockfile + - run: bun run lint:check + if: always() + - run: bun run format:check + if: always() + - run: bun run typecheck + if: always() + - run: bun audit + if: always() + - run: bun run secrets:check + if: always() + + build: + name: Build (${{ matrix.name }}) + runs-on: ${{ fromJSON(matrix.runner) }} + permissions: + contents: read + strategy: + fail-fast: false + matrix: + include: + - name: Linux + runner: '["codebuild-agentcore-e2e-${{ github.run_id }}-${{ github.run_attempt }}"]' + target: linux-x64 + binary: ./dist/bin/agentcore-linux-x64 + - name: Windows + runner: '["codebuild-agentcore-e2e-${{ github.run_id }}-${{ github.run_attempt }}", "image:windows-1.0"]' + target: windows-x64 + binary: ./dist/bin/agentcore-windows-x64.exe + # CodeBuild does not support macOS. + - name: macOS + runner: '["macos-latest"]' + target: darwin-arm64 + binary: ./dist/bin/agentcore-darwin-arm64 + steps: + - uses: actions/checkout@v7 + with: + ref: ${{ inputs.ref }} + persist-credentials: false + - uses: oven-sh/setup-bun@v2 + - run: bun install --frozen-lockfile + + - run: bun run build + + - run: bun pm pack + + - run: bun run compile:${{ matrix.target }} + + - name: Smoke test binary + run: ${{ matrix.binary }} --help + + unit-test: + name: Test (${{ matrix.name }}) + runs-on: ${{ fromJSON(matrix.runner) }} + permissions: + contents: read + strategy: + fail-fast: false + matrix: + include: + - name: Linux + runner: '["codebuild-agentcore-e2e-${{ github.run_id }}-${{ github.run_attempt }}"]' + - name: Windows + runner: '["codebuild-agentcore-e2e-${{ github.run_id }}-${{ github.run_attempt }}", "image:windows-1.0"]' + # CodeBuild does not support macOS. + # https://docs.aws.amazon.com/codebuild/latest/userguide/action-runner-questions.html#action-runner-platform + - name: macOS + runner: '["macos-latest"]' + steps: + - uses: actions/checkout@v7 + with: + ref: ${{ inputs.ref }} + persist-credentials: false + - uses: oven-sh/setup-bun@v2 + - run: bun install --frozen-lockfile + # TODO: investigate sharding as repo grows: https://bun.com/blog/release-notes/bun-v1.3.13#bun-test-shard-m-n-for-splitting-tests-across-ci-jobs + - run: bun test --coverage --coverage-reporter=lcov + + - name: Upload to CodeCov + # only upload on linux to avoid duplicates. + if: always() && runner.os == 'Linux' + uses: codecov/codecov-action@v5 + with: + token: ${{ secrets.CODECOV_TOKEN }} + fail_ci_if_error: false From a548bf11b5f3ef650b2431885e13dae8dabe136d Mon Sep 17 00:00:00 2001 From: Tejas Kashinath Date: Fri, 4 Sep 2026 10:26:44 -0400 Subject: [PATCH 10/11] ci(verify): one job per platform, install once then check, build, compile, smoke test and test --- .github/workflows/README.md | 5 ++- .github/workflows/verify.yml | 68 +++++++++--------------------------- 2 files changed, 18 insertions(+), 55 deletions(-) diff --git a/.github/workflows/README.md b/.github/workflows/README.md index fdefc7cb6..220a4fe31 100644 --- a/.github/workflows/README.md +++ b/.github/workflows/README.md @@ -23,9 +23,8 @@ An orchestrator calls jobs via `workflow_call`. ``` ci.yml - `-- verify.yml (check: lint, format, typecheck, audit, secret scan - build: bundle, package, compile, smoke test per platform - unit-test: Linux, Windows, macOS) + `-- verify.yml (one job per platform: static checks on Linux, then bundle, + package, compile, smoke test and unit tests) release-prepare.yml (version bump, vended CDK pin, release PR) diff --git a/.github/workflows/verify.yml b/.github/workflows/verify.yml index 75e918063..96ecde47b 100644 --- a/.github/workflows/verify.yml +++ b/.github/workflows/verify.yml @@ -1,4 +1,5 @@ -# Verifies a commit: static checks, a build with per-platform smoke tests, and unit tests on all platforms. +# Verifies a commit on every platform in one job each: static checks (Linux only, they are +# platform-independent), bundle, package, compile, smoke test, unit tests. name: verify on: workflow_call: @@ -14,30 +15,8 @@ env: AGENTCORE_TELEMETRY_DISABLED: "1" jobs: - check: - runs-on: codebuild-agentcore-e2e-${{ github.run_id }}-${{ github.run_attempt }} - permissions: - contents: read - steps: - - uses: actions/checkout@v7 - with: - ref: ${{ inputs.ref }} - persist-credentials: false - - uses: oven-sh/setup-bun@v2 - - run: bun install --frozen-lockfile - - run: bun run lint:check - if: always() - - run: bun run format:check - if: always() - - run: bun run typecheck - if: always() - - run: bun audit - if: always() - - run: bun run secrets:check - if: always() - - build: - name: Build (${{ matrix.name }}) + verify: + name: ${{ matrix.name }} runs-on: ${{ fromJSON(matrix.runner) }} permissions: contents: read @@ -54,6 +33,7 @@ jobs: target: windows-x64 binary: ./dist/bin/agentcore-windows-x64.exe # CodeBuild does not support macOS. + # https://docs.aws.amazon.com/codebuild/latest/userguide/action-runner-questions.html#action-runner-platform - name: macOS runner: '["macos-latest"]' target: darwin-arm64 @@ -66,39 +46,23 @@ jobs: - uses: oven-sh/setup-bun@v2 - run: bun install --frozen-lockfile - - run: bun run build + - run: bun run lint:check + if: runner.os == 'Linux' + - run: bun run format:check + if: runner.os == 'Linux' + - run: bun run typecheck + if: runner.os == 'Linux' + - run: bun audit + if: runner.os == 'Linux' + - run: bun run secrets:check + if: runner.os == 'Linux' + - run: bun run build - run: bun pm pack - - run: bun run compile:${{ matrix.target }} - - name: Smoke test binary run: ${{ matrix.binary }} --help - unit-test: - name: Test (${{ matrix.name }}) - runs-on: ${{ fromJSON(matrix.runner) }} - permissions: - contents: read - strategy: - fail-fast: false - matrix: - include: - - name: Linux - runner: '["codebuild-agentcore-e2e-${{ github.run_id }}-${{ github.run_attempt }}"]' - - name: Windows - runner: '["codebuild-agentcore-e2e-${{ github.run_id }}-${{ github.run_attempt }}", "image:windows-1.0"]' - # CodeBuild does not support macOS. - # https://docs.aws.amazon.com/codebuild/latest/userguide/action-runner-questions.html#action-runner-platform - - name: macOS - runner: '["macos-latest"]' - steps: - - uses: actions/checkout@v7 - with: - ref: ${{ inputs.ref }} - persist-credentials: false - - uses: oven-sh/setup-bun@v2 - - run: bun install --frozen-lockfile # TODO: investigate sharding as repo grows: https://bun.com/blog/release-notes/bun-v1.3.13#bun-test-shard-m-n-for-splitting-tests-across-ci-jobs - run: bun test --coverage --coverage-reporter=lcov From 9000d636a047f0665926f31f1a200383c6be92f1 Mon Sep 17 00:00:00 2001 From: Tejas Kashinath Date: Fri, 4 Sep 2026 11:41:15 -0400 Subject: [PATCH 11/11] ci(release): first rc is rc.1, pin Bun via packageManager, report every static check - release-prepare sets series boundaries explicitly so 0.28.1 -> 1.0.0-rc.1 -> 1.0.0-rc.2 -> 1.0.0 - package.json packageManager pins Bun 1.4.0, setup-bun reads it in every workflow - verify keeps reporting all static checks after one fails, pack skips prepublishOnly - update command tracks the dist-tag of its own prerelease identifier (rc, preview) instead of mapping every prerelease to preview and comparing against latest - README documents the recovery path when publish fails after the release PR merges --- .github/workflows/README.md | 9 +++++++++ .github/workflows/release-prepare.yml | 11 +++++++---- .github/workflows/verify.yml | 13 +++++++------ package.json | 1 + src/handlers/update/index.tsx | 6 +++--- src/handlers/update/update.test.ts | 10 +++++++++- 6 files changed, 36 insertions(+), 14 deletions(-) diff --git a/.github/workflows/README.md b/.github/workflows/README.md index 220a4fe31..5290f8dac 100644 --- a/.github/workflows/README.md +++ b/.github/workflows/README.md @@ -41,6 +41,15 @@ compose freely. - **Jobs** are named as verbs or noun-verb pairs describing the work (e.g. `verify`). +## Releasing + +Dispatch `release-prepare` with a bump and a channel, review the release PR it opens, merge it. +`release-publish` then publishes the merged package.json version. + +If publish fails after the merge, rerun the failed `release-publish` jobs. Both the npm publish +and the GitHub release steps skip work that already succeeded. Do not re-dispatch +`release-prepare`, package.json already holds the new version and it would bump again. + ## Key Choices ### Explicit ref passing diff --git a/.github/workflows/release-prepare.yml b/.github/workflows/release-prepare.yml index 8eb56d41b..76a28f7fe 100644 --- a/.github/workflows/release-prepare.yml +++ b/.github/workflows/release-prepare.yml @@ -40,13 +40,16 @@ jobs: run: | set -euo pipefail CURRENT=$(bun pm pkg get version | tr -d '"') - # 0.28.1 -> 1.0.0-rc.0 -> 1.0.0-rc.1 -> 1.0.0. Bun's own "major" would jump an rc to 2.0.0. + # 0.28.1 -> 1.0.0-rc.1 -> 1.0.0-rc.2 -> 1.0.0. Series boundaries set the version explicitly: + # Bun's prerelease counter starts at 0, and its "major" on an rc would jump to 2.0.0. if [[ "$CURRENT" == *-rc.* ]]; then - INCREMENT=$([ "$CHANNEL" = rc ] && echo prerelease || echo "${CURRENT%%-*}") + NEXT=$([ "$CHANNEL" = rc ] && echo prerelease || echo "${CURRENT%%-*}") else - INCREMENT=$([ "$CHANNEL" = rc ] && echo "pre$BUMP" || echo "$BUMP") + NEXT=$(bun pm version "$BUMP" --no-git-tag-version) + NEXT=${NEXT#v} + [ "$CHANNEL" = rc ] && NEXT="$NEXT-rc.1" fi - TAG=$(bun pm version "$INCREMENT" --preid rc --no-git-tag-version) + TAG=$(bun pm version "$NEXT" --preid rc --no-git-tag-version --allow-same-version) if git rev-parse -q --verify "refs/tags/$TAG" >/dev/null; then echo "::error::$TAG is already released" exit 1 diff --git a/.github/workflows/verify.yml b/.github/workflows/verify.yml index 96ecde47b..7f100e567 100644 --- a/.github/workflows/verify.yml +++ b/.github/workflows/verify.yml @@ -46,19 +46,20 @@ jobs: - uses: oven-sh/setup-bun@v2 - run: bun install --frozen-lockfile + # Every static check reports even when an earlier one fails. Build and tests only run when all pass. - run: bun run lint:check - if: runner.os == 'Linux' + if: runner.os == 'Linux' && !cancelled() - run: bun run format:check - if: runner.os == 'Linux' + if: runner.os == 'Linux' && !cancelled() - run: bun run typecheck - if: runner.os == 'Linux' + if: runner.os == 'Linux' && !cancelled() - run: bun audit - if: runner.os == 'Linux' + if: runner.os == 'Linux' && !cancelled() - run: bun run secrets:check - if: runner.os == 'Linux' + if: runner.os == 'Linux' && !cancelled() - run: bun run build - - run: bun pm pack + - run: bun pm pack --ignore-scripts - run: bun run compile:${{ matrix.target }} - name: Smoke test binary run: ${{ matrix.binary }} --help diff --git a/package.json b/package.json index 44dfd9a98..bd51c5281 100644 --- a/package.json +++ b/package.json @@ -7,6 +7,7 @@ }, "module": "src/index.ts", "type": "module", + "packageManager": "bun@1.4.0", "bin": { "agentcore": "./dist/index.js" }, diff --git a/src/handlers/update/index.tsx b/src/handlers/update/index.tsx index d4b296003..f19a8667c 100644 --- a/src/handlers/update/index.tsx +++ b/src/handlers/update/index.tsx @@ -9,8 +9,8 @@ import { PACKAGE_VERSION } from "../../constants"; const PACKAGE_NAME = "@aws/agentcore"; const REGISTRY_URL = "https://registry.npmjs.org"; -function distTag(): string { - return PACKAGE_VERSION.includes("-") ? "preview" : "latest"; +export function distTag(version: string = PACKAGE_VERSION): string { + return semver.prerelease(version)?.[0]?.toString() ?? "latest"; } export function installArgv(): string[] { @@ -20,7 +20,7 @@ export function installArgv(): string[] { export async function fetchLatestVersion(): Promise { let response: Response; try { - response = await fetch(`${REGISTRY_URL}/${PACKAGE_NAME}/latest`); + response = await fetch(`${REGISTRY_URL}/${PACKAGE_NAME}/${distTag()}`); } catch (cause) { throw new NetworkingError( `Could not reach the npm registry: ${cause instanceof Error ? cause.message : String(cause)}`, diff --git a/src/handlers/update/update.test.ts b/src/handlers/update/update.test.ts index 7ce889e9d..5be838a57 100644 --- a/src/handlers/update/update.test.ts +++ b/src/handlers/update/update.test.ts @@ -1,5 +1,5 @@ import { afterEach, describe, expect, mock, spyOn, test } from "bun:test"; -import { fetchLatestVersion, handleUpdate } from "./index"; +import { distTag, fetchLatestVersion, handleUpdate } from "./index"; import { NetworkingError } from "../../errors"; import type { ProcessRunner } from "../../io"; import { PACKAGE_VERSION } from "../../constants"; @@ -10,6 +10,14 @@ import { PACKAGE_VERSION } from "../../constants"; // (runProcess). There is nothing for that harness to record, so a fetch spy plus // an injected fake runner is the right, hermetic way to cover this command. +test.each([ + ["0.28.1", "latest"], + ["1.0.0-rc.1", "rc"], + ["1.0.0-preview.29", "preview"], +])("distTag(%s) tracks the %s dist-tag", (version, tag) => { + expect(distTag(version)).toBe(tag); +}); + describe("fetchLatestVersion", () => { afterEach(() => { spyOn(globalThis, "fetch").mockRestore();