DEV Community

Avery Lin
Avery Lin

Posted on

Pin Change-Ticket Facts Before a Model Drafts the Release Note

Release notes become unsafe when a model invents severity, migration cost, or support promises that no reviewer signed. A practical split is to store those obligations in a change ticket and let generated prose only restate ticket fields. The ticket is the review artifact, and the note is a rendering that CI can reject when it adds new commitments. This article specifies that workflow, a small checker, and the cases where the checker is not sufficient.

Why generated release notes drift

A release note has two jobs that many teams collapse into a single paragraph during release week. One job is orientation, which covers what changed, which module moved, and where the worked example lives. The other job is obligation: severity, downtime, data migration, compatibility, and who must act before upgrade. Orientation can be rewritten many times without changing the contract the reader is expected to rely on.

Obligations change the reader's cost, so they belong in a file a human edits and another human approves. Models are useful on the orientation job because they can turn a terse ticket into readable paragraphs. They are a poor owner of the obligation job because fluent text can imply a promise the ticket never made. A sentence such as upgrade in place with no data rewrite is a commitment, even when it appears inside a casual summary.

The workflow below refuses to treat that sentence as draftable unless the ticket already says it. That refusal is mechanical, and it should fail closed when the ticket is missing, unsigned, or still marked draft. A clear paragraph that invents a safer upgrade path is still a failed note under this policy. Clarity without an approved obligation is not an acceptable substitute for a signed change ticket review.

What the human owns and what the model may draft

Region Owner Draftable by a model Gate
Severity enum Human No Required before prose
Breaking-change flag Human No Required before prose
Data-migration enum Human No Required before prose
Downtime enum Human No No free-text downtime
Minimum supported version Human No Exact token in the note
Non-goals Human No Copied, not paraphrased
Module orientation Model from ticket facts Yes Must cite ticket id
Walkthrough of pinned commands Model from ticket commands Yes Commands copied verbatim
SLA, apology, or zero-risk wording Not draftable No Checker failure

The table is a policy for authorship, not a measurement of any vendor or any hosted model. It does not claim that a given model will follow the policy once a prompt is sent. Enforcement comes from the ticket file and the local checker, which you can run before any network call. If the checker and the table disagree during a review, the human-owned enums win and the prose must change.

Artifact: a change ticket with closed fields

Keep one ticket per user-visible change so reviewers can approve obligations without reading generated prose first. Closed fields use enums so a drafting model cannot helpfully expand them into softer or broader promises. Free-text fields are limited to facts a reviewer can check in the diff, such as module names and pinned commands. The example below is a proposed schema for a local repository, not a report of a production rollout.

schema = "change-ticket/1"
id = "CHG-1842"
status = "human-approved"

[ownership]
severity = "high"
breaking = true
data_migration = "required"
downtime = "brief"
min_version = "2.4.0"
approver = "replace-with-reviewer-handle"

[facts]
modules = ["billing.invoice", "billing.export"]
pinned_commands = [
  "billing export --since 2026-09-01 --dry-run",
]
non_goals = [
  "does not backfill invoices created before 2.4.0",
  "does not change tax rounding rules",
]

[draft_policy]
allow_model_orientation = true
forbid_unlisted_commitments = true
Enter fullscreen mode Exit fullscreen mode

Notice what is absent from the schema, because missing fields are a deliberate limit rather than an oversight in the example. There is no field for marketing tone, no uptime percentage, and no customer count a model could invent. If those claims matter to readers, they need a separate approved source, not a documentation draft from a model. Also notice that status must be human-approved before a generated note is allowed to pass the checker.

