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
2 changes: 1 addition & 1 deletion .github/workflows/release-github.yml
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ jobs:
- uses: actions/checkout@v5
with:
repository: BehindTheMusicTree/github-workflows
ref: 8ae00ab34e74ffe16977cf28bdc93ecd6724ed6d
ref: bbd93e5c3ba2335515b35e1a163f305db51478d7
persist-credentials: false
path: foundation
- name: Validate and publish the existing tag
Expand Down
4 changes: 2 additions & 2 deletions .github/workflows/release-tag.yml
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ jobs:
- uses: actions/checkout@v5
with:
repository: BehindTheMusicTree/github-workflows
ref: f304059fd7e9a9d0743a9e6a76ae11ea6e12f121
ref: bbd93e5c3ba2335515b35e1a163f305db51478d7
persist-credentials: false
path: foundation
- name: Validate the exact push
Expand All @@ -68,7 +68,7 @@ jobs:
- uses: actions/checkout@v5
with:
repository: BehindTheMusicTree/github-workflows
ref: f304059fd7e9a9d0743a9e6a76ae11ea6e12f121
ref: bbd93e5c3ba2335515b35e1a163f305db51478d7
persist-credentials: false
- name: Require App configuration
env:
Expand Down
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,10 @@ All contributors (including maintainers) should update `CHANGELOG.md` when creat

## [Unreleased]

### Fixed

- Release validation and publication preserve complete changelog notes containing fenced Markdown headings and ignore fenced version examples.

### Changed

- Adopt CI-owned tags and changelog-based GitHub publication for this repository, preserving script-based release preparation.
Expand Down
11 changes: 1 addition & 10 deletions release/github-release.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,16 +19,7 @@
def release_notes(changelog, tag):
if not re.fullmatch(r'v(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)', tag):
raise ValueError('Expected a stable vX.Y.Z tag')
lines = changelog.splitlines()
starts = [i for i, line in enumerate(lines) if re.fullmatch(r'## \[' + re.escape(tag[1:]) + r'\](?:[^\S\n][^\n]*)?', line)]
if len(starts) != 1:
raise ValueError('Expected one version changelog section')
end = next((i for i in range(starts[0] + 1, len(lines)) if lines[i].startswith('## ')), len(lines))
body = '\n'.join(lines[starts[0] + 1:end]).strip()
body = re.sub(r'<!--.*?-->', '', body, flags=re.DOTALL).strip()
if not re.sub(r'^#{1,6}\s.*$', '', body, flags=re.MULTILINE).strip():
raise ValueError('Version changelog section is empty')
return body
return VALIDATION['changelog_section'](changelog, tag[1:])


def verify_release(release, tag):
Expand Down
8 changes: 8 additions & 0 deletions release/test_github_release.py
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,14 @@ def test_changelog(self):
with self.assertRaises(ValueError):
module.release_notes('## [2.3.0]\nA', 'v2.3.0-rc.1')

def test_fenced_headings_preserve_complete_release_notes(self):
for fence in ('```md', '~~~markdown', '````md'):
close = fence.split('m')[0]
notes = '- Example syntax:\n\n' + fence + '\n## Example\n## [2.3.0]\n' + close + '\n\n- Other fix.'
text = '```md\n## [2.3.0]\n- Example version\n```\n\n## [2.3.0]\n\n' + notes + '\n\n## [2.2.0]\n- Old'
with self.subTest(fence=fence):
self.assertEqual(module.release_notes(text, TAG), notes)

def test_create_then_retry_without_duplicate(self):
api = FakeAPI([module.APIError(404), RELEASE])
self.assertEqual(module.publish(api, TAG, COMMIT, 'Notes'), 'created')
Expand Down
4 changes: 4 additions & 0 deletions release/test_validate_release.py
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,10 @@ def check(self, after, policy_path='policy.json'):
finally:
os.chdir(previous)

def test_validation_ignores_fenced_example_versions(self):
self.write('CHANGELOG.md', '```md\n## [1.0.0]\n- Example\n```\n\n## [1.0.0]\n- Real\n```md\n## Example\n```\n- Last\n')
self.assertEqual(self.check(self.commit()), {'release': False})

def test_no_bump_is_noop_even_without_tag(self):
self.assertEqual(self.check(self.commit()), {'release': False})

Expand Down
35 changes: 28 additions & 7 deletions release/validate-release.py
Original file line number Diff line number Diff line change
Expand Up @@ -99,19 +99,40 @@ def policy_at(commit, path):
return policy


def changelog_section(text, version):
lines = text.splitlines()
headings = []
fence = None
for index, line in enumerate(lines):
marker = re.match(r"^ {0,3}(`{3,}|~{3,})(.*)$", line)
if marker:
run, tail = marker.groups()
if fence is None and (run[0] != "`" or "`" not in tail):
fence = run
elif fence and run[0] == fence[0] and len(run) >= len(fence) and not tail.strip():
fence = None
continue
if fence is None and line.startswith("## "):
headings.append((index, line))
matches = [index for index, line in headings
if re.fullmatch(r"## \[" + re.escape(version) + r"\](?:[^\S\n][^\n]*)?", line)]
if len(matches) != 1:
raise ValueError("exactly one versioned changelog section required")
end = next((index for index, _ in headings if index > matches[0]), len(lines))
body = "\n".join(lines[matches[0] + 1:end]).strip()
body = re.sub(r"<!--.*?-->", "", body, flags=re.DOTALL).strip()
if not re.sub(r"^#{1,6}\s.*$", "", body, flags=re.MULTILINE).strip():
raise ValueError("versioned changelog section is empty")
return body


def check_current(commit, policy):
version = version_at(commit, policy["version"])
for mirror in policy["mirrors"]:
if version_at(commit, mirror) != version:
raise ValueError("version mirror mismatch")
changelog = read_file(commit, policy["changelog"])
sections = list(re.finditer(r"^## \[" + re.escape(version) + r"\](?:[^\S\n][^\n]*)?$", changelog, re.MULTILINE))
if len(sections) != 1:
raise ValueError("exactly one versioned changelog section required")
body = re.split(r"^## ", changelog[sections[0].end():], maxsplit=1, flags=re.MULTILINE)[0]
body = re.sub(r"<!--.*?-->", "", body, flags=re.DOTALL)
if not any(line.strip() and not line.lstrip().startswith("#") for line in body.splitlines()):
raise ValueError("versioned changelog section is empty")
changelog_section(changelog, version)
git("check-ref-format", f"refs/tags/{policy['tag_prefix']}{version}")
return version

Expand Down
Loading