docs: add RELEASING.md runbook, collapse duplicated release docs into it

The release process was restated in four places, and README still described
the pre-automation flow ("commit the version bump, run release.sh, this
creates a git tag") — which no longer exists. That is exactly how the copies
drift apart.

RELEASING.md is now the single source of truth: how to cut a release, what
fires on merge, what sync-versions owns, how to verify, how to recover when
a publish fails, and the non-obvious design constraints (GITHUB_TOKEN not
triggering tag workflows, tag-existence keying, the metadata-action value=
pin, why release.sh no longer tags, and that CI is advisory since main has
no branch protection).

Everything else links to it instead of restating:
- CLAUDE.md keeps a short pointer + the plan-facing release checklist
- DEPLOYMENT.md goes back to being about deploy targets
- README gets the corrected two-line version
- CONTRIBUTING gains a maintainers-only Releasing section and a "don't bump
  the version in your PR" note

Verified: every relative link and heading anchor across the five root docs
resolves, no stale references to the old process remain outside thoughts/
(historical by design), and the package/workflow names cited in the runbook
match the actual manifests.

Claude-Session: https://claude.ai/code/session_01VCcgVD3bXuuPtYkoVJjZM3
This commit is contained in:
2pac
2026-08-06 01:31:07 +02:00
parent 7c59f05dd1
commit 5900e205bd
5 changed files with 190 additions and 58 deletions
+6 -15
View File
@@ -15,34 +15,25 @@ Bun monorepo: `packages/{core, cli, mcp, server}`
## Releasing & Deployment
See [DEPLOYMENT.md](./DEPLOYMENT.md) for full deployment details (npm publishing, secrets, install methods).
**[RELEASING.md](./RELEASING.md) is the source of truth for the release process** — read it before touching versions, release scripts, or the publish workflows. Do not restate its mechanics elsewhere; link to it.
## Release Steps
See [DEPLOYMENT.md](./DEPLOYMENT.md) for deployment targets (npm package contents, Docker/GHCR, Fly.io, env vars).
**Releases are automatic.** A version change landing on `main` is the trigger — there is no manual tagging step.
Short version: releases are automatic. A version change landing on `main` triggers tag + publish.
```bash
./scripts/release.sh 0.13.0 # sync every version target, commit, push
```
On a branch this just pushes the bump; open a PR and the release fires when it merges. Run it on `main` (or merge the bump) and `.github/workflows/auto-release.yml` takes over:
1. Runs `sync-versions.ts --check`**fails the release** if any version target drifted
2. Tags `v{version}` if that tag doesn't exist yet
3. Dispatches `npm-publish.yml` (typecheck + build + test, then npm with provenance + GitHub Release) and `docker-publish.yml` (multi-arch GHCR image)
It is keyed on "does the tag for the current root version exist", so it is idempotent — reruns resume rather than duplicate, and a bump that reaches `main` untagged is picked up on the next push.
Never bump the version by hand-editing `package.json` alone. `sync-versions.ts` owns all 12 targets (7 sub-package manifests, the CLI's `optionalDependencies` pins, `Cargo.toml`, `Cargo.lock`, `.claude-plugin/plugin.json`); CI rejects a partial bump on the PR.
Never bump a version by hand-editing `package.json``scripts/sync-versions.ts` owns every place the version appears, and CI fails a partial bump.
## Release Checklist (applies to plans, research, and Plan mode)
When making changes to core ops, CLI commands, or MCP tools, always check:
1. **Skill update** — If a new op/command was added or existing behavior changed, update `skills/agent-fs/SKILL.md` (command tables, description triggers, workflow examples).
2. **Plugin version bump** — Handled by `sync-versions.ts`; never edit `.claude-plugin/plugin.json` by hand.
3. **Package version bump** — Run `./scripts/release.sh <version>` (patch for fixes/features, minor for breaking changes). This bumps every target at once and, once merged to `main`, publishes automatically.
4. **E2E coverage** — If a new op was added, add corresponding tests to `scripts/e2e.ts`.
2. **E2E coverage** — If a new op was added, add corresponding tests to `scripts/e2e.ts`.
3. **Version bump** — Run `./scripts/release.sh <version>` (patch for fixes/features, minor for breaking changes). This bumps every target at once — including `.claude-plugin/plugin.json`, which must never be edited by hand — and publishes automatically once it reaches `main`.
Plans and research documents MUST include these as explicit steps when they involve core/CLI/MCP changes.
+18
View File
@@ -8,6 +8,8 @@ Thanks for your interest in contributing to Agent FS!
- [Running the Project](#running-the-project)
- [Code Quality](#code-quality)
- [Project Structure](#project-structure)
- [Submitting Changes](#submitting-changes)
- [Releasing](#releasing)
---
@@ -87,4 +89,20 @@ agent-fs/
4. Run `bun run typecheck` and `bun run test`
5. Open a PR
Don't bump the version in your PR unless you're cutting a release — see below.
---
## Releasing
Maintainers only. Releases are automatic: a version change landing on `main` tags and publishes.
```bash
./scripts/release.sh 0.13.0
```
Never hand-edit a version in `package.json``scripts/sync-versions.ts` owns every place it appears, and CI fails a partial bump. Full process in [RELEASING.md](./RELEASING.md).
---
Join our [Discord](https://discord.gg/KZgfyyDVZa) if you have questions or want to discuss ideas.
+5 -39
View File
@@ -1,38 +1,16 @@
# Deployment
## Prerequisites
- `NPM_TOKEN` secret set in GitHub repo settings (granular access token for `@desplega-ai` scope)
- `@desplega-ai` org exists on npm
This file covers **what gets deployed and where**. For how a release is cut, tagged, and published — plus recovery when a publish fails — see **[RELEASING.md](./RELEASING.md)**.
## Release Process
Releases are **automatic**: a version change landing on `main` is the trigger. There is no manual tagging step.
Releases are automatic: a version change landing on `main` tags and publishes.
```bash
./scripts/release.sh 0.13.0
```
That syncs every version target, commits, and pushes. On a branch it stops there (open a PR); on `main` — or as soon as the PR merges — `.github/workflows/auto-release.yml` runs:
1. **Verifies version sync** (`sync-versions.ts --check`) and fails the release on any drift
2. **Tags** `v{version}`, unless that tag already exists
3. **Dispatches the publish workflows**, which run typecheck + build + tests, publish `@desplega.ai/agent-fs`, `@desplega.ai/agent-fs-just-bash`, and the FUSE sub-packages to npm with provenance, create the GitHub Release, and push the multi-arch GHCR image
The release is keyed on tag existence rather than on a diff between pushes, so it is idempotent — a rerun after a failure resumes instead of duplicating, and every publish step independently skips versions already on the registry.
> A tag pushed with `GITHUB_TOKEN` does **not** fire `on: push: tags` workflows (GitHub blocks recursive runs from the default token). That is why `auto-release.yml` invokes `npm-publish.yml` and `docker-publish.yml` through `workflow_dispatch` rather than relying on the tag push. Both still accept a plain tag push, so a manually pushed tag works as before.
### Version sync
`scripts/sync-versions.ts` is the single source of truth for the release version. It owns:
- root `package.json` + the 7 sub-package manifests
- `optionalDependencies` pins on the CLI for the FUSE sub-packages (`^{version}`)
- `packages/fuse-helper/Cargo.toml` and the `agent-fs-fuse` entry in `Cargo.lock`
- `.claude-plugin/plugin.json`
`bun run scripts/sync-versions.ts --check` verifies all of them match the root version and exits non-zero on drift. It runs in CI on every PR, so a partial bump fails review rather than shipping a mismatched version set.
Full process, version-sync rules, and recovery steps: **[RELEASING.md](./RELEASING.md)**.
## npm Package
@@ -60,21 +38,9 @@ agent-fs --help
|---------|-------------|
| `bun run build` | Bundle CLI for npm to `packages/cli/dist/cli.js` |
## Manual npm Publish (if needed)
## Manual npm Publish
```bash
bun run build
cd packages/just-bash && bun run build && bun publish --access public
cd packages/cli && bun publish --access public
```
Requires `NPM_CONFIG_TOKEN` env var set with a valid npm token.
## Notes
- `bun publish` auto-resolves `workspace:*` dependencies to real versions
- Auth uses `NPM_CONFIG_TOKEN` (not `NODE_AUTH_TOKEN` — Bun ignores that)
- No `--provenance` support in `bun publish` yet; use `npm publish` if needed
A last resort for when GitHub Actions itself is broken — see [Publishing from a laptop](./RELEASING.md#publishing-from-a-laptop) in RELEASING.md.
## Docker / GHCR
+6 -4
View File
@@ -146,11 +146,13 @@ See [CONTRIBUTING.md](./CONTRIBUTING.md) for the full development guide.
## Releasing
1. Update `version` in root `package.json`
2. Commit the version bump
3. Run `./scripts/release.sh`
Releases are automatic — a version change landing on `main` tags and publishes.
This creates a git tag and pushes it, triggering the release workflow which publishes to npm and creates a GitHub Release.
```bash
./scripts/release.sh 0.13.0
```
See [RELEASING.md](./RELEASING.md) for the full process, version-sync rules, and recovery steps.
## Deploy to Fly.io
+155
View File
@@ -0,0 +1,155 @@
# Releasing
**A version change landing on `main` is the release trigger.** There is no manual tagging step.
This file is the single source of truth for the release process. `CLAUDE.md`, `README.md`, and `DEPLOYMENT.md` link here rather than restating it — keep it that way, so the mechanics can never drift between copies.
---
## Cut a release
```bash
./scripts/release.sh 0.13.0
```
That syncs every version target, commits as `chore: release v0.13.0`, and pushes the current branch.
- **On a branch** it stops there. Open a PR; the release fires when it merges.
- **On `main`** (or the moment the PR merges) `.github/workflows/auto-release.yml` takes over.
Never bump the version by hand-editing `package.json` alone — see [Version targets](#version-targets).
### Before you bump
When the change touched core ops, CLI commands, or MCP tools:
1. **Skill** — new or changed op? Update `skills/agent-fs/SKILL.md` (command tables, description triggers, workflow examples).
2. **E2E** — new op? Add cases to `scripts/e2e.ts`.
3. **Version** — patch for fixes/features, minor for breaking changes.
`.claude-plugin/plugin.json` is bumped for you by `sync-versions.ts`; never edit it by hand.
---
## What happens on merge
`auto-release.yml` runs on every push to `main`:
1. **Verify version sync**`sync-versions.ts --check`. Any drift fails the release here, before anything is tagged.
2. **Tag** `v{version}`, unless that tag already exists.
3. **Dispatch the publish workflows.**
Then, in parallel:
| Workflow | Does |
|---|---|
| `npm-publish.yml` | Validates the tag matches `package.json`, runs `--frozen-lockfile` install + typecheck + build + tests, cross-builds the FUSE helper for musl x64/arm64, publishes the two FUSE sub-packages, builds the SHA-256 binary manifest, then publishes the main + `just-bash` packages and creates the GitHub Release |
| `docker-publish.yml` | Builds the multi-arch (`linux/amd64,arm64`) image and pushes to GHCR, tagged full version / major.minor / major / SHA / `latest` |
Ordering inside `npm-publish.yml` is load-bearing: the FUSE sub-packages must reach the registry **before** the main package, or `optionalDependencies` won't resolve for anyone installing it; and `dist/fuse-bin.manifest.json` must be in the published tarball so the CLI can hash-verify the binary it spawns at mount time.
### Published artifacts
| Package | Published? |
|---|---|
| `@desplega.ai/agent-fs` | npm, with provenance |
| `@desplega.ai/agent-fs-just-bash` | npm, with provenance |
| `@desplega.ai/agent-fs-fuse-linux-x64` | npm, with provenance |
| `@desplega.ai/agent-fs-fuse-linux-arm64` | npm, with provenance |
| `ghcr.io/desplega-ai/agent-fs` | GHCR, multi-arch |
| `@desplega.ai/agent-fs-core`, `-server`, `-mcp` | **No** — workspace-only, versioned but never published |
---
## Version targets
`scripts/sync-versions.ts` owns the release version everywhere it appears:
- root `package.json`
- `packages/{cli,core,server,mcp,just-bash,fuse-helper-linux-x64,fuse-helper-linux-arm64}/package.json`
- the FUSE `optionalDependencies` pins in `packages/cli/package.json` (as `^{version}`)
- `packages/fuse-helper/Cargo.toml` and the `agent-fs-fuse` entry in `Cargo.lock`
- `.claude-plugin/plugin.json`
```bash
bun run scripts/sync-versions.ts 0.13.0 # rewrite them all
bun run scripts/sync-versions.ts 0.13.0 --dry-run # show what would change
bun run scripts/sync-versions.ts --check # verify, exit 1 on drift
```
`--check` compares version *fields*, not whole-file bytes, so reformatting a `package.json` can't trip it. It runs in two places:
- **`ci.yml`, on every PR** — a partial bump (root moved, sub-packages left behind) turns the PR red.
- **`auto-release.yml`, before tagging** — the real backstop.
`live/` and `landing/` are deliberately excluded — they're deployed by Vercel and carry their own versions.
---
## Verify a release
```bash
gh run list --limit 5 # auto-release + both publishes
npm view @desplega.ai/agent-fs version
gh release view v0.13.0
```
Check both publish workflows, not just npm.
---
## Recovery
### A partial bump reached `main`
`auto-release` fails at the `--check` step: nothing is tagged, nothing publishes. Fix it forward:
```bash
./scripts/release.sh # re-syncs to whatever root package.json says
```
### Tag exists but a publish failed
This is the one case the automation won't retry on its own — the tag exists, so the next push sees nothing to do. Re-dispatch by hand:
```bash
gh workflow run npm-publish.yml -f tag=v0.13.0
gh workflow run docker-publish.yml -f tag=v0.13.0
```
Safe to re-run: every publish step checks the registry first and skips versions already there.
### Publishing from a laptop
Last resort, when Actions itself is the problem. Requires `NPM_CONFIG_TOKEN` (Bun ignores `NODE_AUTH_TOKEN`):
```bash
bun run build
cd packages/just-bash && bun run build && bun publish --access public
cd packages/cli && bun publish --access public
```
`bun publish` resolves `workspace:*` to real versions, but has no `--provenance` support — use `npm publish --provenance` if provenance matters. This path skips the FUSE sub-packages and the binary manifest.
---
## Design notes
Things that look wrong until you know why.
**Tags pushed by CI don't trigger tag workflows.** A tag pushed with `GITHUB_TOKEN` does not fire `on: push: tags` — GitHub blocks recursive workflow runs from the default token. That's why `auto-release.yml` invokes the publish workflows through `workflow_dispatch` instead of just pushing the tag and walking away. Both workflows still accept a plain tag push, so a manually pushed tag works exactly as before.
**The release is keyed on tag existence, not on a version diff.** Comparing against the previous push would miss a bump that already reached `main` and couldn't self-heal after a failed run. "Does `v{version}` exist yet?" is idempotent: reruns resume instead of duplicating, and an untagged bump gets picked up on the next push.
**`docker-publish.yml` pins `value=` on its semver tags.** `metadata-action`'s `type=semver` reads `github.ref`, which is `refs/heads/main` under `workflow_dispatch` — it would emit no version tags at all. `value=${{ env.RELEASE_TAG }}` points it at the tag instead.
**`release.sh` deliberately does not tag.** It used to. Now that a push to `main` triggers tagging, doing it locally too would race the workflow.
**CI is advisory, not blocking.** `main` has no branch protection and no required status checks, so a red PR can still be merged. The `--check` inside `auto-release` is what actually prevents a broken version set from shipping — it fails safe (no tag, no publish), but it will not stop the merge itself.
---
## Prerequisites
- `NPM_TOKEN` repo secret — granular npm access token scoped to `@desplega-ai`
- Default workflow permissions set to **read and write** (needed to push the tag)