Skip to content

Commit 4831d28

Browse files
djbclarkclaude
andcommitted
Bump to 0.19.1-sudo.5 and teach the release helper the sudo serial
The helper still only accepted `-djbclark.N`, which is why 0.19.1-sudo.4 was cut by hand and picked up the defects 5d30740 repaired. It now derives the serial from a DOWNSTREAM_SUFFIX constant, and its formula-restamp regex recognizes both spellings so a formula carried across the rename is not left naming a stale version. Preflight gains the guard that would have caught 0.19.1-sudo.4: `cargo metadata --locked` must succeed, so a lockfile that disagrees with the manifest fails before a tag exists rather than after it is published. Release helper tests 19 -> 21. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
1 parent 6a5f9bf commit 4831d28

5 files changed

Lines changed: 108 additions & 32 deletions

File tree

‎CHANGELOG.md‎

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -131,7 +131,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
131131
- `Cargo.lock` now matches the workspace version. The `0.19.1-sudo.4` bump
132132
updated `Cargo.toml` but not the lockfile, and because the formula builds
133133
with `cargo install --locked`, an install from that tag aborted before
134-
compiling anything.
134+
compiling anything. `v0.19.1-sudo.4` is superseded and should not be
135+
installed; use `v0.19.1-sudo.5` or later. The release helper now refuses to
136+
cut a version whose lockfile disagrees with the manifest, and it understands
137+
the `-sudo.N` serial that `0.19.1-sudo.4` renamed to.
135138

136139
- Corrected the documented tamper-evidence of the audit ledger, in
137140
`AI-GUIDANCE.md` and in the library's own notes. Both said truncating the

‎Cargo.lock‎

Lines changed: 7 additions & 7 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎Cargo.toml‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ members = [
1111
resolver = "2"
1212

1313
[workspace.package]
14-
version = "0.19.1-sudo.4"
14+
version = "0.19.1-sudo.5"
1515
edition = "2024"
1616

1717
[workspace.dependencies]

‎packaging/release.py‎

Lines changed: 30 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -46,9 +46,18 @@
4646
FORMULA = ROOT / FORMULA_REL
4747
DEFAULT_TAP = Path.home() / "src" / "homebrew-sudo-secretspec"
4848
FORMULA_NAME = "djbclark/sudo-secretspec/sudo-secretspec"
49-
VERSION_RE = re.compile(rf"^{re.escape(UPSTREAM_VERSION)}-djbclark\.([1-9][0-9]*)$")
49+
# The downstream serial's prefix. Releases through 0.19.1-djbclark.3 used
50+
# "djbclark"; everything from 0.19.1-sudo.4 on uses "sudo".
51+
DOWNSTREAM_SUFFIX = "sudo"
52+
VERSION_RE = re.compile(
53+
rf"^{re.escape(UPSTREAM_VERSION)}-{DOWNSTREAM_SUFFIX}\.([1-9][0-9]*)$"
54+
)
5055
# Any downstream version, anywhere in a file. Used to restamp the formula.
51-
ANY_VERSION_RE = re.compile(rf"{re.escape(UPSTREAM_VERSION)}-djbclark\.[0-9]+")
56+
# Both spellings, because a formula carried over from before the rename still
57+
# names the old one and has to be restamped rather than silently left behind.
58+
ANY_VERSION_RE = re.compile(
59+
rf"{re.escape(UPSTREAM_VERSION)}-(?:{DOWNSTREAM_SUFFIX}|djbclark)\.[0-9]+"
60+
)
5261
# Places the formula names its own version: the source `url`, the explicit
5362
# `version` stanza, and both `brew test` assertions. Pinned as a count so that
5463
# adding a fifth site fails this script loudly instead of shipping a formula
@@ -74,7 +83,7 @@ class Release:
7483

7584
@property
7685
def version(self) -> str:
77-
return f"{UPSTREAM_VERSION}-djbclark.{self.serial}"
86+
return f"{UPSTREAM_VERSION}-{DOWNSTREAM_SUFFIX}.{self.serial}"
7887

7988
@property
8089
def tag(self) -> str:
@@ -122,8 +131,8 @@ def parse_release(version: str) -> Release:
122131
match = VERSION_RE.fullmatch(version)
123132
if not match:
124133
raise ReleaseError(
125-
f"downstream version must be {UPSTREAM_VERSION}-djbclark.N with N >= 1, "
126-
f"got {version!r}"
134+
f"downstream version must be {UPSTREAM_VERSION}-{DOWNSTREAM_SUFFIX}.N "
135+
f"with N >= 1, got {version!r}"
127136
)
128137
return Release(serial=int(match.group(1)))
129138

@@ -188,6 +197,21 @@ def preflight(release: Release, *, allow_dirty: bool) -> None:
188197
in (ROOT / "Cargo.toml").read_text(encoding="utf-8"),
189198
f"workspace is not stamped {release.version}; bump Cargo.toml first",
190199
)
200+
# Cargo.lock records every workspace member's version, so bumping Cargo.toml
201+
# without regenerating the lock leaves the two disagreeing. The formula
202+
# builds with `cargo install --locked`, which refuses that tree outright, so
203+
# the tag installs nowhere. This is exactly how v0.19.1-sudo.4 shipped
204+
# broken; catch it here, before a tag exists to be un-published.
205+
_require(
206+
run(
207+
["cargo", "metadata", "--locked", "--format-version", "1"],
208+
check=False,
209+
).returncode
210+
== 0,
211+
f"Cargo.lock is out of date for {release.version}; run `cargo check` to "
212+
"regenerate it and commit the result, or the published tag will fail "
213+
"`cargo install --locked`",
214+
)
191215
# Catch a re-used serial here rather than after the test suite has run and
192216
# a local tag already exists. The remote is the authority: a serial can be
193217
# published from another checkout.
@@ -477,7 +501,7 @@ def parse_args(argv: list[str] | None = None) -> argparse.Namespace:
477501
parser.add_argument(
478502
"--version",
479503
required=True,
480-
help=f"downstream version to cut, e.g. {UPSTREAM_VERSION}-djbclark.2",
504+
help=f"downstream version to cut, e.g. {UPSTREAM_VERSION}-{DOWNSTREAM_SUFFIX}.5",
481505
)
482506
parser.add_argument("--dry-run", action="store_true")
483507
parser.add_argument("--allow-dirty", action="store_true")

