mirror of
https://github.com/agentclientprotocol/claude-agent-acp.git
synced 2026-10-27 04:30:59 +00:00
ci: simplify the release flow and make it agent-friendly (#965)
This commit is contained in:
6
.github/workflows/publish.yml
vendored
6
.github/workflows/publish.yml
vendored
@@ -39,7 +39,11 @@ jobs:
|
||||
id: release
|
||||
with:
|
||||
token: ${{ steps.generate-token.outputs.token }}
|
||||
release-type: node
|
||||
# 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:
|
||||
|
||||
42
AGENTS.md
Normal file
42
AGENTS.md
Normal file
@@ -0,0 +1,42 @@
|
||||
# Agent guide
|
||||
|
||||
An ACP-compatible coding agent powered by the Claude Agent SDK. Source in `src/`,
|
||||
TypeScript, ESM, Node >= 22.
|
||||
|
||||
## Commands
|
||||
|
||||
```sh
|
||||
npm run build # tsc
|
||||
npm run test:run # vitest, single pass
|
||||
npm run check # eslint + prettier --check
|
||||
npm run lint:fix # eslint --fix
|
||||
npm run format # prettier --write
|
||||
```
|
||||
|
||||
CI runs `format:check`, `lint`, `build` and `test:run`. Run `npm run check` before
|
||||
opening a PR.
|
||||
|
||||
## Pull requests
|
||||
|
||||
Squash merges use the PR title as the commit subject, and release-please parses it
|
||||
to compute the next version. Titles must be conventional commits using one of:
|
||||
`feat`, `fix`, `perf`, `revert`, `docs`, `style`, `chore`, `refactor`, `test`,
|
||||
`build`, `ci`. `conventional-prs.yml` rejects anything else.
|
||||
|
||||
The title also decides the release: `feat:` bumps the minor, `fix:`/`perf:` the
|
||||
patch, and `chore:`/`ci:`/`test:` and friends do not release at all.
|
||||
|
||||
## Releasing
|
||||
|
||||
Releases are fully automated by release-please. There is no manual release
|
||||
workflow, and the version is never chosen by hand — it follows from the commit
|
||||
history.
|
||||
|
||||
```sh
|
||||
npm run release:preflight # verifies it is safe to release, prints the PR and version
|
||||
gh pr merge <pr-number> --squash
|
||||
```
|
||||
|
||||
The preflight is the guard-list as code; if it exits non-zero, follow what it
|
||||
prints rather than merging. Full runbook, including how to recover a stalled
|
||||
release: [`docs/RELEASES.md`](docs/RELEASES.md).
|
||||
109
docs/RELEASES.md
109
docs/RELEASES.md
@@ -1,24 +1,111 @@
|
||||
## How to publish a new release
|
||||
# Releases
|
||||
|
||||
1. Create a new branch prep-vX.X.X
|
||||
Releases are automated by [release-please](https://github.com/googleapis/release-please)
|
||||
in [`.github/workflows/publish.yml`](../.github/workflows/publish.yml). Every push
|
||||
to `main` re-computes the next version from the conventional commit messages and
|
||||
keeps a single release PR open, titled `chore(main): release X.Y.Z` and labelled
|
||||
`autorelease: pending`.
|
||||
|
||||
Merging that PR is what releases. It tags `vX.Y.Z`, creates the GitHub release,
|
||||
publishes to npm, and dispatches a version update to the agent registry.
|
||||
|
||||
There is no manual release button, and versions are never typed in by hand: the
|
||||
version is an output of the commit history, not an input.
|
||||
|
||||
## Releasing
|
||||
|
||||
```sh
|
||||
git checkout -b prep-vX.X.X
|
||||
npm run release:preflight
|
||||
```
|
||||
|
||||
2. Bump the version in package.json
|
||||
This reports the open release PR, the version it will ship, and checks that the
|
||||
repository is in a state where merging is safe. Nothing has to be remembered —
|
||||
if it exits non-zero, follow what it prints instead of merging.
|
||||
|
||||
Then merge it, using the PR number the preflight printed:
|
||||
|
||||
```sh
|
||||
npm version vX.X.X
|
||||
gh pr merge <pr-number> --squash
|
||||
gh run watch "$(gh run list --workflow=publish.yml --limit 1 --json databaseId --jq '.[0].databaseId')"
|
||||
```
|
||||
|
||||
3. Add a new entry in CHANGELOG.md
|
||||
4. Create a PR for `prev-vX.X.X` & merge it
|
||||
5. Create tag and push it
|
||||
The run is looked up rather than picked interactively, so this is safe to script.
|
||||
If the workflow has already finished, `gh run list --workflow=publish.yml` shows
|
||||
the outcome instead.
|
||||
|
||||
Merging main requires no review, so a green preflight and `Build` are the only
|
||||
gates. Once the workflow finishes, confirm both outputs landed:
|
||||
|
||||
```sh
|
||||
git tag vX.X.X
|
||||
git push origin vX.X.X
|
||||
gh release view "v<version>"
|
||||
npm view "@agentclientprotocol/claude-agent-acp@<version>"
|
||||
```
|
||||
|
||||
6. Paste CHANGELOG into release notes and publish the version on GitHub
|
||||
## How the version is chosen
|
||||
|
||||
Squash merges use the PR title as the commit subject, so the PR title decides the
|
||||
next version. [`conventional-prs.yml`](../.github/workflows/conventional-prs.yml)
|
||||
rejects titles release-please would not understand.
|
||||
|
||||
| PR title prefix | Effect |
|
||||
| --------------------------------------------------------- | --------------------------- |
|
||||
| `fix:`, `perf:`, `revert:`, `docs:` | patch, e.g. 0.66.0 → 0.66.1 |
|
||||
| `feat:` | minor, e.g. 0.66.0 → 0.67.0 |
|
||||
| any of the above with `!`, or BREAKING CHANGE | minor while below 1.0.0 |
|
||||
| `chore:`, `ci:`, `build:`, `test:`, `refactor:`, `style:` | no release on their own |
|
||||
|
||||
Breaking changes bump the minor rather than the major because
|
||||
`bump-minor-pre-major` is set in
|
||||
[`release-please-config.json`](../release-please-config.json) and the package is
|
||||
still below 1.0.0. That is deliberate: it keeps a single `!` in a PR title from
|
||||
shipping 1.0.0 by accident.
|
||||
|
||||
Note that `config-file` only takes effect while the workflow does **not** pass a
|
||||
`release-type` input to the action — with `release-type` set, the action ignores
|
||||
the config entirely. The release type is declared inside the config instead.
|
||||
|
||||
Releasing 1.0.0 is therefore an explicit act: add `"release-as": "1.0.0"` to
|
||||
`release-please-config.json` in its own PR, release, then remove it again.
|
||||
|
||||
## Recovering a stalled release
|
||||
|
||||
### The release PR merged but nothing was tagged
|
||||
|
||||
The preflight fails with `release-please is jammed`. While a merged release PR
|
||||
still carries `autorelease: pending`, release-please refuses to open any new
|
||||
release PR at all, so every later release stalls silently until this is cleared.
|
||||
|
||||
Take the release notes release-please already wrote into the changelog, create
|
||||
the missing release, then move the label the way release-please would have:
|
||||
|
||||
```sh
|
||||
awk '/^## \[<version>\]/{f=1;print;next} /^## \[/{f=0} f' CHANGELOG.md > notes.md
|
||||
gh release create "v<version>" --target <merge-commit-sha> --notes-file notes.md
|
||||
gh pr edit <pr-number> --remove-label "autorelease: pending" \
|
||||
--add-label "autorelease: tagged"
|
||||
```
|
||||
|
||||
Then publish the tag as described below.
|
||||
|
||||
### The tag exists but npm or the registry is missing
|
||||
|
||||
npm publishes through OIDC from inside the workflow, so this cannot be done from
|
||||
a laptop. Re-run the publish workflow against the existing tag:
|
||||
|
||||
```sh
|
||||
gh workflow run publish.yml -f ref="v<version>" -f publish_npm=true
|
||||
```
|
||||
|
||||
npm versions are immutable. If the package already published and only the
|
||||
registry update failed, pass `-f publish_npm=false` so the run skips publishing
|
||||
and only re-dispatches the registry update.
|
||||
|
||||
## Credentials
|
||||
|
||||
| Secret | Used for |
|
||||
| ------------------------------------------------------------- | ----------------------------------------------------------------- |
|
||||
| `RELEASE_PLZ_APP_ID`, `RELEASE_PLZ_APP_PRIVATE_KEY` | App token for release PRs and tags, so they can trigger workflows |
|
||||
| `REGISTRY_UPDATER_APP_ID`, `REGISTRY_UPDATER_APP_PRIVATE_KEY` | App token scoped to the `registry` repository |
|
||||
|
||||
Publishing to npm uses OIDC trusted publishing, so there is no npm token. All
|
||||
release jobs run in the `release` environment.
|
||||
|
||||
@@ -35,6 +35,7 @@
|
||||
"format": "prettier --write .",
|
||||
"format:check": "prettier --check .",
|
||||
"check": "npm run lint && npm run format:check",
|
||||
"release:preflight": "bash scripts/release-preflight.sh",
|
||||
"test": "vitest",
|
||||
"test:integration": "RUN_INTEGRATION_TESTS=true vitest run",
|
||||
"test:run": "vitest run",
|
||||
|
||||
@@ -1,12 +1,10 @@
|
||||
{
|
||||
"bump-minor-pre-major": true,
|
||||
"bump-patch-for-minor-pre-major": true,
|
||||
"packages": {
|
||||
".": {
|
||||
"changelog-path": "CHANGELOG.md",
|
||||
"release-type": "node",
|
||||
"bump-minor-pre-major": true,
|
||||
"bump-patch-for-minor-pre-major": true
|
||||
"bump-minor-pre-major": true
|
||||
}
|
||||
},
|
||||
"$schema": "https://raw.githubusercontent.com/googleapis/release-please/main/schemas/config.json"
|
||||
|
||||
103
scripts/release-preflight.sh
Executable file
103
scripts/release-preflight.sh
Executable file
@@ -0,0 +1,103 @@
|
||||
#!/usr/bin/env bash
|
||||
#
|
||||
# Preflight checks for a release.
|
||||
#
|
||||
# release-please decides what the next version is; this script only verifies the
|
||||
# repository is in a state where merging the open release PR produces a correct
|
||||
# release. Merging main needs no review, so these checks are the only thing
|
||||
# standing between a mistake and a published package — keep them as code rather
|
||||
# than as a list someone has to remember. See docs/RELEASES.md.
|
||||
#
|
||||
# Requires an authenticated GitHub CLI. Nothing else: gh has a jq engine builtin.
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
PENDING_LABEL="autorelease: pending"
|
||||
|
||||
fail() {
|
||||
printf 'FAIL %s\n' "$1" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
pass() {
|
||||
printf 'ok %s\n' "$1"
|
||||
}
|
||||
|
||||
command -v gh >/dev/null 2>&1 ||
|
||||
fail "GitHub CLI (gh) is not installed."
|
||||
gh auth status >/dev/null 2>&1 ||
|
||||
fail "GitHub CLI is not authenticated. Run 'gh auth login'."
|
||||
|
||||
repo=$(gh repo view --json nameWithOwner --jq .nameWithOwner 2>/dev/null) ||
|
||||
fail "cannot work out which GitHub repository this is. Run from a checkout."
|
||||
|
||||
# A merged release PR that still carries the pending label means release-please
|
||||
# never tagged it. In that state it refuses to open any new release PR at all,
|
||||
# so every later release stalls silently until it is recovered by hand.
|
||||
stuck=$(gh pr list --repo "$repo" --state merged --label "$PENDING_LABEL" \
|
||||
--json number --jq 'map("#\(.number)") | join(", ")')
|
||||
[ -z "$stuck" ] ||
|
||||
fail "merged release PR still labelled '$PENDING_LABEL': $stuck.
|
||||
release-please is jammed and will not open new release PRs.
|
||||
See docs/RELEASES.md, 'Recovering a stalled release'."
|
||||
pass "no untagged merged release PR"
|
||||
|
||||
rows=$(gh pr list --repo "$repo" --state open --label "$PENDING_LABEL" \
|
||||
--json number,title,headRefName --jq '.[] | [.number, .headRefName, .title] | @tsv')
|
||||
count=$(printf '%s\n' "$rows" | grep -c . || true)
|
||||
|
||||
if [ "$count" -eq 0 ]; then
|
||||
fail "no open release PR.
|
||||
Nothing has landed since the last tag that release-please puts in a
|
||||
changelog, so there is nothing to release. Land a feat/fix/perf/revert/docs
|
||||
change first, or check that the Publish and Release workflow is running."
|
||||
fi
|
||||
if [ "$count" -gt 1 ]; then
|
||||
fail "$count open release PRs, expected exactly one:
|
||||
$rows"
|
||||
fi
|
||||
IFS=$(printf '\t') read -r pr_number head_ref pr_title <<EOF
|
||||
$rows
|
||||
EOF
|
||||
pass "one open release PR: #$pr_number ($pr_title)"
|
||||
|
||||
# release-please reads the version to tag from the PR title, while npm publishes
|
||||
# whatever package.json says. If those disagree the tag and the published package
|
||||
# describe different releases, so check all three sources against each other.
|
||||
case $pr_title in
|
||||
*"release "*) title_version=${pr_title##*release } ;;
|
||||
*) fail "cannot read a version from PR title '$pr_title'." ;;
|
||||
esac
|
||||
pkg_version=$(gh api "repos/$repo/contents/package.json?ref=$head_ref" \
|
||||
-H "Accept: application/vnd.github.raw" --jq .version)
|
||||
manifest_version=$(gh api "repos/$repo/contents/.release-please-manifest.json?ref=$head_ref" \
|
||||
-H "Accept: application/vnd.github.raw" --jq '."."')
|
||||
|
||||
if [ "$title_version" != "$pkg_version" ] || [ "$title_version" != "$manifest_version" ]; then
|
||||
fail "version mismatch on #$pr_number:
|
||||
PR title $title_version
|
||||
package.json $pkg_version
|
||||
.release-please-manifest.json $manifest_version"
|
||||
fi
|
||||
pass "version agrees across PR title, package.json and manifest: $title_version"
|
||||
|
||||
if gh release view "v$title_version" --repo "$repo" >/dev/null 2>&1; then
|
||||
fail "tag v$title_version already exists. Merging would try to release it twice."
|
||||
fi
|
||||
pass "tag v$title_version does not exist yet"
|
||||
|
||||
build=$(gh pr view "$pr_number" --repo "$repo" --json statusCheckRollup --jq '
|
||||
[.statusCheckRollup[]? | select(.name == "Build")]
|
||||
| if length == 0 then "MISSING" else (.[0].conclusion // .[0].status // "PENDING") end')
|
||||
[ "$build" = "SUCCESS" ] ||
|
||||
fail "required check 'Build' is $build on #$pr_number, expected SUCCESS."
|
||||
pass "required check 'Build' passed"
|
||||
|
||||
cat <<EOF
|
||||
|
||||
Ready to release $title_version.
|
||||
|
||||
gh pr merge $pr_number --squash
|
||||
|
||||
Merging tags v$title_version, publishes to npm and updates the agent registry.
|
||||
EOF
|
||||
Reference in New Issue
Block a user