diff --git a/ansible/roles/decdn_node/README.md b/ansible/roles/decdn_node/README.md index 6cefa21..2c92a85 100644 --- a/ansible/roles/decdn_node/README.md +++ b/ansible/roles/decdn_node/README.md @@ -700,7 +700,7 @@ The repo pins one decdn/decdn release everywhere, and a new one moves every pin one change. `scripts/open-decdn-bump.sh [vX.Y.Z]` opens that PR: decdn's `publish-crates.sh` runs it at the end of each decdn release, and a maintainer runs it by hand from a decdn/devops clone (with no tag, it takes decdn's latest release). -It does steps 1 to 6 and the changelog entries in a worktree at `origin/main`, takes +It does steps 1 to 6 in a worktree at `origin/main`, takes the digests from the release's assets once their signatures verify against `files/decdn-release-KEYS.asc`, and lists in the PR body what is left: the prose of step 7, the chart check against the real binary in step 5, any schema-key or KEYS @@ -731,12 +731,7 @@ The steps: copy, and `SECURITY.md`'s fingerprints). 7. The prose that names the version: `git grep -n ''` outside the changelogs. The changelog entry is the PR title (`feat`, `fix` or `chore`, not `ci` or - `build`, which the changelogs leave out; RELEASING.md); also a `[Unreleased]` - entry in each of `ansible/galaxy/node/CHANGELOG.md`, - `ansible/galaxy/publisher/CHANGELOG.md` (the onramp's decdn pin) and - `charts/decdn-node/CHANGELOG.md` whose artifact is not released yet. The - collections each keep one such entry, which a later bump rewrites - (`scripts/bump_decdn.py`). + `build`, which the changelogs leave out; RELEASING.md). `make test-scripts` fails while the role, the onramp's decdn pin (role and Compose example), the chart's `appVersion` and its `artifacthub.io/images` image, or the KEYS diff --git a/ansible/roles/sponsord/README.md b/ansible/roles/sponsord/README.md index 4c8f329..e77d45d 100644 --- a/ansible/roles/sponsord/README.md +++ b/ansible/roles/sponsord/README.md @@ -249,9 +249,7 @@ move every pin in one change: `crates/*/src/config.rs` (AGENTS.md, `compose/`). 5. The prose that names the version: `git grep -n ''` outside the changelogs. The changelog entry is the PR title (`feat`, `fix` or `chore`, not `ci` or - `build`, which the changelogs leave out; RELEASING.md); before the `decdn.publisher` - collection's first release, also a `[Unreleased]` entry in - `ansible/galaxy/publisher/CHANGELOG.md`. + `build`, which the changelogs leave out; RELEASING.md). `make test-scripts` fails while the two versions disagree; the weekly `upstream-drift` job checks the digests against the release. A diff --git a/scripts/bump_decdn.py b/scripts/bump_decdn.py index 4d35d9c..66f03aa 100755 --- a/scripts/bump_decdn.py +++ b/scripts/bump_decdn.py @@ -9,7 +9,6 @@ rewrite --sums-sha256 --image-digest sha256: move every hand-copied pin (checklist steps 1, 3, 4 and 5 of ansible/roles/decdn_node/README.md, "Bumping the decdn release") - changelog add the bump entry under each collection's [Unreleased] The generated mirrors (step 2) and the vendored KEYS (step 6) are open-decdn-bump.sh's: they come from a checkout of the tag, not from this repo. Prose that names a version @@ -46,25 +45,6 @@ CHART = "charts/decdn-node/Chart.yaml" CHART_VALUES = "charts/decdn-node/values.yaml" -# Collection changelogs that get a bump entry while they hold an [Unreleased] section -# (until each collection's first release; release.sh generates every later section). -# The marker starts the entry, so a later bump replaces an earlier one; no other entry -# may start with it. -CHANGELOGS = { - "ansible/galaxy/node/CHANGELOG.md": ( - "- `decdn_node`: `decdn_node_version` defaults to", - "- `decdn_node`: `decdn_node_version` defaults to `{v}`, decdn/decdn v{v}. The\n" - " generated mirrors (`vars/main/networks.yml`, `schema-keys.txt`) are\n" - " regenerated from that tag.\n", - ), - "ansible/galaxy/publisher/CHANGELOG.md": ( - "- `sponsord_onramp`: the installers' decdn pin defaults to", - "- `sponsord_onramp`: the installers' decdn pin defaults to decdn/decdn `v{v}`\n" - " and that release's `SHA256SUMS` digest, following the `decdn_node` role's\n" - " `decdn_node_version`.\n", - ), -} - class ShapeError(Exception): """A file is not in the shape the bump expects.""" @@ -222,45 +202,6 @@ def verify(root: Path, version: str, sums: str, digest: str) -> None: raise ShapeError(f"{CHART_VALUES}: the example digest for v{version} is {example}, expected [{digest!r}]") -def changelog(root: Path, version: str) -> list[str]: - """Write the bump entry under [Unreleased]: in place of an earlier one, else first - under ### Changed.""" - if not VERSION.match(version): - raise ShapeError(f"{version} is not X.Y.Z") - changed = [] - for rel, (marker, entry) in CHANGELOGS.items(): - lines = read(root, rel).splitlines(keepends=True) - try: - start = lines.index("## [Unreleased]\n") - except ValueError: - # Released: release.sh writes every section from the PR titles. - print(f"bump_decdn: {rel} has no [Unreleased] section; no entry written", file=sys.stderr) - continue - end = next((i for i in range(start + 1, len(lines)) if lines[i].startswith("## ")), len(lines)) - earlier = [i for i in range(start, end) if lines[i].startswith(marker)] - if len(earlier) > 1: - raise ShapeError(f"{rel}: {len(earlier)} [Unreleased] entries start {marker!r}, expected at most 1") - if earlier: - # An earlier bump's entry: its bullet and the indented lines that continue it. - at = earlier[0] - stop = at + 1 - while stop < end and lines[stop].startswith(" "): - stop += 1 - del lines[at:stop] - else: - changed_at = next((i for i in range(start, end) if lines[i] == "### Changed\n"), None) - if changed_at is None: - raise ShapeError(f"{rel}: [Unreleased] has no ### Changed") - # First bullet of ### Changed: after the heading and its blank line. - at = changed_at + 1 - while at < end and lines[at].strip() == "": - at += 1 - lines[at:at] = [entry.format(v=version)] - (root / rel).write_text("".join(lines)) - changed.append(rel) - return changed - - def main() -> None: ap = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter) ap.add_argument("--root", type=Path, default=Path.cwd(), help="devops checkout (default: the current directory)") @@ -271,7 +212,6 @@ def main() -> None: rw.add_argument("version") rw.add_argument("--sums-sha256", required=True) rw.add_argument("--image-digest", required=True) - sub.add_parser("changelog").add_argument("version") args = ap.parse_args() try: @@ -282,9 +222,6 @@ def main() -> None: elif args.cmd == "rewrite": for rel in rewrite(args.root, args.version, args.sums_sha256, args.image_digest): print(f"wrote {rel}") - elif args.cmd == "changelog": - for rel in changelog(args.root, args.version): - print(f"wrote {rel}") except ShapeError as e: print(f"bump_decdn: {e}", file=sys.stderr) sys.exit(1) diff --git a/scripts/open-decdn-bump.sh b/scripts/open-decdn-bump.sh index 533fcc2..60011db 100755 --- a/scripts/open-decdn-bump.sh +++ b/scripts/open-decdn-bump.sh @@ -13,7 +13,7 @@ # `origin` credentials and opens the PR with your `gh` login: a PR opened that # way runs CI, which one opened by Actions' GITHUB_TOKEN would not. # -# The bump does checklist steps 1 to 6 and the collections' changelog entries. +# The bump does checklist steps 1 to 6. # Release facts (the SHA256SUMS digest, the decdn-node image digest) come from # the release's assets once their signatures verify against the vendored # decdn-release-KEYS.asc; the generated mirrors and KEYS come from a clone of the @@ -226,8 +226,6 @@ if ! cmp -s "$UPSTREAM/KEYS" "$KEYS"; then (\`ansible/roles/sponsord/files/sponsord-release-KEYS.asc\`) and SECURITY.md's fingerprints." fi -bump changelog "$VERSION" >/dev/null || die "could not add the changelog entries; no PR opened" - # Step 7 is a reviewer's: decdn/sponsord pins carry version numbers too, so a # bare old version is not always decdn's. The list holds the old version as a # whole word (not inside 127.0.0.1, nor a prefix of 0.0.10), outside the diff --git a/scripts/tests/test_bump_decdn.py b/scripts/tests/test_bump_decdn.py index cc0145e..93dfffc 100644 --- a/scripts/tests/test_bump_decdn.py +++ b/scripts/tests/test_bump_decdn.py @@ -9,9 +9,7 @@ from __future__ import annotations -import contextlib import importlib.util -import io import shutil import subprocess import sys @@ -34,7 +32,12 @@ bd.CHART, bd.CHART_VALUES, "ansible/molecule/validation-sponsord/converge.yml", - *bd.CHANGELOGS, +] +# rewrite never touches these: release.sh writes them. +CHANGELOGS = [ + "ansible/galaxy/node/CHANGELOG.md", + "ansible/galaxy/publisher/CHANGELOG.md", + "charts/decdn-node/CHANGELOG.md", ] SUMS = "a" * 64 DIGEST = "sha256:" + "b" * 64 @@ -44,7 +47,7 @@ class Fixture(unittest.TestCase): def setUp(self) -> None: self._tmp = tempfile.TemporaryDirectory() self.root = Path(self._tmp.name) - for rel in FILES: + for rel in FILES + CHANGELOGS: (self.root / rel).parent.mkdir(parents=True, exist_ok=True) shutil.copyfile(REPO / rel, self.root / rel) subprocess.run(["git", "init", "-q", str(self.root)], check=True) @@ -116,12 +119,14 @@ def test_leaves_the_sponsord_pin(self) -> None: bd.ONRAMP_DEFAULTS), sponsord) def test_leaves_the_changelogs(self) -> None: - for rel in bd.CHANGELOGS: - self.write(rel, self.text(rel) + f"\n{self.sums}\n") + image = self.digest.removeprefix("sha256:") + for rel in CHANGELOGS: + self.write(rel, self.text(rel) + f"\n{self.sums}\n{image}\n") subprocess.run(["git", "-C", str(self.root), "add", "-A"], check=True) bd.rewrite(self.root, "9.8.7", SUMS, DIGEST) - for rel in bd.CHANGELOGS: + for rel in CHANGELOGS: self.assertIn(self.sums, self.text(rel)) + self.assertIn(image, self.text(rel)) def test_refuses_a_reshaped_file(self) -> None: self.write(bd.COMPOSE_ENV, self.text(bd.COMPOSE_ENV).replace(f"# decdn/decdn v{self.version}\n", "")) @@ -180,68 +185,6 @@ def test_skips_binary_and_refuses_undecodable_files(self) -> None: bd.rewrite(self.root, "1.2.3", "e" * 64, DIGEST) -class Changelog(Fixture): - def unreleased_entries(self, rel: str) -> list[str]: - marker = bd.CHANGELOGS[rel][0] - return [line for line in self.text(rel).splitlines() if line.startswith(marker)] - - def test_replaces_the_entry_in_place(self) -> None: - bd.changelog(self.root, "9.8.7") - for rel in bd.CHANGELOGS: - with self.subTest(rel=rel): - entries = self.unreleased_entries(rel) - self.assertEqual(len(entries), 1) - self.assertIn("9.8.7", entries[0]) - # Only the entry moved. - old = self.original[rel].splitlines() - new = self.text(rel).splitlines() - self.assertEqual(len(old), len(new)) - self.assertEqual(sum(a != b for a, b in zip(old, new)), 1) - - def test_current_version_is_a_no_op(self) -> None: - bd.changelog(self.root, self.version) - for rel in bd.CHANGELOGS: - self.assertEqual(self.text(rel), self.original[rel]) - - def test_inserts_first_under_changed(self) -> None: - rel = "ansible/galaxy/node/CHANGELOG.md" - marker = bd.CHANGELOGS[rel][0] - lines = self.text(rel).splitlines(keepends=True) - at = next(i for i, line in enumerate(lines) if line.startswith(marker)) - while lines[at + 1].startswith(" "): - del lines[at + 1] - del lines[at] - self.write(rel, "".join(lines)) - bd.changelog(self.root, "9.8.7") - text = self.text(rel) - self.assertIn("### Changed\n\n" + bd.CHANGELOGS[rel][1].format(v="9.8.7"), text) - - def test_refuses_an_unreleased_without_changed(self) -> None: - rel = "ansible/galaxy/node/CHANGELOG.md" - marker = bd.CHANGELOGS[rel][0] - lines = [line for line in self.text(rel).splitlines(keepends=True) - if line != "### Changed\n" and not line.startswith(marker)] - self.write(rel, "".join(lines)) - with self.assertRaisesRegex(bd.ShapeError, "no ### Changed"): - bd.changelog(self.root, "9.8.7") - - def test_refuses_two_entries(self) -> None: - rel = "ansible/galaxy/publisher/CHANGELOG.md" - entry = bd.CHANGELOGS[rel][1].format(v="0.0.0") - self.write(rel, self.text(rel).replace("### Changed\n\n", "### Changed\n\n" + entry, 1)) - with self.assertRaisesRegex(bd.ShapeError, "2 \\[Unreleased\\] entries"): - bd.changelog(self.root, "9.8.7") - - def test_skips_a_released_collection(self) -> None: - rel = "ansible/galaxy/node/CHANGELOG.md" - released = self.text(rel).replace("## [Unreleased]\n", "## [0.1.0] — 2026-01-01\n") - self.write(rel, released) - with contextlib.redirect_stderr(io.StringIO()) as err: - self.assertNotIn(rel, bd.changelog(self.root, "9.8.7")) - self.assertIn(f"{rel} has no [Unreleased] section", err.getvalue()) - self.assertEqual(self.text(rel), released) - - class Cli(Fixture): """open-decdn-bump.sh reads stdout and the exit code; a refusal must exit 1."""