mirror of
https://github.com/agentclientprotocol/claude-agent-acp.git
synced 2026-10-27 04:30:59 +00:00
Today nothing reaches npm between two release-please releases, so trying a merged but unreleased change means building from source. This adds a continuously published preview channel. Every CI-green push to `main` that is not a release merge publishes `<package.json version, patch bumped>-preview.N` under the `preview` dist-tag, tags the commit `v<version>`, and dispatches the same agent registry update a stable release does. With `main` at 0.73.0 that gives `0.73.1-preview.1`, `0.73.1-preview.2`, and so on; `N` restarts at 1 whenever a release moves the base. `latest` is untouched. ## Approach - **Added to `publish.yml` rather than a new workflow.** npm binds a trusted publisher to one repo + workflow filename, and a package gets only one. A separate file would fail OIDC and cost the stable path its publisher. - **Gated on the `CI` workflow succeeding** (`workflow_run`), so a red commit is never published. release-please's release merge is excluded by the head commit's author and subject — its `releases_created` output is not reachable from a different workflow run. - **`N` is the max of the npm registry and local git tags.** npm reserves versions forever, even after `unpublish`, so a reused `N` is a hard failure; but the registry is CDN-served and lags, which the tags cover. A registry read failure aborts rather than guessing. - **Base is patch+1, not release-please's prediction**, which moves mid-flight and would reset `N` under already-published previews. - **The version is applied to the CI working tree only**, never committed — `package.json` and the release-please manifest stay release-please's to own. - **`--tag preview` is mandatory:** `npm publish` defaults to `latest` even for a semver prerelease. - **Three jobs.** `publish-npm-preview` mirrors `publish-npm` and only publishes (no repo write access); `publish-tag-preview` only tags, so a failed tag is retryable with _Re-run failed jobs_ without re-publishing an immutable version; `trigger-registry-update` is shared with the stable path. Both downstream jobs gate on a `published` output the publish step sets, not on job status — the tag push happens after the publish, so a red job can still mean npm has the version. ## Next step The publish is `npm publish --dry-run` for now. The `workflow_run` trigger and the `release` environment's protected-branch policy mean this job cannot run off the default branch at all, so merging this is the only way to exercise the trigger, the gate and the version calculation — and with `published` unset, the rehearsal neither tags nor touches the registry. Once one run looks right, a follow-up drops `--dry-run` and adds `echo "published=true" >> "$GITHUB_OUTPUT"` to that step. Nothing else changes.
247 lines
11 KiB
YAML
247 lines
11 KiB
YAML
name: Publish and Release
|
|
on:
|
|
push:
|
|
branches:
|
|
- main
|
|
# Previews are gated on CI, so they hang off the CI workflow finishing rather than
|
|
# off the push itself. `branches` filters on the *validated* branch, which keeps
|
|
# every PR CI run from spawning a Publish and Release run with all jobs skipped.
|
|
workflow_run:
|
|
workflows: [CI]
|
|
types: [completed]
|
|
branches: [main]
|
|
workflow_dispatch:
|
|
inputs:
|
|
channel:
|
|
description: Which channel to publish
|
|
required: true
|
|
default: stable
|
|
type: choice
|
|
options:
|
|
- stable
|
|
- preview
|
|
ref:
|
|
description: Tag or commit to publish
|
|
required: true
|
|
type: string
|
|
publish_npm:
|
|
description: Publish the package before updating the registry (stable only)
|
|
required: true
|
|
default: true
|
|
type: boolean
|
|
|
|
jobs:
|
|
release-please:
|
|
if: ${{ github.event_name == 'push' }}
|
|
runs-on: ubuntu-latest
|
|
environment: release # Optional: for enhanced security
|
|
permissions:
|
|
contents: write
|
|
issues: write
|
|
pull-requests: write
|
|
steps:
|
|
# Generating a GitHub token, so that PRs and tags created by
|
|
# the release-please-action can trigger actions workflows.
|
|
- name: Generate GitHub token
|
|
uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1
|
|
id: generate-token
|
|
with:
|
|
# GitHub App ID secret name
|
|
app-id: ${{ secrets.RELEASE_PLZ_APP_ID }}
|
|
# GitHub App private key secret name
|
|
private-key: ${{ secrets.RELEASE_PLZ_APP_PRIVATE_KEY }}
|
|
- uses: googleapis/release-please-action@45996ed1f6d02564a971a2fa1b5860e934307cf7
|
|
id: release
|
|
with:
|
|
token: ${{ steps.generate-token.outputs.token }}
|
|
# Do not add release-type here. The action ignores config-file whenever
|
|
# release-type is set, which silently disables the pre-1.0 versioning
|
|
# rules in release-please-config.json and lets a breaking change jump
|
|
# straight to 1.0.0. The node release type is declared in that config
|
|
# instead, so nothing is lost by leaving this out.
|
|
config-file: release-please-config.json
|
|
manifest-file: .release-please-manifest.json
|
|
outputs:
|
|
release_created: ${{ steps.release.outputs.release_created }}
|
|
tag_name: ${{ steps.release.outputs.tag_name }}
|
|
|
|
publish-npm:
|
|
name: Publish to npm
|
|
runs-on: ubuntu-latest
|
|
needs: [release-please]
|
|
if: ${{ always() && ((github.event_name == 'workflow_dispatch' && inputs.channel == 'stable' && inputs.publish_npm) || needs.release-please.outputs.release_created == 'true') }}
|
|
environment: release # Optional: for enhanced security
|
|
permissions:
|
|
contents: read
|
|
id-token: write
|
|
steps:
|
|
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1
|
|
with:
|
|
ref: ${{ github.event_name == 'workflow_dispatch' && inputs.ref || github.ref }}
|
|
# Setup .npmrc file to publish to npm
|
|
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020
|
|
with:
|
|
node-version: "lts/*"
|
|
registry-url: "https://registry.npmjs.org"
|
|
- run: npm ci
|
|
- run: npm run build
|
|
- run: npm publish
|
|
|
|
# Same shape as publish-npm. Tagging is a separate job, so nothing here needs
|
|
# write access to the repository.
|
|
publish-npm-preview:
|
|
name: Publish preview to npm
|
|
runs-on: ubuntu-latest
|
|
# Every CI-green push to main gets a preview, except release-please's own release
|
|
# merge: merging that PR already published this exact tree as a stable version, so
|
|
# a preview of it would be a double release. release-please's `releases_created`
|
|
# output would be the sharper signal, but it belongs to the push-triggered run and
|
|
# this job runs in the workflow_run one, so the commit is checked instead — by
|
|
# author and by the subject release-please generates, either of which is enough.
|
|
if: >-
|
|
${{
|
|
(github.event_name == 'workflow_dispatch' && inputs.channel == 'preview') ||
|
|
(github.event_name == 'workflow_run'
|
|
&& github.event.workflow_run.conclusion == 'success'
|
|
&& github.event.workflow_run.event == 'push'
|
|
&& github.event.workflow_run.head_repository.full_name == github.repository
|
|
&& github.event.workflow_run.head_commit.author.name != 'acp-release-bot[bot]'
|
|
&& !startsWith(github.event.workflow_run.head_commit.message, 'chore(main): release '))
|
|
}}
|
|
# Same environment as the stable job, so the npm trusted-publisher binding holds.
|
|
# It carries no required reviewers, so previews never wait for an approval.
|
|
environment: release
|
|
permissions:
|
|
contents: read
|
|
id-token: write # npm OIDC trusted publishing
|
|
timeout-minutes: 15
|
|
# Serialise previews so two pushes cannot read the same registry state and compute
|
|
# the same -preview.N. Job-level rather than workflow-level: a workflow-level group
|
|
# would also serialise release-please, and cancelling a queued release-please run
|
|
# means a release PR that silently stops updating. The trade-off is that GitHub
|
|
# keeps only one run pending per group, so a third push landing while one preview
|
|
# runs and another waits drops the waiting one — that commit gets no preview, but a
|
|
# version is never reused.
|
|
concurrency:
|
|
group: publish-npm-preview
|
|
cancel-in-progress: false
|
|
steps:
|
|
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1
|
|
with:
|
|
# workflow_run runs default to the tip of the default branch, so the commit
|
|
# CI actually validated has to be asked for explicitly.
|
|
ref: ${{ github.event_name == 'workflow_dispatch' && inputs.ref || github.event.workflow_run.head_sha }}
|
|
# Brings the tags the version calculation reads as its floor. The repo is a
|
|
# few megabytes packed, so a full fetch is cheap and sidesteps every
|
|
# shallow-clone tag caveat.
|
|
fetch-depth: 0
|
|
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020
|
|
with:
|
|
node-version: "lts/*"
|
|
registry-url: "https://registry.npmjs.org"
|
|
# Ahead of the version bump, so this still validates the committed lockfile.
|
|
- run: npm ci
|
|
- name: Compute the preview version
|
|
id: preview
|
|
run: |
|
|
version="$(node scripts/next-preview-version.mjs)"
|
|
sha="$(git rev-parse HEAD)"
|
|
echo "version=$version" >> "$GITHUB_OUTPUT"
|
|
echo "sha=$sha" >> "$GITHUB_OUTPUT"
|
|
echo "Preview \`$version\` from \`$sha\`" >> "$GITHUB_STEP_SUMMARY"
|
|
- name: Apply the version to the working tree only
|
|
# Never committed: package.json and .release-please-manifest.json on main stay
|
|
# release-please's to own, and this only changes what goes into the tarball.
|
|
# `npm version` keeps package-lock.json's version fields in step, and
|
|
# --no-git-tag-version skips every git operation.
|
|
run: npm version "${{ steps.preview.outputs.version }}" --no-git-tag-version
|
|
- run: npm run build
|
|
# TODO(preview-releases): once one run has proved the trigger, the gate and the
|
|
# version calculation on main, drop --dry-run and add
|
|
# `echo "published=true" >> "$GITHUB_OUTPUT"` to this step. Nothing else needs
|
|
# touching — leaving `published` unset is what keeps the tag and the registry
|
|
# dispatch dormant while the publish is only a rehearsal. The workflow_run
|
|
# trigger and the `release` environment's protected-branch policy mean this job
|
|
# cannot be exercised from a feature branch at all.
|
|
- name: Publish
|
|
id: publish
|
|
# --tag is mandatory: npm publish defaults to `latest` even for a semver
|
|
# prerelease, which would point every plain `npm install` at a preview.
|
|
run: npm publish --dry-run --tag preview
|
|
outputs:
|
|
published: ${{ steps.publish.outputs.published }}
|
|
version: ${{ steps.preview.outputs.version }}
|
|
sha: ${{ steps.preview.outputs.sha }}
|
|
|
|
publish-tag-preview:
|
|
name: Tag the published preview
|
|
runs-on: ubuntu-latest
|
|
needs: publish-npm-preview
|
|
if: ${{ needs.publish-npm-preview.outputs.published == 'true' }}
|
|
environment: release
|
|
permissions:
|
|
contents: write # create refs/tags/v<version>
|
|
timeout-minutes: 5
|
|
steps:
|
|
# A re-run carries over the outputs of jobs it did not re-run. If that ever stops
|
|
# holding, fail with the manual recipe rather than tagging the wrong commit.
|
|
- name: Check the publish handed over a version and a commit
|
|
env:
|
|
VERSION: ${{ needs.publish-npm-preview.outputs.version }}
|
|
SHA: ${{ needs.publish-npm-preview.outputs.sha }}
|
|
run: |
|
|
if [ -z "$VERSION" ] || [ -z "$SHA" ]; then
|
|
echo "::error::publish-npm-preview reported version='$VERSION' sha='$SHA'." \
|
|
"Tag it by hand — see docs/RELEASES.md, 'A preview published but the" \
|
|
"commit was not tagged'."
|
|
exit 1
|
|
fi
|
|
- name: Create the tag
|
|
env:
|
|
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
VERSION: ${{ needs.publish-npm-preview.outputs.version }}
|
|
SHA: ${{ needs.publish-npm-preview.outputs.sha }}
|
|
run: |
|
|
gh api "repos/$GITHUB_REPOSITORY/git/refs" \
|
|
-f ref="refs/tags/v$VERSION" \
|
|
-f sha="$SHA"
|
|
echo "Tagged \`v$VERSION\` at \`$SHA\`" >> "$GITHUB_STEP_SUMMARY"
|
|
|
|
trigger-registry-update:
|
|
needs: [publish-npm, publish-npm-preview]
|
|
# Both channels dispatch this: the registry has its own handling for preview
|
|
# versions. The two never fire together — a release merge publishes stable and
|
|
# skips the preview, and every other push does the reverse — so the registry sees
|
|
# exactly one dispatch per published version. The payload is deliberately
|
|
# unchanged: it names no version, and the registry resolves what it needs itself.
|
|
if: >-
|
|
${{
|
|
always() && (
|
|
needs.publish-npm.result == 'success'
|
|
|| needs.publish-npm-preview.outputs.published == 'true'
|
|
|| (github.event_name == 'workflow_dispatch' && inputs.channel == 'stable' && !inputs.publish_npm)
|
|
)
|
|
}}
|
|
runs-on: ubuntu-latest
|
|
environment: release
|
|
permissions: {}
|
|
steps:
|
|
- name: Generate token scoped to the registry repo
|
|
uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1
|
|
id: registry-token
|
|
with:
|
|
app-id: ${{ secrets.REGISTRY_UPDATER_APP_ID }}
|
|
private-key: ${{ secrets.REGISTRY_UPDATER_APP_PRIVATE_KEY }}
|
|
owner: ${{ github.repository_owner }}
|
|
repositories: registry
|
|
|
|
- name: Dispatch registry version update
|
|
env:
|
|
GH_TOKEN: ${{ steps.registry-token.outputs.token }}
|
|
run: |
|
|
gh workflow run update-versions.yml \
|
|
--repo ${{ github.repository_owner }}/registry \
|
|
--ref main \
|
|
-f apply=true \
|
|
-f agents=claude-acp
|