Numbered workflow

  1. Write the ticket from the diff, and fill only fields you can point to in code, tests, or an explicit product decision.
  2. Ask a second reviewer to sign approver and set status to human-approved, using your normal pull-request review rather than a model score.
  3. Optionally send the ticket, not the whole repository, to a drafting step that uses MonkeyCode's free model access and free server option. Disclosure: This article was prepared as part of MonkeyCode's product outreach. Those two availability facts are the only product claims this workflow relies on, and they are operator-supplied. Quotas, model names, hardware, and duration stay unspecified because they were not provided as stable inputs.
  4. Require the draft to quote the ticket id, the severity line, the minimum version, and every non-goal without softer paraphrases.
  5. Run the checker in CI against the ticket and the generated Markdown, and block merge on any exit code other than zero.
  6. Have the same approver read the note once for implication risk, because a keyword checker cannot see every implied promise.
  7. Publish only the ticket-approved note, and store any draft transcript outside the published tree if your tool keeps one.

Step 3 is optional for teams that prefer to write orientation paragraphs without an external drafting call. Teams that cannot use an external drafting service can still fill the orientation paragraphs by hand and keep the checker. The free server option is a convenience for the draft pass, not a reason to skip steps 2, 5, or 6. Nothing in the checker requires a particular vendor SDK, a hosted runtime, or a logged-in session to pass.

Proposed checker

The following script is an unexecuted example you can copy into a branch and adapt to your paths. It does not call a model, and it does not measure prose quality or factual accuracy of the ticket. It only checks that required tokens from the ticket appear, and that a small denylist is absent. Passing the script still depends on a human reading for implications that the substring search cannot see.

#!/usr/bin/env python3
"""Proposed local check: a release note must restate ticket facts."""

from __future__ import annotations

import sys
import tomllib
from pathlib import Path

DENY = (
    "zero downtime",
    "zero risk",
    "fully backward compatible",
    "no action required",
    "guaranteed",
    "always safe",
)

def main() -> int:
    ticket = tomllib.loads(Path(sys.argv[1]).read_text(encoding="utf-8"))
    note = Path(sys.argv[2]).read_text(encoding="utf-8").lower()
    errors: list[str] = []

    if ticket.get("status") != "human-approved":
        errors.append("ticket status must be human-approved")

    own = ticket["ownership"]
    required = [
        ticket["id"].lower(),
        f"severity: {own['severity']}",
        own["min_version"].lower(),
    ]
    required.extend(item.lower() for item in ticket["facts"]["non_goals"])

    for token in required:
        if token not in note:
            errors.append(f"missing required token: {token}")

    if own["breaking"] and "breaking" not in note:
        errors.append("breaking change must be named in the note")
    if own["data_migration"] == "required" and "migration" not in note:
        errors.append("required migration must be named in the note")

    for phrase in DENY:
        if phrase in note:
            errors.append(f"unapproved commitment phrase: {phrase}")

    if errors:
        print("\n".join(errors))
        return 1
    print("release note matches ticket constraints")
    return 0

if __name__ == "__main__":
    raise SystemExit(main())
Enter fullscreen mode Exit fullscreen mode

The script uses tomllib, which is in the Python standard library starting with Python 3.11, so older images need another parser. Swap in a TOML library only if you keep the same field checks and the same non-zero exits. Do not weaken the denylist while you change the parser, because that would change the gate rather than the packaging.

Run it locally after you create both files, and treat a non-zero exit as a blocked documentation change.

python3 check_release_note.py changes/CHG-1842.toml docs/notes/CHG-1842.md
Enter fullscreen mode Exit fullscreen mode

A passing note must contain the identifier, the severity line, the version, and both non-goal sentences unchanged. It must also contain the word breaking and the word migration when those flags are set as in the sample. A failing note that says fully backward compatible exits non-zero even if the rest of the prose is clear. That behavior is a property of the script above, not a measured result from a hosted model.

Sample note that satisfies the tokens

The sample note below is written to satisfy the checker, and it is not offered as recommended house style. Keep the required tokens on their own lines if your formatter might wrap them into a different string. The orientation sentences may be rewritten later, but the non-goal lines must stay intact after lowercasing. This fixture is proposed example text, not a record of a shipped billing release.

CHG-1842

severity: high

This change is breaking and requires a migration before upgrade.
Minimum version 2.4.0.

does not backfill invoices created before 2.4.0
does not change tax rounding rules

Pinned check: billing export --since 2026-09-01 --dry-run
Enter fullscreen mode Exit fullscreen mode

How to prompt without handing over ownership

