Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 2 additions & 7 deletions ansible/roles/decdn_node/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -731,12 +731,7 @@ The steps:
copy, and `SECURITY.md`'s fingerprints).
7. The prose that names the version: `git grep -n '<old version>'` 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
Expand Down
4 changes: 1 addition & 3 deletions ansible/roles/sponsord/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 '<old version>'` 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
Expand Down
63 changes: 0 additions & 63 deletions scripts/bump_decdn.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@
rewrite <version> --sums-sha256 <hex> --image-digest sha256:<hex>
move every hand-copied pin (checklist steps 1, 3, 4 and 5 of
ansible/roles/decdn_node/README.md, "Bumping the decdn release")
changelog <version> 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
Expand Down Expand Up @@ -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."""
Expand Down Expand Up @@ -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)")
Expand All @@ -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:
Expand All @@ -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)
Expand Down
4 changes: 1 addition & 3 deletions scripts/open-decdn-bump.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
81 changes: 12 additions & 69 deletions scripts/tests/test_bump_decdn.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,7 @@

from __future__ import annotations

import contextlib
import importlib.util
import io
import shutil
import subprocess
import sys
Expand All @@ -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
Expand All @@ -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)
Expand Down Expand Up @@ -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", ""))
Expand Down Expand Up @@ -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."""

Expand Down
Loading