‎tests/sudo_packaging/test_release.py‎

Lines changed: 66 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ def workspace_release(release):
4747
tests that are not about the version.
4848
"""
4949
text = (ROOT / "Cargo.toml").read_text(encoding="utf-8")
50-
match = re.search(r'version = "(\d+\.\d+\.\d+-djbclark\.\d+)"', text)
50+
match = re.search(r'version = "(\d+\.\d+\.\d+-sudo\.\d+)"', text)
5151
assert match, "workspace Cargo.toml is not stamped with a downstream version"
5252
return release.parse_release(match.group(1))
5353

@@ -75,27 +75,31 @@ def workspace_release(release):
7575

7676

7777
def test_release_identity_is_derived_from_the_serial(release, cut):
78-
cut = release.parse_release("0.19.1-djbclark.2")
79-
assert cut.serial == 2
80-
assert cut.version == "0.19.1-djbclark.2"
81-
assert cut.tag == "v0.19.1-djbclark.2"
82-
assert cut.title == "SecretSpec 0.19.1 — sudo-secretspec downstream 2"
78+
cut = release.parse_release("0.19.1-sudo.5")
79+
assert cut.serial == 5
80+
assert cut.version == "0.19.1-sudo.5"
81+
assert cut.tag == "v0.19.1-sudo.5"
82+
assert cut.title == "SecretSpec 0.19.1 — sudo-secretspec downstream 5"
8383
assert cut.archive_url == (
8484
"https://github.com/djbclark/sudo-secretspec/archive/refs/tags/"
85-
"v0.19.1-djbclark.2.tar.gz"
85+
"v0.19.1-sudo.5.tar.gz"
8686
)
8787

8888

8989
def test_only_downstream_versions_on_the_pinned_upstream_base_are_accepted(release):
9090
# The upstream base stays a constant: rebasing onto a new upstream tag is a
9191
# separate decision, not something a --version argument may do implicitly.
9292
for bad in (
93-
"0.20.0-djbclark.1",
93+
"0.20.0-sudo.1",
9494
"0.19.1",
95-
"v0.19.1-djbclark.1",
96-
"0.19.1-djbclark.0",
97-
"0.19.1-djbclark.01",
95+
"v0.19.1-sudo.1",
96+
"0.19.1-sudo.0",
97+
"0.19.1-sudo.01",
9898
"0.19.1-other.1",
99+
# The pre-rename spelling is no longer a version this script may cut.
100+
# It still has to be *recognized* when restamping a formula, which is
101+
# ANY_VERSION_RE's job, not parse_release's.
102+
"0.19.1-djbclark.4",
99103
"",
100104
):
101105
with pytest.raises(release.ReleaseError):
@@ -104,7 +108,7 @@ def test_only_downstream_versions_on_the_pinned_upstream_base_are_accepted(relea
104108

105109
def test_https_release_url_guard(release):
106110
release.validate_release_url(
107-
"https://github.com/djbclark/sudo-secretspec/archive/refs/tags/v0.19.1-djbclark.1.tar.gz"
111+
"https://github.com/djbclark/sudo-secretspec/archive/refs/tags/v0.19.1-sudo.1.tar.gz"
108112
)
109113
for url in ("http://github.com/x", "file:///tmp/x", "https://example.com/x"):
110114
with pytest.raises(release.ReleaseError):
@@ -127,13 +131,15 @@ def test_formula_rewrite_restamps_every_version_site(tmp_path: Path, release, cu
127131
" end\n",
128132
encoding="utf-8",
129133
)
130-
cut = release.parse_release("0.19.1-djbclark.2")
134+
cut = release.parse_release("0.19.1-sudo.5")
131135

132136
release.rewrite_formula(formula, cut, "a" * 64)
133137

134138
text = formula.read_text(encoding="utf-8")
139+
# The fixture is deliberately on the pre-rename spelling: a formula carried
140+
# across the djbclark -> sudo rename must be restamped, not left behind.
135141
assert "0.19.1-djbclark.1" not in text, text
136-
assert text.count("0.19.1-djbclark.2") == release.FORMULA_VERSION_SITES
142+
assert text.count("0.19.1-sudo.5") == release.FORMULA_VERSION_SITES
137143
assert f'url "{cut.archive_url}"' in text
138144
assert f'sha256 "{"a" * 64}"' in text
139145

@@ -150,7 +156,7 @@ def test_formula_rewrite_refuses_an_unexpected_number_of_version_sites(
150156
)
151157
with pytest.raises(release.ReleaseError, match="version references"):
152158
release.rewrite_formula(
153-
formula, release.parse_release("0.19.1-djbclark.2"), "a" * 64
159+
formula, release.parse_release("0.19.1-sudo.5"), "a" * 64
154160
)
155161

156162

@@ -226,7 +232,7 @@ def test_preflight_refuses_a_serial_that_is_already_published(monkeypatch, relea
226232

227233
def fake_run(argv, **kwargs):
228234
if argv[:2] == ["git", "ls-remote"]:
229-
return completed(argv, "9f4c…\trefs/tags/v0.19.1-djbclark.9\n")
235+
return completed(argv, "9f4c…\trefs/tags/v0.19.1-sudo.9\n")
230236
if argv[:4] == ["gh", "repo", "view", "djbclark/sudo-secretspec"]:
231237
return completed(
232238
argv,
@@ -263,7 +269,50 @@ def fake_run(argv, **kwargs):
263269
monkeypatch.setattr(release, "run", fake_run)
264270
# A serial nobody will ever cut, so it cannot match the real Cargo.toml.
265271
with pytest.raises(release.ReleaseError, match="bump Cargo.toml"):
266-
release.preflight(release.parse_release("0.19.1-djbclark.999"), allow_dirty=False)
272+
release.preflight(release.parse_release("0.19.1-sudo.999"), allow_dirty=False)
273+
274+
275+
def test_preflight_refuses_a_lockfile_that_disagrees_with_the_manifest(
276+
monkeypatch, release
277+
):
278+
"""The guard that v0.19.1-sudo.4 shipped without.
279+
280+
That release bumped Cargo.toml and left Cargo.lock naming the previous
281+
version. The formula builds with `cargo install --locked`, so the published
282+
tag aborted before compiling anything. `cargo metadata --locked` is the
283+
cheapest way to ask, and asking in preflight means the failure lands before
284+
a tag exists rather than after it is public.
285+
"""
286+
287+
def fake_run(argv, **kwargs):
288+
if argv[:2] == ["cargo", "metadata"]:
289+
assert "--locked" in argv
290+
assert kwargs.get("check") is False, "must not raise past the guard"
291+
return completed(argv, returncode=101)
292+
if argv[:4] == ["gh", "repo", "view", "djbclark/sudo-secretspec"]:
293+
return completed(
294+
argv,
295+
json.dumps(
296+
{
297+
"nameWithOwner": "djbclark/sudo-secretspec",
298+
"parent": {"nameWithOwner": "cachix/secretspec"},
299+
"defaultBranchRef": {"name": "sudo-main"},
300+
}
301+
),
302+
)
303+
return completed(argv, PREFLIGHT_GIT_OUTPUTS.get(tuple(argv), ""))
304+
305+
monkeypatch.setattr(release, "run", fake_run)
306+
with pytest.raises(release.ReleaseError, match="Cargo.lock is out of date"):
307+
release.preflight(workspace_release(release), allow_dirty=False)
308+
309+
310+
def test_any_version_re_matches_both_downstream_spellings(release):
311+
"""Restamping must recognize the pre-rename spelling as well as the current
312+
one, or a formula carried across the rename keeps its stale version."""
313+
assert release.ANY_VERSION_RE.findall(
314+
'url ".../v0.19.1-djbclark.3.tar.gz"\nversion "0.19.1-sudo.4"\n'
315+
) == ["0.19.1-djbclark.3", "0.19.1-sudo.4"]
267316

268317

269318
def test_parent_slug_accepts_every_gh_parent_shape(release):

0 commit comments

Comments
 (0)