Generated changelog prose stays reviewable only after a human release owner locks version, date, breaking changes, and security notes. A model may then summarize commit subjects that already match an allowlist, and it must not invent impact language. This article specifies that ownership split, a release packet schema, and a proposed Python gate for the draft. Treat the listed code as an unexecuted reference implementation, not as a measured result from a production release.
Why summaries and contracts must stay apart
A changelog mixes two different evidence classes, and a single generation prompt collapses that difference into one voice. Commit subjects are historical text that a parser can group, while version numbers and support windows are decisions a person must accept. When both classes sit in one instruction, the draft can sound complete even though no owner confirmed impact. The practical rule is therefore narrow: draft narration from locked inputs, and refuse the draft when those inputs are absent.
Keep a Changelog 1.1.0 separates Added, Changed, Deprecated, Removed, Fixed, and Security into heading names only. Those headings are a vocabulary for organizers, not permission for a model to invent missing entries. Conventional Commits 1.0.0 defines types such as feat, fix, and docs, plus a breaking-change footer a script can read. Semantic Versioning 2.0.0 defines how version numbers move, but it does not decide whether users can absorb that move.
Primary pages for those three specifications should be rechecked before anyone encodes them into a release bot. This article cites Keep a Changelog 1.1.0, Conventional Commits 1.0.0, and Semantic Versioning 2.0.0 as stable references. It does not claim that a newer revision was reviewed on 2026-10-09, so the links below remain the verification path. Use the linked pages rather than a copied summary if your tooling already pins a different specification version.
- Keep a Changelog 1.1.0: https://keepachangelog.com/en/1.1.0/
- Conventional Commits 1.0.0: https://www.conventionalcommits.org/en/v1.0.0/
- Semantic Versioning 2.0.0: https://semver.org/spec/v2.0.0.html
Field rights before any prose exists
The table below assigns each packet field to a writer and states the merge rule that precedes prose. Human fields record decisions, while grouped summaries are narration over commits the owner already accepted into the packet. An empty breaking-change list is valid only when the owner recorded that absence on purpose in the packet. A missing list is a different fact, because a model can fill silence with plausible incidents that nobody reviewed.
| Packet field | Evidence source | Who may write it | Merge rule |
|---|---|---|---|
version |
Human release decision | Human owner | Required SemVer core triple |
release_date |
Owner calendar confirmation | Human owner | Required YYYY-MM-DD
|
breaking_changes |
Owner list, not commit adjectives | Human owner | Present list, empty allowed |
security_notes |
Advisory process outside the model | Human owner | Empty or already approved |
support_window |
Team support policy allowlist | Human owner | Required, no invented labels |
commits |
Reviewed subjects and short SHAs | Human owner selects rows | Types limited before drafting |
grouped_summaries |
Brief built only from commits
|
Model after the gate | Each bullet must cite a packet SHA |
sign_off |
Named role and timestamp | Human owner | Required before a brief is printed |
Read the table as a control surface, not as guidance for sentence style or marketing tone. Support window values should come from a team allowlist, and the sample uses three labels only as placeholders. Replace those labels with the words your support policy actually uses before the first real packet is signed. Do not let a model invent a fourth support label in order to make a draft sound more precise.
Packet file the gate will read
Store the human packet in release_packet.json beside the changelog, and store model output in a separate file. The JSON example is illustrative, and it was not copied from a shipping product or a customer release. Short SHAs are placeholders, so replace them with commits you can open before anyone trusts a summary. Subject lines in the sample are synthetic, and they should not be cited as evidence of a real defect or feature.
{
"version": "1.4.0",
"release_date": "2026-10-09",
"support_window": "current_minor_only",
"breaking_changes": [],
"security_notes": [],
"commits": [
{"sha": "a1b2c3d", "type": "feat", "subject": "add export filter for closed accounts"},
{"sha": "e4f5a6b", "type": "fix", "subject": "retain timezone when saving weekly reports"}
],
"sign_off": {"name": "release-owner", "role": "release", "signed_at": "2026-10-09T15:00:00Z"}
}
Numbered workflow
- Collect candidate subjects with a normal git log command, then copy only reviewed lines into the commits array. A subject that you would not say to a user does not become safer because a model rewrites the wording. The owner deletes or edits bad subjects before sign-off, because the gate checks structure rather than factual accuracy. Keep the command output in the review notes so a later reader can see which range was considered.
git log --pretty=format:'%h%x09%s' v1.3.0..HEAD
Fill version, release date, support window, breaking changes, and security notes without a model in the loop. Use an empty array when the honest answer is none, and record that choice where reviewers can see it. Do not ask a model whether a refactor is breaking, because a commit type is not a customer contract. If security notes are non-empty, the named security owner must already have approved each identifier outside this workflow.
Run the proposed checker so it either prints a constrained draft brief or exits with status 2. The checker refuses the brief when sign-off, version shape, date shape, or the support allowlist fails. It also refuses commit types outside feat, fix, perf, and docs, which keeps chores out of user-facing summaries. Refactor and chore commits can still matter internally, but they should not be narrated as product changes by default.
Send only the printed brief to the model, then save the returned Markdown as
summary_draft.mdfor scanning. The brief allows grouping under Added, Changed, and Fixed, and it requires each bullet to end with a packet SHA. It forbids version promises, calendar dates, advisory identifiers, and any sentence that orders users to migrate now. If the model adds a heading that the brief did not allow, discard the file instead of editing the extra claim into safety.Scan the draft with the same checker in scan mode before a human spends time on tone. Any forbidden token fails the run even when the surrounding paragraph is clear and grammatically clean. A release owner still reads every bullet against the cited commit, because a matching SHA does not prove the subject. Open the commit in the repository when the subject and the diff disagree, and then rewrite the bullet by hand.
Merge only after the owner accepts or rewrites the draft and the signed packet remains byte-for-byte unchanged. If any impact field changes, discard the draft and generate a new brief from the updated packet. That order stops a polished paragraph from outliving the decision it appears to describe to readers. Keep the brief, the draft, and the packet in the same pull request so reviewers can audit the boundary.
Proposed checker and unexecuted checks
The script below is a proposal for local review, and it has not been executed for this article. It uses only the Python standard library, so the control flow does not depend on a model vendor SDK. Run it with python3 check_release_packet.py release_packet.json and expect a non-zero exit when human fields are incomplete. Pass --scan and a draft path only after a brief has been produced from the same packet file.
#!/usr/bin/env python3
"""Proposed gate: refuse changelog drafts until human impact fields exist."""
import json, re, sys
SUPPORT = {"current_minor_only", "current_major", "extended_lts"}
TYPES = {"feat", "fix", "perf", "docs"}
SEMVER = re.compile(r"^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)$")
FORBIDDEN = re.compile(r"\b(CVE-\d{4}-\d+|SLA|guaranteed|must migrate)\b", re.I)
def require_human(packet):
errors = []
if not SEMVER.match(str(packet.get("version", ""))):
errors.append("version must be a SemVer core triple")
if not re.match(r"^\d{4}-\d{2}-\d{2}$", str(packet.get("release_date", ""))):
errors.append("release_date must be YYYY-MM-DD")
if packet.get("support_window") not in SUPPORT:
errors.append("support_window is outside the allowlist")
for key in ("breaking_changes", "security_notes", "commits"):
if not isinstance(packet.get(key), list):
errors.append(f"{key} must be a list")
sign = packet.get("sign_off") or {}
if not sign.get("name") or not sign.get("role") or not sign.get("signed_at"):
errors.append("sign_off name, role, and signed_at are required")
for item in packet.get("commits") or []:
if item.get("type") not in TYPES:
errors.append("commit type not draftable: %s" % item.get("type"))
if not re.match(r"^[0-9a-f]{7,40}$", str(item.get("sha", ""))):
errors.append("commit sha must be lowercase hex")
return errors
def brief(packet):
lines = [
"Summarize only these commits. Cite each sha.",
"Do not add versions, dates, CVEs, or migration orders.",
]
for item in packet["commits"]:
lines.append("- %s %s: %s" % (item["type"], item["sha"], item["subject"]))
return "\n".join(lines)
def main():
with open(sys.argv[1], encoding="utf-8") as handle:
packet = json.load(handle)
errors = require_human(packet)
if errors:
print("\n".join(errors), file=sys.stderr)
return 2
if len(sys.argv) > 2 and sys.argv[2] == "--scan":
with open(sys.argv[3], encoding="utf-8") as handle:
text = handle.read()
shas = {item["sha"] for item in packet["commits"]}
if FORBIDDEN.search(text) or not any(sha in text for sha in shas):
print("draft failed citation or forbidden-language scan", file=sys.stderr)
return 3
print("scan passed")
return 0
print(brief(packet))
return 0
if __name__ == "__main__":
sys.exit(main())
A passing scan is not a release approval, and the forbidden-language list in the sample is intentionally small. A careful sentence can still overclaim without using CVE, SLA, guaranteed, or the phrase must migrate. Extend the expression from incidents your team has actually recorded, rather than from hypothetical model failures. Do not treat this sample as a complete compliance control for regulated products or public security notices.
The version expression accepts only a core triple, so tags such as release candidates need an owner exception. The scan checks that at least one packet SHA appears, which is weaker than requiring every bullet to cite one. Tighten that test before you rely on it, and keep the human read of each bullet even after you do. These gaps are reasons to treat the script as a gate sketch, not as a finished release system.
The following checks describe how to validate the proposal, and they were not run for this article. Use a temporary directory and synthetic JSON so the exercise cannot touch a real release branch. Expect exit status 0 only for the complete packet, and expect status 2 when sign-off is removed. Expect status 3 when the draft contains an advisory token or omits every SHA that the packet listed.
python3 check_release_packet.py release_packet.json
python3 check_release_packet.py release_packet.json --scan summary_draft.md
Where a free model may enter
Disclosure: This article was prepared as part of MonkeyCode's product outreach.
MonkeyCode free model access belongs in this workflow only after the checker prints a constrained brief. The brief is the instruction boundary, and the model should not receive unsigned impact fields or raw repository secrets. The free server option is a candidate host for the checker only when that environment already provides Python 3. This article does not assert image contents, quotas, hardware, durations, or that either free option is permanent.
If that access is already available, prefer the free server for the checker only when Python 3 is already installed there. That order is the useful product step in this method, and the same gate still works on any Python 3 host you trust. Do not add a second model pass to invent missing security notes, because absence must stay an owner decision. Remove the product from the procedure and the packet, the brief, and the scan remain the parts that protect readers.
Who should not use this split
Do not use this split for security advisories, legal notices, or status pages, because those pages are commitments. Skip the workflow when no named person will sign the packet, since an unsigned file is only a draft container. Also skip it when history is squashed into vague messages that hide user-visible behavior behind a single merge commit. Private customer notices are a poor fit, because a missing qualifier can change an agreement the checker cannot read.
The gate does not prove that a commit subject is true, and a sloppy subject can pass every regular expression. Free model access adds no factual warranty, and a free server does not store an approval or a legal signature. As of 2026-10-09, recheck the three linked specification pages before you encode version policy into automation. Review the packet, the brief, and the draft together, and reject any bullet whose SHA you cannot open.
Top comments (0)