Files
hermes-agent/scripts/releases/draft_warning.py
ethernet fcd1fdd57b feat(release): warn in the draft body against publishing it by hand
Nobody should publish a stable release from the GitHub UI, and once
immutable releases land that mistake is permanent: the release stays on
the attempt ref with no receipt tag, no feed move, and no alias move.
The draft now carries a fenced caution above and below the generated
notes, and publish strips both fences before the release goes public,
refusing an unbalanced fence. The notes are fetched from the API and
written to a file because --generate-notes cannot place text below the
notes.
2026-09-23 12:53:00 -04:00

60 lines
2.4 KiB
Python

"""Fenced draft-body warning and its strip, shared by the entrypoint and publish.
Immutable releases take no edits after publication, so the draft carries a
caution against publishing it by hand at both ends of the body and the
publication pass strips both fenced blocks while the release is still a draft.
The helpers live here so the entrypoint and the publish step can import them
without a cycle.
"""
from __future__ import annotations
WARNING_OPEN = "<!-- hermes-release:draft-warning -->"
WARNING_CLOSE = "<!-- /hermes-release:draft-warning -->"
_WARNING = """> [!CAUTION]
> ## **DO NOT PUBLISH THIS RELEASE FROM GITHUB.**
> **This is attempt `{attempt_ref}`. Publishing it here skips the `v{version}` receipt tag, the update feeds, the Docker aliases, and the Store release. Releases are immutable, so a release published here cannot be fixed.**
>
> **Run this instead:**
> ```
> python scripts/release.py publish --version {version}
> ```
> **To drop this attempt:** `python scripts/release.py abandon --version {version}`"""
def draft_body(*, version: str, attempt_ref: str, notes: str) -> str:
"""The draft body: fenced warning block, generated notes, fenced warning block."""
block = "\n".join([WARNING_OPEN, _WARNING.format(version=version, attempt_ref=attempt_ref),
WARNING_CLOSE])
return "\n".join([block, notes, block])
def strip_draft_warning(body: str) -> str:
"""Remove each fenced warning block, fence lines included.
The fences must pair up in order; anything else means the draft body was
edited underneath the tool, and publish refuses rather than publishing a
mangled body.
"""
kept: list[str] = []
inside = False
for line in body.splitlines():
stripped = line.strip()
if inside:
if stripped == WARNING_OPEN:
raise ValueError("draft warning fences are unbalanced")
if stripped == WARNING_CLOSE:
inside = False
continue
else:
if stripped == WARNING_OPEN:
inside = True
continue
if stripped == WARNING_CLOSE:
raise ValueError("draft warning fences are unbalanced")
kept.append(line)
text = "\n".join(kept)
if inside or WARNING_OPEN in text or WARNING_CLOSE in text:
raise ValueError("draft warning fences are unbalanced")
return text