# Releasing `agent-plugins`
Releases are **tag-driven**. Nothing publishes from a laptop: `cargo release`
produces a tag, and CI turns that tag into a crates.io release only after
re-running every check on a clean runner.
## One-time setup
### 1. Install the tooling
```bash
cargo install just # or: brew install just / apt install just
just setup # installs cargo-release and cargo-audit
```
### 2. Claim the crate name
`agent-plugins` was unclaimed on crates.io when this repo was set up. Claim it
with a manual first publish, from a clean checkout of `main`:
```bash
cargo login # paste a token from https://crates.io/settings/tokens
just ci # make sure everything is green first
cargo publish
```
After that first publish, every subsequent release goes through CI.
> Why publish the first one by hand? crates.io ties the crate's ownership to
> whoever publishes v0.1.0. Doing it manually means the crate belongs to your
> account, and the CI token that follows only ever needs *publish-update*
> scope for this one crate.
### 3. Give CI a way to publish
Create a scoped token at <https://crates.io/settings/tokens>:
- **Name:** `agent-plugin-rs CI`
- **Scopes:** `publish-update` only (not `publish-new`, not `yank`)
- **Crates:** `agent-plugins`
- **Expiry:** whatever your policy says; 90 days is a reasonable default
Then, in the GitHub repo:
1. **Settings → Environments → New environment**, named `crates-io`.
2. Optionally add yourself as a **required reviewer** — with that on, every
release pauses for a human click before the publish step runs.
3. **Add secret** `CARGO_REGISTRY_TOKEN` with the token value.
The environment name matters: `.github/workflows/release.yml` pins the publish
job to `environment: crates-io`, so the token is not readable from any other
job, including anything a pull request can trigger.
### Optional: Trusted Publishing instead of a token
crates.io supports OIDC Trusted Publishing, which removes the long-lived
secret entirely. To switch:
1. On crates.io, open the crate's **Settings → Trusted Publishing** and add a
GitHub publisher for `Toasterson/agent-plugin-rs`, workflow
`release.yml`, environment `crates-io`.
2. In `release.yml`, replace the `Publish` step's `env:` block with a token
exchange before `cargo publish`:
```yaml
- uses: rust-lang/crates-io-auth-action@v1
id: auth
- run: cargo publish
env:
CARGO_REGISTRY_TOKEN: ${{ steps.auth.outputs.token }}
```
3. Delete the `CARGO_REGISTRY_TOKEN` secret.
The `id-token: write` permission the publish job already declares is what
makes this work.
## Cutting a release
From an up-to-date `main` with a clean tree:
```bash
just ci # optional; CI runs it again anyway
just release-dry patch # show exactly what would change
just release patch # do it
```
`cargo release` will, in one step:
1. bump the version in `Cargo.toml` and `Cargo.lock`,
2. promote `## [Unreleased]` in `CHANGELOG.md` to the new version and open a
fresh Unreleased section,
3. commit as `release: v<version>`,
4. tag `v<version>`,
5. push the commit and the tag.
Pushing the tag starts `.github/workflows/release.yml`, which:
1. re-runs fmt, clippy, tests, and doc tests,
2. checks the tag matches the version in `Cargo.toml` — a mismatch fails the
release rather than publishing something surprising,
3. publishes to crates.io,
4. creates a GitHub release with generated notes.
`LEVEL` can be `patch`, `minor`, `major`, or an explicit version:
```bash
just release minor
just release 1.0.0
```
## If a release goes wrong
**CI failed before publishing.** Nothing reached crates.io. Fix the problem on
`main`, delete the tag locally and remotely, and release again:
```bash
git tag -d v0.1.1
git push origin :refs/tags/v0.1.1
```
**CI published, but the release is broken.** crates.io versions are permanent
— you cannot replace one. Yank it so new dependents cannot pick it up, then
release a fixed patch version:
```bash
cargo yank --version 0.1.1
just release patch
```
Yanking does not break anyone who already has it in a `Cargo.lock`, which is
the point.
## Keeping the two implementations in step
The conformance corpus in `fixtures/cases/` is mirrored in
[agent-plugin-ts](https://github.com/Toasterson/agent-plugin-ts). A fixture
change belongs in both repos, or the implementations quietly stop agreeing:
```bash
# from a checkout with both repos side by side
just sync-fixtures # pulls from ../agent-plugin-ts
just test
```
The two packages version independently — a Rust-only fix does not need a
TypeScript release.