diff --git a/.github/workflows/version_branch.yml b/.github/workflows/version_branch.yml index 61e2e13..5af30d2 100644 --- a/.github/workflows/version_branch.yml +++ b/.github/workflows/version_branch.yml @@ -443,6 +443,132 @@ jobs: print("[INFO] Updated manifests:", len(updated_manifests)) PY + - name: Update CHANGELOG (move Unreleased into version entry) + run: | + source "$CI_HELPERS" + moko_init "Update CHANGELOG" + + if [[ ! -f "CHANGELOG.md" ]]; then + echo "[FATAL] CHANGELOG.md not found." >&2 + exit 2 + fi + + python3 - <<'PY' + import os + from datetime import datetime, timezone + from pathlib import Path + + new_version = (os.environ.get("NEW_VERSION") or "").strip() + if not new_version: + raise SystemExit("[FATAL] NEW_VERSION not set") + + stamp = datetime.now(timezone.utc).strftime("%Y-%m-%d") + p = Path("CHANGELOG.md") + lines = p.read_text(encoding="utf-8", errors="replace").splitlines(True) + + def is_h2(line: str) -> bool: + return line.lstrip().startswith("## ") + + def norm(line: str) -> str: + return line.strip().lower() + + unreleased_idx = None + for i, line in enumerate(lines): + if norm(line) == "## [unreleased]": + unreleased_idx = i + break + + version_idx = None + for i, line in enumerate(lines): + if line.lstrip().startswith(f"## [{new_version}]"): + version_idx = i + break + + if unreleased_idx is None: + if version_idx is None: + # Insert new version entry after top H1 if present, else at start. + insert_at = 0 + for i, line in enumerate(lines): + if line.lstrip().startswith("# "): + insert_at = i + 1 + while insert_at < len(lines) and lines[insert_at].strip() == "": + insert_at += 1 + break + + entry = [" +", f"## [{new_version}] - {stamp} +", " +", "- No changes recorded. +", " +"] + lines[insert_at:insert_at] = entry + p.write_text("".join(lines), encoding="utf-8") + raise SystemExit(0) + + # Find Unreleased body range. + u_start = unreleased_idx + 1 + u_end = len(lines) + for j in range(u_start, len(lines)): + if is_h2(lines[j]): + u_end = j + break + + unreleased_body = "".join(lines[u_start:u_end]).strip() + + # Ensure version header exists. If not, create immediately after Unreleased header. + if version_idx is None: + insert_at = u_end + header = [" +", f"## [{new_version}] - {stamp} +", " +"] + lines[insert_at:insert_at] = header + version_idx = insert_at + 1 + + # If we created the header, version_idx points to the header line. + # Insert moved body after version header line. + if unreleased_body: + # Recompute version header index safely after any insertions. + for i, line in enumerate(lines): + if line.lstrip().startswith(f"## [{new_version}]"): + version_idx = i + break + + insert_at = version_idx + 1 + while insert_at < len(lines) and lines[insert_at].strip() == "": + insert_at += 1 + + body_lines = [" +"] + [x + (" +" if not x.endswith(" +") else "") for x in unreleased_body.split(" +")] + [" +"] + lines[insert_at:insert_at] = body_lines + + # Clear Unreleased body. + # Recompute Unreleased indices after modifications. + unreleased_idx = None + for i, line in enumerate(lines): + if norm(line) == "## [unreleased]": + unreleased_idx = i + break + + if unreleased_idx is not None: + u_start = unreleased_idx + 1 + u_end = len(lines) + for j in range(u_start, len(lines)): + if is_h2(lines[j]): + u_end = j + break + + # Replace body with exactly one blank line. + lines[u_start:u_end] = [" +"] + + p.write_text("".join(lines), encoding="utf-8") + PY + - name: Commit changes if: ${{ env.REPORT_ONLY != 'true' }} run: |