From b9cb9b963c5e07dc07718eabc76ee1451b817c60 Mon Sep 17 00:00:00 2001 From: "@permadeath.com" Date: Wed, 12 Aug 2026 10:02:54 -0400 Subject: [PATCH] feat(release): write the current release into the README The README now carries an italic line under the lockup naming the release it describes and linking that tag on Tangled. A `pre-release-replacements` rule rewrites it during a bump, so it lands in the `chore(release):` commit next to Cargo.toml rather than trailing the release by however long it takes someone to notice it has gone stale. The line is matched by its opening prose rather than by hidden HTML comment markers around it. Markers would let the wording drift freely, but they only stay invisible if the renderer drops comments, and this file is read through Tangled's sanitizer as well as anything else; the price is that `*Current release: ` is load-bearing in two files at once. `exactly = 1` turns a lost line into a failed bump instead of a silently skipped rewrite, and tests/release_metadata.rs holds the line and Cargo.toml to the same version on every commit rather than only at release time, since a README pinned at an old version reads exactly like a correct one. Co-Authored-By: Claude Opus 5 (1M context) --- CONTRIBUTING.md | 4 +- README.md | 2 + TODO.md | 13 ++++++ release.toml | 34 ++++++++++++++ tests/release_metadata.rs | 95 +++++++++++++++++++++++++++++++++++++++ 5 files changed, 147 insertions(+), 1 deletion(-) create mode 100644 tests/release_metadata.rs diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 8026503..cc2af1b 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -74,7 +74,9 @@ One command, run from a clean `main`: cargo release "$(git cliff --unreleased --bumped-version | sed 's/^v//')" --execute ``` -`git cliff` reads the conventional commits since the last tag and prints the version they imply; `cargo release` writes it into `Cargo.toml` and `Cargo.lock`, makes a `chore(release):` commit, and creates an annotated tag. It asks for confirmation first and shows the version it computed — read that line before answering. Nothing is pushed and nothing is published; those are separate, deliberate acts. The rules live in `cliff.toml` and `release.toml`, both of which explain themselves. +`git cliff` reads the conventional commits since the last tag and prints the version they imply; `cargo release` writes it into `Cargo.toml` and `Cargo.lock`, rewrites the "current release" line under the README's lockup, makes a `chore(release):` commit, and creates an annotated tag. It asks for confirmation first and shows the version it computed — read that line before answering. Nothing is pushed and nothing is published; those are separate, deliberate acts. The rules live in `cliff.toml` and `release.toml`, both of which explain themselves. + +The README line is generated, so do not hand-edit it: it lives between `` markers and is replaced wholesale on each bump. Losing a marker fails the bump rather than skipping the line, and `tests/release_metadata.rs` fails the commit if the line and `Cargo.toml` ever disagree. While the project is pre-1.0: `fix` and `docs` bump the patch, `feat` bumps the minor, and a breaking `!` **also** bumps the minor rather than jumping to 1.0.0. That last one overrides a git-cliff default that would have gone straight to 1.0.0, and it is set explicitly in `cliff.toml` — 1.0.0 is a compatibility promise this project has not made, and it should be a version somebody types on purpose. diff --git a/README.md b/README.md index 978ed70..8c20221 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,7 @@ ![atgc](brand/png/lockup-512.png) +*Current release: [v0.15.0](https://tangled.org/permadeath.com/atgc/tags/v0.15.0)* + **AT**proto **G**it **C**lient. ## Features diff --git a/TODO.md b/TODO.md index 9c0d9c7..159a012 100644 --- a/TODO.md +++ b/TODO.md @@ -760,6 +760,10 @@ warn where the stack command is better. ## release (artifacts on tags) - [ ] `release create` / `list` / `view` — sh.tangled.repo.artifact - [ ] `release upload` / `download` / `delete` +- [ ] Point the README's current-release line at the binary once there is + one. The line already exists and is rewritten on every bump; it says + only the tag today because a tag is all there is to say. Widening it + is a one-line edit to `pre-release-replacements` in release.toml ## key - [x] `key add` / `list` / `delete` — sh.tangled.publicKey records, which are what a knot authorizes a push against. `add` defaults to the one @@ -955,6 +959,15 @@ warn where the stack command is better. the minor while pre-1.0, overriding git-cliff's jump to 1.0.0. No longer unexercised: v0.2.0, v0.2.1 and v0.3.0 were all cut this way, through scripts/release.sh +- [x] The bump also writes which release is current into the README, on an + italic line under the lockup linking the tag on Tangled. A + `pre-release-replacements` rule in release.toml rewrites the block + between two HTML comment markers, so it lands in the `chore(release):` + commit rather than trailing it; `tests/release_metadata.rs` fails if + that line and Cargo.toml ever disagree. The markers, rather than a + match on the prose, because the line is meant to grow — see the + release-artifacts entry above, which is what it should point at once + there is a built binary to point at - [x] Push the `v0.1.0` epoch tag. Annotated, on 6d16818 — the last commit before Conventional Commits start — so the first bump counts every conforming commit. It is on origin, and the bumps since have counted diff --git a/release.toml b/release.toml index 915c76e..5851a49 100644 --- a/release.toml +++ b/release.toml @@ -43,3 +43,37 @@ tag-message = "atgc {{tag_name}}" # a single-crate repo, so off is also simply the truthful setting. pre-release-commit-message = "chore(release): atgc v{{version}}" consolidate-commits = false + +# The README says which release is current, and this is what writes it. The +# line is rewritten before the version commit is made, so it lands in that +# commit alongside Cargo.toml and Cargo.lock rather than trailing a release +# by however long it takes someone to notice. +# +# The line is found by its opening prose and not by hidden `` markers +# around it. Markers would let the wording drift freely, but they only stay +# invisible if the renderer drops comments, and this README is read through +# Tangled's sanitizer as well as any other — a renderer that escaped them +# instead would print the scaffolding at the top of the front page. The +# tradeoff bought by not finding out: "*Current release: " is now load-bearing +# text. Reword it here and in tests/release_metadata.rs together. +# +# `(?m)` so `^` and `$` bind to the line rather than the file. The match stops +# at the newline, which is why the replacement is a single line — it is one +# line by design, and widening it later (a built binary alongside the tag) is +# an edit to this string, not to the shape of the rule. +# +# `exactly = 1` is the check that matters. Without it a reword that got out of +# step would leave the README pinned at whatever version it last said, and a +# release that quietly does not update the thing it exists to update is worse +# than one that fails; with it, cargo-release stops with "N replacements +# expected, found M" before anything is committed. +# +# The repository URL is spelled out rather than taken from {{repository}}. +# Only {{version}} is documented for this step, and an unrecognized +# placeholder is not an error — it is copied through as literal text, which +# would put a dead link in the README and tag it as current. The test in +# tests/release_metadata.rs is the backstop for that whole class of failure: +# it reads this line back and fails if it disagrees with Cargo.toml. +pre-release-replacements = [ + { file = "README.md", exactly = 1, search = "(?m)^\\*Current release: .*$", replace = "*Current release: [v{{version}}](https://tangled.org/permadeath.com/atgc/tags/v{{version}})*" }, +] diff --git a/tests/release_metadata.rs b/tests/release_metadata.rs new file mode 100644 index 0000000..5a833eb --- /dev/null +++ b/tests/release_metadata.rs @@ -0,0 +1,95 @@ +//! The README's "current release" line agrees with the version it claims. +//! +//! Nobody types that line. `cargo release` rewrites it as part of a bump, +//! from the `pre-release-replacements` rule in release.toml, and commits it +//! alongside Cargo.toml. So the two move together or the release is wrong, +//! and this is where "or the release is wrong" gets noticed. +//! +//! It runs on every commit rather than only at release time, because the +//! failure it guards against is silent: a README pinned at an old version +//! reads exactly like a correct one. The check is cheap and the release step +//! is rare, so paying for it continuously is the right trade. + +/// The prefix release.toml's regex anchors on. Spelled here as a plain +/// literal so that a reword which updates only one of the two files fails +/// this test rather than passing quietly and skipping the line at the next +/// bump. Keep the two in step. +const PREFIX: &str = "*Current release: "; + +/// The line as published. +/// +/// Panics rather than returning an error: a README with no release line is +/// the same failure as a README with a stale one, and the test wants both to +/// read as a failure of that file, not of the parsing. +fn release_line() -> &'static str { + let readme = include_str!("../README.md"); + let mut hits = readme.lines().filter(|l| l.starts_with(PREFIX)); + let line = hits + .next() + .expect("README has no line starting with the release prefix release.toml rewrites"); + assert!( + hits.next().is_none(), + "README has more than one release line; release.toml's `exactly = 1` would refuse the bump" + ); + line +} + +/// The version in the README is the version in Cargo.toml. This is the whole +/// point of the file: a bump that updated one and not the other is a release +/// that lies about which release it is. +#[test] +fn readme_names_the_crate_version() { + let line = release_line(); + let tag = format!("v{}", env!("CARGO_PKG_VERSION")); + assert!( + line.contains(&tag), + "README's release line says {line:?}, which does not mention {tag}" + ); +} + +/// A placeholder cargo-release does not recognize is copied through as +/// literal text instead of failing, so `{{repository}}` in a replacement +/// would publish a link to a host called `{{repository}}`. Nothing else on +/// the line should contain braces, which makes this a cheap total check. +#[test] +fn readme_release_line_has_no_unrendered_placeholders() { + let line = release_line(); + assert!( + !line.contains("{{"), + "README's release line has an unrendered template placeholder: {line:?}" + ); +} + +/// Italics, and only italics. The line is a caption on the lockup above it, +/// not the first sentence of the README; the opening `*` comes with the +/// prefix, so what is worth asserting is that the replacement still closes +/// the emphasis and has not been promoted to bold. +#[test] +fn readme_release_line_is_italic() { + let line = release_line(); + assert!( + line.ends_with('*') && !line.starts_with("**"), + "README's release line is not italic: {line:?}" + ); +} + +/// Underneath the lockup, which is the whole reason it reads as a caption. +/// The image is the first line of the README and the release line is the +/// next non-blank one; anything in between has pushed the version further +/// from the mark it belongs to. +#[test] +fn readme_release_line_sits_under_the_lockup() { + let readme = include_str!("../README.md"); + let mut lines = readme.lines().filter(|l| !l.trim().is_empty()); + let first = lines.next().unwrap_or_default(); + assert!( + first.starts_with("![atgc](brand/"), + "README no longer opens with the lockup, it opens with {first:?}" + ); + let second = lines.next().unwrap_or_default(); + assert_eq!( + second, + release_line(), + "the release line is no longer the first thing under the lockup" + ); +} -- 2.51.2