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
6 changes: 4 additions & 2 deletions .github/actions/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -374,8 +374,10 @@ Requirements in the consumer repo:
- Org variable `CI_APP_ID` / secret `MEGA_MAXWELL_PK` (the Maxwell app). PRs
and tag pushes must come from an App token: `GITHUB_TOKEN` does not trigger
downstream workflows.
- An environment `release` with required reviewers on the publish job — this
is the human gate for creating a tag.
- A reviewed settle PR is the human gate for creating a tag: the
`release-*` branch ruleset below requires it. No environment approval is
needed on the publish job (an `environment:` with required reviewers can
be added by repos that want a second, separate approver).
- A tag ruleset for `v*` (no creation/deletion/force-push) with the app as a
bypass actor, so `release-publish` is the only tag creator.
- A branch ruleset for `release-*` requiring PRs, so settlement is always a
Expand Down
14 changes: 10 additions & 4 deletions .github/actions/release-tools/release_tools.py
Original file line number Diff line number Diff line change
Expand Up @@ -166,21 +166,27 @@ def _section_span(text: str, version: str) -> tuple[int, int] | None:
return None


def _tidy(text: str) -> str:
"""Exactly one blank line between blocks and one newline at EOF, so the
result is stable under formatters like dprint/prettier."""
return re.sub(r"\n{3,}", "\n\n", text).rstrip("\n") + "\n"


def insert_changelog_section(text: str, version: str, section: str) -> tuple[str, str]:
"""Insert `section` as the newest entry (or replace an existing entry for
the same version). Returns (new_text, "inserted"|"replaced"). An empty or
missing changelog gets the standard header."""
section = section.rstrip("\n") + "\n\n"
if not text.strip():
return CHANGELOG_HEADER + "\n" + section, "inserted"
return _tidy(CHANGELOG_HEADER + "\n" + section), "inserted"
span = _section_span(text, version)
if span:
s, e = span
return text[:s] + section + text[e:].lstrip("\n"), "replaced"
return _tidy(text[:s] + section + text[e:].lstrip("\n")), "replaced"
first = re.search(r"^## ", text, re.M)
if first:
return text[: first.start()] + section + text[first.start():], "inserted"
return text.rstrip("\n") + "\n\n" + section, "inserted"
return _tidy(text[: first.start()] + section + text[first.start():]), "inserted"
return _tidy(text.rstrip("\n") + "\n\n" + section), "inserted"


def extract_changelog_section(text: str, version: str) -> str | None:
Expand Down
12 changes: 12 additions & 0 deletions .github/actions/release-tools/test_release_tools.py
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,18 @@ def test_replace_same_version(self):
self.assertNotIn("- b (", twice)
self.assertIn("## v0.1.0", twice)

def test_spacing_is_formatter_stable(self):
# One blank line between blocks, one newline at EOF — whether the
# section lands at the end (no prior entries), at the top, or replaces.
for text in ("", "# Changelog\n\nIntro line.\n", self.OLD):
new, _ = rt.insert_changelog_section(text, "0.2.0", self.SEC)
self.assertFalse(new.endswith("\n\n"), repr(new[-10:]))
self.assertTrue(new.endswith("\n"))
self.assertNotIn("\n\n\n", new)
twice, _ = rt.insert_changelog_section(new, "0.2.0", self.SEC)
self.assertNotIn("\n\n\n", twice)
self.assertFalse(twice.endswith("\n\n"))

def test_extract_missing(self):
self.assertIsNone(rt.extract_changelog_section(self.OLD, "9.9.9"))

Expand Down
7 changes: 3 additions & 4 deletions workflow-templates/release-publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,9 @@ name: Release Publish
# Publishes a settled release when its settle PR merges into the release
# branch: annotated tag at the merge commit (once), GitHub Release with the
# changelog section as notes, and a back-merge PR of the changelog to the
# default branch. The job runs under the `release` environment so an admin
# approves every publish; create that environment with required reviewers,
# and add the CI app as a bypass actor on the `v*` tag ruleset.
# default branch. Merging the settle PR is the release approval (the
# `release-*` branch ruleset requires a reviewed PR), so no environment gate
# is needed; add the CI app as a bypass actor on the `v*` tag ruleset.
#
# This file must exist on the release branch (it does, when the branch is cut
# from a default branch that has it).
Expand All @@ -25,7 +25,6 @@ jobs:
startsWith(github.event.pull_request.head.ref, 'chore/release-settle-') &&
github.event.pull_request.user.login == 'mega-maxwell[bot]'
runs-on: ubuntu-latest
environment: release
steps:
- uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3
id: app-token
Expand Down
Loading