From 7ef4f8dc8594d31e23c2f2e7aa781a27c3de0503 Mon Sep 17 00:00:00 2001 From: Seto Date: Tue, 13 Oct 2026 13:45:31 +0200 Subject: [PATCH 1/2] ci: publish crate to crates.io on v* tags Add release-crates.yml, which runs `cargo publish --locked` when a v* tag is pushed, using the CARGO_REGISTRY_TOKEN secret. Also add the release skill documenting the version-bump checklist, the fmt/clippy/test gate, fast-forwarding main, tagging, and verifying the published GitHub release and crate. Co-Authored-By: SplitFire v1.1.1-acp --- .agents/skills/release/SKILL.md | 130 +++++++++++++++++++++++++++ .github/workflows/release-crates.yml | 18 ++++ 2 files changed, 148 insertions(+) create mode 100644 .agents/skills/release/SKILL.md create mode 100644 .github/workflows/release-crates.yml diff --git a/.agents/skills/release/SKILL.md b/.agents/skills/release/SKILL.md new file mode 100644 index 0000000..56fc671 --- /dev/null +++ b/.agents/skills/release/SKILL.md @@ -0,0 +1,130 @@ +--- +name: release +description: Cut a tagged release of the splitfire-agent crate. Covers the version-bump checklist, the gate (fmt/clippy/test), fast-forwarding main, the annotated tag that fires the cross-platform release and crates.io workflows, and verifying the published GitHub release and crate. Use whenever the user asks to release, cut, tag, or publish a new splitfire-agent version. +allowed-tools: Read, Write, Edit, Glob, Grep, Bash +user-invocable: true +--- + +# Release + +How to cut a `splitfire-agent` release. Releases are driven by git tags. Pushing a +`v*` tag fires two workflows: + +- `.github/workflows/release.yml` — builds the binary on five targets + (darwin-arm64/x64, linux-x64/arm64, windows-x64) and publishes a GitHub release + with auto-generated notes and the archives attached. +- `.github/workflows/release-crates.yml` — runs `cargo publish --locked` to push + the crate to crates.io (needs the `CARGO_REGISTRY_TOKEN` repo secret). + +Branch model: work lands on `development` (via PRs); `main` is the release branch; +every tag points at a commit that is on `main`. Previous tags were plain annotated +(not GPG-signed) tags authored by the maintainer, message `Release vX.Y.Z`. + +--- + +## 1. Confirm the version is bumped + +The version bump often already landed with the feature PR — check before assuming +you need a bump commit. These three must agree and must be the version you're +releasing: + +- `Cargo.toml` (`version = "X.Y.Z"`) +- `Cargo.lock` (the `splitfire-agent` package entry) +- `registry/agent.json` (`"version": "X.Y.Z"`) + +```sh +grep -m1 '^version' Cargo.toml +grep -A2 '^name = "splitfire-agent"' Cargo.lock | grep version +grep '"version"' registry/agent.json +``` + +If a bump is needed, edit all three, run `cargo build` to refresh `Cargo.lock`, and +commit on `development` (or the release branch) before continuing. `registry/` is the +ACP registry submission; keeping its `version` in sync with `Cargo.toml` is part of +the release, per AGENTS.md. + +## 2. Run the gate + +CI runs exactly these three; run them locally and get them green before tagging. Use +`cargo` directly — this is a Rust crate, the global `rv` rule is Ruby-only. + +```sh +cargo fmt -- --check +cargo clippy --locked --all-targets -- -D warnings +cargo test --locked +``` + +## 3. Get the release commit onto main + +`main` must fast-forward to the commit you're releasing (no merge commit, matching +history). Verify there is nothing on `main` that isn't on your source branch: + +```sh +git log --oneline main..origin/main # expect empty (main not ahead) +git log --oneline origin/main..development # the commits you're about to release +``` + +Fast-forward `main` to the release commit (works without checking it out), then push: + +```sh +git branch -f main development +git push origin main +``` + +## 4. Tag and push + +Confirm your git identity and gh account are the maintainer's before tagging — the +`git-commit-signing` skill governs identity selection. Match the previous tag's +style: plain annotated, message `Release vX.Y.Z`. Pushing the tag is what fires the +release workflow, so push it last. + +```sh +git tag -a vX.Y.Z -m "Release vX.Y.Z" +git push origin vX.Y.Z +``` + +## 5. Verify the published release + +The workflow takes ~8-9 minutes (five-target matrix). Watch it, then confirm all +five assets are attached: + +```sh +gh run list --workflow=release.yml --limit 1 +gh run view --json status,conclusion,jobs \ + --jq '{status,conclusion,jobs:[.jobs[]|{name,conclusion}]}' +gh release view vX.Y.Z +``` + +A green release has five assets: +`splitfire-agent-{darwin-arm64,darwin-x64,linux-arm64,linux-x64}.tar.gz` and +`splitfire-agent-windows-x64.zip`. + +Also confirm the crate published: + +```sh +gh run list --workflow=release-crates.yml --limit 1 +curl -s https://crates.io/api/v1/crates/splitfire-agent | python3 -Ic \ + 'import sys,json;print(json.load(sys.stdin)["crate"]["max_version"])' +``` + +--- + +## Notes + +- A release is outward-facing and hard to reverse (public binaries, cached/indexed). + The user asking to "release vX.Y.Z" authorizes it; still report faithfully if a + gate step fails rather than tagging anyway. +- The GitHub release is created by `github-actions[bot]`, not the tagger. That's + expected — the workflow uses `GITHUB_TOKEN`. +- Release notes are auto-generated from merged PRs since the previous tag + (`--generate-notes`). There is no hand-written CHANGELOG file in this repo. +- crates.io is immutable and append-only: a version can be published exactly once, + then only *yanked* (hidden from new resolves, existing lockfiles keep working) — + never replaced or deleted. So the tag's `Cargo.toml` version must not already be on + crates.io; any re-release needs a fresh version bump. The `ed-*` dependencies are + already on crates.io, so no dependency needs publishing first. To publish manually + instead of via CI: `cargo login` once (token from crates.io/settings/tokens), then + `cargo publish --locked` from the tagged commit. +- If a platform build fails after the tag is pushed, fix the cause and re-run the + failed jobs (`gh run rerun --failed`); the release-create step tolerates + an already-existing release and `--clobber`s assets, so re-runs are safe. diff --git a/.github/workflows/release-crates.yml b/.github/workflows/release-crates.yml new file mode 100644 index 0000000..4825a07 --- /dev/null +++ b/.github/workflows/release-crates.yml @@ -0,0 +1,18 @@ +name: release-crates + +on: + push: + tags: ["v*"] + +jobs: + publish: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v5 + - uses: dtolnay/rust-toolchain@stable + # Publishes the version in Cargo.toml to crates.io. crates.io is immutable: + # a version can only be published once (then yanked, never replaced), so the + # tag's Cargo.toml version must not already be on crates.io. + - run: cargo publish --locked + env: + CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }} From cf210b43759c4e282844d75457d019894cd255bb Mon Sep 17 00:00:00 2001 From: Seto Date: Tue, 13 Oct 2026 13:52:27 +0200 Subject: [PATCH 2/2] ci: publish to crates.io via trusted publishing Swap the CARGO_REGISTRY_TOKEN secret for crates.io trusted publishing: the job requests an OIDC token (permissions: id-token: write), rust-lang/crates-io-auth-action exchanges it for a short-lived crates.io token, and cargo publish --locked consumes it. The action revokes the token in its post step, so there is no long-lived credential in repo secrets. The action is pinned to v1.0.5 rather than the v1 the crates.io docs show, because that repo publishes no moving v1 tag and @v1 fails to resolve. Document the one-time crates.io-side setup in the release skill: a trusted publisher must name SplitFireAI / splitfire-agent / release-crates.yml, and the workflow filename is part of the trust claim. Co-Authored-By: SplitFire v1.1.1-acp --- .agents/skills/release/SKILL.md | 24 +++++++++++++++++++++++- .github/workflows/release-crates.yml | 14 +++++++++++++- 2 files changed, 36 insertions(+), 2 deletions(-) diff --git a/.agents/skills/release/SKILL.md b/.agents/skills/release/SKILL.md index 56fc671..b198c71 100644 --- a/.agents/skills/release/SKILL.md +++ b/.agents/skills/release/SKILL.md @@ -14,7 +14,9 @@ How to cut a `splitfire-agent` release. Releases are driven by git tags. Pushing (darwin-arm64/x64, linux-x64/arm64, windows-x64) and publishes a GitHub release with auto-generated notes and the archives attached. - `.github/workflows/release-crates.yml` — runs `cargo publish --locked` to push - the crate to crates.io (needs the `CARGO_REGISTRY_TOKEN` repo secret). + the crate to crates.io, authenticated by **trusted publishing**: a GitHub OIDC + token is exchanged for a short-lived crates.io token. There is no + `CARGO_REGISTRY_TOKEN` secret to store or rotate. Branch model: work lands on `development` (via PRs); `main` is the release branch; every tag points at a commit that is on `main`. Previous tags were plain annotated @@ -22,6 +24,23 @@ every tag points at a commit that is on `main`. Previous tags were plain annotat --- +## 0. One-time setup: the trusted publisher + +Trusted publishing needs a publisher registered on crates.io **before** the first +tagged run. The crate must already be on crates.io (a first-ever publish still +needs a manual `cargo login` + `cargo publish`), and you must be a crate owner. +Then: crate page → **Settings → Trusted Publishing → Add → GitHub**: + +- Repository owner: `SplitFireAI` +- Repository name: `splitfire-agent` +- Workflow filename: `release-crates.yml` (the bare filename, not a path) +- Environment: leave blank unless the job also declares `environment:` + +The workflow filename is part of the trust claim, so renaming the workflow breaks +publishing until the crates.io config is updated to match — keep the two in sync. + +--- + ## 1. Confirm the version is bumped The version bump often already landed with the feature PR — check before assuming @@ -125,6 +144,9 @@ curl -s https://crates.io/api/v1/crates/splitfire-agent | python3 -Ic \ already on crates.io, so no dependency needs publishing first. To publish manually instead of via CI: `cargo login` once (token from crates.io/settings/tokens), then `cargo publish --locked` from the tagged commit. +- `crates-io-auth-action` is pinned to `v1.0.5` deliberately: the action repo has + no moving `v1` tag (only `v1.0.x`), so the `@v1` shown in the crates.io docs + example fails to resolve. Bump the pin when a newer release lands. - If a platform build fails after the tag is pushed, fix the cause and re-run the failed jobs (`gh run rerun --failed`); the release-create step tolerates an already-existing release and `--clobber`s assets, so re-runs are safe. diff --git a/.github/workflows/release-crates.yml b/.github/workflows/release-crates.yml index 4825a07..58d910d 100644 --- a/.github/workflows/release-crates.yml +++ b/.github/workflows/release-crates.yml @@ -7,12 +7,24 @@ on: jobs: publish: runs-on: ubuntu-latest + permissions: + id-token: write # required for the OIDC token exchange with crates.io steps: - uses: actions/checkout@v5 - uses: dtolnay/rust-toolchain@stable + # Trusted publishing: no long-lived token to store or rotate. This action + # exchanges a GitHub OIDC token for a short-lived crates.io token (valid 30 + # minutes) and revokes it in its post step. + # + # It only works if a trusted publisher is configured for this crate on + # crates.io naming SplitFireAI / splitfire-agent / this exact filename + # (release-crates.yml). The workflow filename is part of the trust check, + # so renaming this file breaks publishing until crates.io is updated too. + - uses: rust-lang/crates-io-auth-action@v1.0.5 + id: auth # Publishes the version in Cargo.toml to crates.io. crates.io is immutable: # a version can only be published once (then yanked, never replaced), so the # tag's Cargo.toml version must not already be on crates.io. - run: cargo publish --locked env: - CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }} + CARGO_REGISTRY_TOKEN: ${{ steps.auth.outputs.token }}