From 25958fcb7863be59614995a73f3e22d051bbc3e5 Mon Sep 17 00:00:00 2001 From: Jonathan Miller Date: Sat, 18 Jul 2026 15:57:57 -0500 Subject: [PATCH] fix(ci): make changelog promotion idempotent and structure-preserving The auto-release 'promote changelog' step used a blind string replace of the '## [Unreleased]' marker, which injected an empty version header, pushed the intro block below the version headers, and compounded on every re-run. Rewrite it to parse the changelog into a top header block plus '## [' sections, promote only when [Unreleased] has real content (a ### subheading or a bullet), move that content under a new version header, and leave [Unreleased] empty. No-op runs leave the file byte-identical (EOL-preserving) so CI makes no spurious commit. Authored-by: Moko Consulting --- .mokogit/workflows/auto-release.yml | 33 +++++++++++++++++++++++------ 1 file changed, 27 insertions(+), 6 deletions(-) diff --git a/.mokogit/workflows/auto-release.yml b/.mokogit/workflows/auto-release.yml index 436396d..ab467dd 100644 --- a/.mokogit/workflows/auto-release.yml +++ b/.mokogit/workflows/auto-release.yml @@ -349,13 +349,34 @@ jobs: if [ -n "$VERSION" ] && [ -f "CHANGELOG.md" ]; then DATE=$(date +%Y-%m-%d) python3 -c " - import sys + import sys, re version, date = sys.argv[1], sys.argv[2] - content = open('CHANGELOG.md').read() - old = '## [Unreleased]' - new = f'## [Unreleased]\n\n## [{version}] --- {date}' - content = content.replace(old, new, 1) - open('CHANGELOG.md', 'w').write(content) + with open('CHANGELOG.md', 'r', newline='') as f: + raw = f.read() + eol = '\r\n' if '\r\n' in raw else '\n' + content = raw.replace('\r\n', '\n') + m = re.search(r'(?m)^## \[', content) + if not m: + sys.exit(0) + head, rest = content[:m.start()], content[m.start():] + tokens = re.split(r'(?m)(^## \[.*)', rest) + sections, i = [], 1 + while i < len(tokens): + sections.append([tokens[i], tokens[i+1] if i+1 < len(tokens) else '']) + i += 2 + out, promoted = head, False + for header, body in sections: + if header.strip() == '## [Unreleased]' and re.search(r'(?m)^\s*(###\s|[-*]\s)\S', body): + out += '## [Unreleased]\n\n' + f'## [{version}] --- {date}' + body + promoted = True + else: + out += header + body + if not promoted: + sys.exit(0) + if eol != '\n': + out = out.replace('\n', eol) + with open('CHANGELOG.md', 'w', newline='') as f: + f.write(out) " "$VERSION" "$DATE" git add CHANGELOG.md git commit -m "chore: promote changelog [Unreleased] → [${VERSION}]" || true -- 2.52.0