| name | release |
| description | Release a new version of cow. Verifies tests, checks changelog, bumps version, commits, builds a universal macOS binary, creates a GitHub release (with notes matching docs/changelog.html), updates the Homebrew tap, and publishes to crates.io. |
cow Release Skill
Full release lifecycle for the cow project at personal/cow/main/.
Steps
Work through these in order. Stop and tell the user if any step fails.
1. Confirm version
Ask the user what the new version number is if they have not already provided it (e.g. 0.1.2). Call it NEW_VERSION throughout.
2. Run tests
cargo test
All tests must pass. If any fail, stop and report — do not proceed.
3. Verify changelog is ready
Read docs/changelog.html. Check that:
- There is a release entry with
release-version matching v{NEW_VERSION}
- The entry has a date set (not a placeholder)
- It has at least one
change-group with actual content
If the changelog entry is missing or incomplete, tell the user what needs to be added and stop. The docs changelog must be written before shipping — it becomes the source of truth for the GitHub release notes too.
4. Bump version in Cargo.toml
Edit Cargo.toml, changing the version field to NEW_VERSION.
Run cargo build briefly to confirm Cargo.lock is regenerated correctly:
cargo build --release --target aarch64-apple-darwin 2>&1 | tail -3
(This is the first arch build — keep the output for step 6.)
5. Update real-world stats on homepage
Run cow stats to get current numbers:
cow stats
From the Total row in the output, extract:
- Pasture count (Pastures column)
- Disk delta (Delta column)
- npm savings (Saved (npm) column — number only, strip leading
~)
- pnpm savings (Saved (pnpm) column — number only, strip leading
~)
Edit docs/index.html — find the proof-strip div in the hero section and update:
- The
proof-num span inside the "active pastures" stat
- The
proof-num span inside the "total disk delta" stat
- The
proof-pm-num span for "npm / yarn"
- The
proof-pm-num span for "pnpm / bun"
Keep the ~ prefix on the savings figures. The caption line ("the developer's own experience…") does not need to change.
6. Commit
Stage only the release files — never commit .tickets/ or other unrelated untracked files:
git add Cargo.toml Cargo.lock docs/changelog.html docs/docs.html docs/index.html src/ tests/
git status --short
Write a concise commit message: Bump to v{NEW_VERSION}: <one-line summary> followed by a bullet-point body drawn from the changelog entry.
Commit and push:
git commit -m "..." && git push origin main
7. Build universal binary
Build both architectures (the arm64 build may already be done from step 4):
cargo build --release --target aarch64-apple-darwin
cargo build --release --target x86_64-apple-darwin
Combine with lipo:
lipo -create -output /tmp/cow-universal \
target/aarch64-apple-darwin/release/cow \
target/x86_64-apple-darwin/release/cow
lipo -info /tmp/cow-universal
Copy the man page (generated by build.rs into target/man/):
cp target/man/cow.1 /tmp/cow.1
Create the tarball and record the SHA256:
cd /tmp && tar -czf cow-{NEW_VERSION}.tar.gz cow-universal cow.1
shasum -a 256 cow-{NEW_VERSION}.tar.gz
8. Extract release notes from docs/changelog.html
Read the v{NEW_VERSION} entry from docs/changelog.html and convert it to markdown for the GitHub release body. The structure to follow:
- Use the
<h2> inside the entry as the first line
- Each
change-group-label becomes a ## heading
- Each
<li> becomes a bullet, with the .tag span rendered as a bold label e.g. **perf**
- Strip all HTML tags from the text
9. Create GitHub release
gh release create v{NEW_VERSION} /tmp/cow-{NEW_VERSION}.tar.gz \
--repo joeinnes/cow \
--title "v{NEW_VERSION}" \
--notes "<markdown release notes from step 7>"
10. Update Homebrew tap
Get the current formula file SHA:
gh api repos/joeinnes/homebrew-tap/contents/Formula/cow.rb --jq '.sha'
Build the new formula. The install block must use cow-universal (the binary name inside the tarball) and rename it to cow:
class Cow < Formula
desc "Copy-on-write workspace manager for parallel development"
homepage "https://github.com/joeinnes/cow"
url "https://github.com/joeinnes/cow/releases/download/v{NEW_VERSION}/cow-{NEW_VERSION}.tar.gz"
sha256 "<sha256 from step 6>"
version "{NEW_VERSION}"
license "MIT"
def install
bin.install "cow-universal" => "cow"
man1.install "cow.1"
end
test do
assert_match "cow", shell_output("#{bin}/cow --version")
end
end
Push via the GitHub API:
gh api --method PUT repos/joeinnes/homebrew-tap/contents/Formula/cow.rb \
--field message="cow {NEW_VERSION}" \
--field content="<base64-encoded formula>" \
--field sha="<sha from above>"
10. Publish to crates.io
cargo publish --allow-dirty
The --allow-dirty flag is needed because .tickets/ files are untracked. They are not included in the published crate (cargo only packages files tracked by git).
11. Done
Confirm all three destinations are updated:
- GitHub release:
https://github.com/joeinnes/cow/releases/tag/v{NEW_VERSION}
- Homebrew:
joeinnes/homebrew-tap Formula/cow.rb
- crates.io:
cow-cli v{NEW_VERSION}
Run say to announce completion.