../RELEASE.md defines the supported product contract. This document is the canonical maintainer process for validating, versioning, tagging, and publishing that contract on GitHub Releases and npm.
Releases are driven by release-please running as a library with Communique-generated changelog notes (ADR 0009). The short version: a workflow maintains one release PR at all times; merging that PR is the release decision — the tag, GitHub Release, and publish pipeline follow automatically.
- Ensure the intended npm package name remains
agent-tty. - On npm, configure trusted publishing for
agent-ttywith these GitHub Actions settings:- organization or user:
coder - repository:
agent-tty - workflow filename:
release.yml(the workflow file committed at.github/workflows/release.yml) - environment name: leave empty unless this workflow later adds a protected GitHub environment for publishing
- organization or user:
- After the first successful trusted publish, restrict the package's npm publishing access to require 2FA and disallow traditional tokens.
- Keep the package metadata aligned with npm provenance expectations:
package.json.namemust stayagent-ttypackage.json.repository.urlmust staygit+https://github.com/coder/agent-tty.gitpackage.json.publishConfig.registrymust stayhttps://registry.npmjs.org/
- No GitHub Actions secret is required for npm publishing in this flow; the workflow uses GitHub-hosted runners plus OIDC trusted publishing.
Three workflows cooperate:
- Release Please (
.github/workflows/release-please.yml) runs on every push tomain. It executessrc/tools/release-please-runner.ts, which runs release-please as a library with Communique registered as the changelog generator ("changelog-type": "communique"inrelease-please-config.json). Each run:- tags and creates the GitHub Release for any release PR that merged since the last run, then dispatches the Release workflow for that tag;
- opens or updates the single release PR (title
chore(release): <version>, head branchrelease-please--branches--main), carrying thepackage.jsonversion bump plus a newCHANGELOG.mdsection written by Communique (communique generate HEAD <last-tag> --concise); - dispatches CI and Validate skills onto the release branch (pushes made with the workflow token never trigger
pull_requestworkflows on their own).
- Release (
.github/workflows/release.yml) is the publish pipeline (see below). It is dispatched by Release Please after the tag exists, and still also triggers on manually pushedv*tags. - CI runs on the release PR like on any other PR and gates the merge.
Configuration lives in release-please-config.json (release behavior) and .release-please-manifest.json (the version release-please considers current; release-please updates it through release PRs).
The next version is computed from Conventional Commit subjects on main since the last release. While the package is pre-1.0 the config maps:
- breaking changes (
feat!:,BREAKING CHANGE:) → minor bump (bump-minor-pre-major) feat:/fix:/ everything else releasable → patch bump (bump-patch-for-minor-pre-major)
To force a specific version (for example the eventual 1.0.0, or a prerelease), land an empty commit with a Release-As footer and let the workflow rebuild the PR:
git commit --allow-empty -m "chore: force release" -m "Release-As: 1.0.0"-
CHANGELOG.mdkeeps a## [Unreleased]section at the top, normally empty. Communique refuses to run when the heading is missing, and it feeds the section's body to the LLM as draft material to reconcile into the notes it generates. The release PR's livingCHANGELOG.mddiff and body are still the authoritative view of the pending release. -
New release sections are inserted below the
[Unreleased]anchor (a custom updater in the runner replaces release-please's stock one, which would insert above it), and the release PR clears any reconciled draft body so it cannot leak into later releases. -
New sections use the heading
## [<version>] - <date>(novinside the brackets — release-please parses the version back out of the merged PR body, and its parser requires the bracket to be followed by a digit; entries before v0.4.2 keep the historical[v<version>]style). -
Notes are regenerated by Communique on every push to
main, so manual edits to the release PR (branch or body) are overwritten by the next push. To shape wording, improve the source material instead:- stage draft notes under
## [Unreleased]in a small dedicated PR — Communique reconciles them into the generated section, and the release PR clears them on merge; - or edit the merged feature PR descriptions/titles, or add a
BEGIN_COMMIT_OVERRIDEblock to a merged PR body.
(After the release,
gh release editcan still fix the published notes.) - stage draft notes under
-
Feature PRs must not add release entries to
CHANGELOG.md; the versioned sections are written only through release PRs.
- Re-read
../RELEASE.mdand confirm it still matches the shipped surface. - Verify the primary docs route correctly from
../README.mdto release, design, and dogfood materials. - Review
../dogfood/CATALOG.mdand make sure the release-signoff bundle is current and easy to find. - Confirm the published package metadata still points at
agent-ttyand the public GitHub repository. - Confirm release-note automation has an LLM provider secret available in GitHub Actions:
- default/recommended:
ANTHROPIC_API_KEY - OpenAI-compatible fallback:
OPENAI_API_KEYplus a repository variable namedCOMMUNIQUE_MODEL
- default/recommended:
This flow assumes gh can inspect checks, approve, and merge PRs. gh auth status is useful for a quick summary, but in some environments it can report a stale or misleading state even when real GitHub API calls still work.
Before treating release automation as blocked, verify with a real API call such as:
gh api graphql -f query='query { viewer { login } }'If that succeeds, prefer the result of the real gh operation over the status summary.
The release PR runs the same CI as any other PR, and the publish pipeline reruns the full quality bar against the tagged commit before any asset is built. For local validation use mise:
mise run ciGitHub Actions installs mise-managed tools from the committed ../mise.lock with --locked. If mise.toml tool versions or supported CI platforms change, regenerate the lock before merging:
mise lockIf mise is unavailable, run:
npm run verifyIf the public bootstrap under skills/ or the bundled runtime skills under skill-data/ changed, also run:
npm run intent:validateUse the same release packer that CI relies on:
RELEASE_DIR=$(mktemp -d)
npm run build
npm run pack:release -- --pack-destination "$RELEASE_DIR" --metadata-file "$RELEASE_DIR/package-metadata.json"
cat "$RELEASE_DIR/package-metadata.json"
sha256sum -c "$RELEASE_DIR"/*.tgz.sha256When skill packaging changes, also inspect npm pack --dry-run output to confirm the tarball still includes both skills/ (bootstrap) and skill-data/ (runtime skills).
-
Open the current release PR (title
chore(release): <version>). If it is missing or stale, trigger the Release Please workflow manually (gh workflow run release-please.yml) or push tomain. -
Review the proposed version and the
CHANGELOG.mdsection. If the version is wrong, land aRelease-Ascommit (see above) rather than editing the PR. If notes are wrong, fix the source material (feature PR bodies / commit overrides) and let the next push rebuild them. -
Wait for the dispatched checks on the release branch to pass. If they were never dispatched (for example after a manual branch poke), dispatch them yourself:
BRANCH=$(gh pr list --search 'chore(release) in:title' --state open --json headRefName --jq '.[0].headRefName') gh workflow run ci.yml --ref "$BRANCH" gh workflow run validate-skills.yml --ref "$BRANCH"
-
Approve and merge the release PR (squash, like any other PR). The release PR is authored by the workflow bot, so a maintainer approval satisfies the required-review ruleset — no admin bypass is needed.
-
The merge push triggers Release Please again, which:
- creates the
v<version>tag on the merge commit and the GitHub Release (notes = the changelog section from the merged PR body), - dispatches the Release workflow for the tag.
- creates the
-
Watch the Release workflow to completion, then verify (sections below). The GitHub Release exists for a few minutes before assets attach and before the editorial notes replace the changelog section — the notes themselves tell readers npm install becomes available once the publish job completes.
The hand-curated workflow lives at .github/workflows/release.yml.
It is dispatched by Release Please for new tags, triggers automatically on manually pushed v* tags, and can be rerun for an existing tag via the Release workflow's tag input.
The workflow will:
- resolve the release tag and check out that exact ref,
- install mise-managed tools from the committed lock file,
- verify the tagged commit is already reachable from the default branch,
- validate that the tag matches the
package.jsonversion, - run
mise run ci, - pack the verified tarball with
npm run pack:release, - upload the tarball, checksum, and metadata JSON as workflow artifacts,
- generate editorial Communique release notes for the tag,
- update the GitHub Release with those notes plus the deterministic install/checksum block and the
.tgz/.sha256assets attached, - and publish that same verified tarball to npm via trusted publishing on a GitHub-hosted runner.
Stable releases publish with npm's default latest dist-tag.
Prerelease versions publish with the prerelease identifier as the dist-tag, so 0.1.1-beta.0 publishes to the beta dist-tag and 0.1.1-rc.1 publishes to the rc dist-tag.
After the workflow succeeds, verify the exact npm package version before announcing the release. Run these checks under Node 24; if your interactive shell is older, point NODE_BIN at an explicit Node 24 binary first.
PACKAGE_NAME='agent-tty'
PACKAGE_VERSION='<version>'
NODE_BIN=${NODE_BIN:-node}
INSTALL_PREFIX=$(mktemp -d)
AGENT_TTY_HOME=$(mktemp -d)
npm view "$PACKAGE_NAME" dist-tags --json
npm install -g --prefix "$INSTALL_PREFIX" "$PACKAGE_NAME@$PACKAGE_VERSION"
"$NODE_BIN" "$INSTALL_PREFIX/bin/agent-tty" version --json | jq -r '.result.cliVersion'
"$NODE_BIN" "$INSTALL_PREFIX/bin/agent-tty" --home "$AGENT_TTY_HOME" doctor --json | jq '.result.ok'doctor --json uses the outer ok field for command-envelope success; the release-health signal is .result.ok.
If the release is a prerelease, also confirm the intended dist-tag points at the exact published version:
PACKAGE_NAME='agent-tty'
PACKAGE_VERSION='<version>'
DIST_TAG=$(node --input-type=module <<'EOF_NODE'
const version = process.env.PACKAGE_VERSION;
if (!version.includes('-')) {
process.stdout.write('latest');
process.exit(0);
}
const prerelease = version.split('-', 2)[1] ?? '';
const distTag = prerelease.split('.', 1)[0] ?? '';
if (distTag.length === 0) {
throw new Error(`unable to derive dist-tag from ${version}`);
}
process.stdout.write(distTag);
EOF_NODE
)
npm view "$PACKAGE_NAME" dist-tags --json
printf 'expected dist-tag %s for %s\n' "$DIST_TAG" "$PACKAGE_VERSION"Also verify the hosted tarball fallback before announcing the release. Run these checks under Node 24 for the same reason as the npm verification above.
VERSION=<version>
RELEASE_TAG="v${VERSION}"
RELEASE_TGZ="agent-tty-${VERSION}.tgz"
NODE_BIN=${NODE_BIN:-node}
DOWNLOAD_DIR=$(mktemp -d)
INSTALL_PREFIX=$(mktemp -d)
AGENT_TTY_HOME=$(mktemp -d)
gh release download "$RELEASE_TAG" --repo coder/agent-tty --dir "$DOWNLOAD_DIR" --pattern "$RELEASE_TGZ"
gh release download "$RELEASE_TAG" --repo coder/agent-tty --dir "$DOWNLOAD_DIR" --pattern "${RELEASE_TGZ}.sha256"
(
cd "$DOWNLOAD_DIR"
sha256sum -c "${RELEASE_TGZ}.sha256"
)
npm install -g --prefix "$INSTALL_PREFIX" "$DOWNLOAD_DIR/$RELEASE_TGZ"
"$NODE_BIN" "$INSTALL_PREFIX/bin/agent-tty" version --json | jq -r '.result.cliVersion'
"$NODE_BIN" "$INSTALL_PREFIX/bin/agent-tty" --home "$AGENT_TTY_HOME" doctor --json | jq '.result.ok'For private releases, authenticated download is the expected verification route.
If you are testing a public release and the direct asset URL is reachable in your environment, you can also verify the hosted install path directly with npm install -g <release-asset-url>.
Trigger the workflow again — it rebuilds the PR from scratch on every run:
gh workflow run release-please.ymlWrong version → land a Release-As commit. Wrong notes → fix the merged feature PR bodies (or add BEGIN_COMMIT_OVERRIDE blocks) and rerun. Never edit the release PR directly; edits are overwritten.
The Release Please run triggered by the merge failed before createReleases() finished. Rerun it — release creation is idempotent (it finds merged release PRs still labeled autorelease: pending):
gh workflow run release-please.ymlFix the underlying issue, then rerun the publish pipeline for the existing tag:
gh workflow run release.yml --field tag=v<version>If npm publish succeeds, never reuse the same version, even if later GitHub Release asset creation or verification fails. Repair forward with a new version, or complete missing release assets manually according to maintainer policy.
If the GitHub Release exists but npm publish fails, treat the release as partial. Verify which assets and npm state exist, then follow maintainer policy before deleting assets, deleting tags, or retrying publish automation.
Manual tagging (git tag + push, or gh release create) still triggers the publish pipeline, but it bypasses release-please's bookkeeping: .release-please-manifest.json keeps the old version, so the next release PR computes its version and commit range from the wrong baseline. After any manual release, open a PR updating .release-please-manifest.json (and package.json if it was not bumped) to the manually released version.
- Keep at least one current release-readiness bundle under
dogfood/. - Keep evergreen scenario bundles easy to discover from the dogfood catalog.
- When a change affects release, packaging, install, renderer, screenshot, wait, export, or review UX, include screenshots and recordings in the relevant proof bundle when feasible.