Send the model the ticket body and a short instruction that forbids new facts beyond the enums. A compact prompt shape is enough, and you should keep it in the repository next to the ticket. Reviewers can then see the instruction that was used, instead of inferring it from polished prose. Treat the returned Markdown as untrusted input to the checker, same as any other patch from a colleague.

Draft a release note from this ticket only.
Restate severity, min_version, breaking, data_migration, and each non_goal verbatim.
Do not add compatibility, downtime, or support claims beyond the enums.
If a field is unknown, write unknown rather than inferring a safer outcome.
Enter fullscreen mode Exit fullscreen mode

If you use a free server for that call, keep secrets and customer data out of the prompt. A free drafting path does not change your data-handling duties or the requirement for a human approver. It only removes a procurement step for the orientation draft, within limits published outside this article. Do not paste production invoices, access tokens, or customer identifiers into the drafting request at all.

Record the draft boundary in the pull request

A useful review comment records three links: the ticket path, the note path, and the checker command. That comment should state whether a model drafted the orientation text or a person wrote it. It should not include hidden reasoning traces, private prompts with secrets, or a claim that the model verified behavior. The pull request diff remains the evidence, and the comment only points reviewers at the ownership split.

When the note and the ticket disagree, fix the ticket first if the product decision changed, then edit the note. When the note adds color but no new fact, keep the note and do not churn the approved enums. When the note drops a non-goal, treat the drop as a release risk rather than as tighter writing. Shorter prose is not automatically safer prose if it omits a restriction the ticket required readers to see.

A minimal fixture plan

Store the ticket and the note as fixtures so the checker can run in CI without a model present. The note fixture should be boring, because boring text is easier to diff when an enum changes later. The point of the fixture is constraint coverage, not literary quality or a demonstration of model style. Replace the sample module names before you treat the files as real release content for a product.

Expected checker results belong next to the fixtures in the test plan, written as plain outcomes rather than scores.

Fixture Expected exit Reason
Ticket status still draft 1 Unsigned obligations
Note missing a non-goal sentence 1 Dropped restriction
Note contains guaranteed 1 Denylist phrase
Note restates every required token 0 Constraints matched
Note restates tokens but implies no migration Human fail Checker blind spot

The last row is intentional, and it documents a blind spot instead of hiding it behind a green exit. Automated exit code zero is not publication approval, and the test plan should say that beside the command. Reviewers who only look at green CI will miss implied promises that never used a denied phrase. That is why step 6 remains a human read even after the script is wired into the default pipeline.

Limitations and who should skip this

The denylist is short on purpose, and a writer can imply no action required without using those words. Negation, translation, and wide tables can also hide a commitment that the substring search will miss. The checker does not parse semantics, and it will not catch a wrong version that the human typed into the ticket. Human approval of a wrong ticket still produces a wrong note that the checker will happily accept.

Do not use this approach when counsel must author controlled commitments inside the publishing tool itself before release. Do not use it if your team will skip the human approver because the model sounded confident. Do not use it for security advisories, where impact and fixed versions need a vulnerability workflow rather than a release-note renderer. Teams that cannot store an approver identity in git should not pretend the approver field is accountability.

Availability of a free model or a free server can change, so do not encode either one as a required CI dependency. The required CI dependency is the local checker, which runs without network access and without a vendor account. If the drafting service is down, write the orientation paragraphs yourself and still refuse unlisted commitments. This article does not claim a quota, a hardware shape, a latency number, or a permanent free tier.

What to review before you merge

Review the ticket diff first, because that is where severity and migration cost are actually decided. Review the note second, looking only for implications that the checker cannot see in the wording. If both reviews pass, the generated paragraphs can stay, even if a later edit rewrites tone. Tone is disposable across later revisions, while the enums remain the contract that readers will plan against.

If your repository already tracks user-visible changes in pull requests, you can add this ticket format beside those reviews. Use a hosted drafting pass only for orientation text, and keep the named approver on the ticket itself. Treat any smoother sentence that adds a promise as a defect rather than an improvement in clarity.

Top comments (0)