tiangolo/fastapi · error · RuntimeError

{release_notes_file} must start with {latest_header!r}

Error message

{release_notes_file} must start with {latest_header!r}

What it means

Raised by update_release_notes() in scripts/prepare_release.py:74. After confirming the release-notes header (error 113) and absence of the target version (error 114), the function requires the '## Latest Changes' heading to immediately follow the header (latest_header at scripts/prepare_release.py:72). New version sections are spliced in right after '## Latest Changes', so its presence and position are mandatory.

Source

Thrown at scripts/prepare_release.py:74

        raise RuntimeError(
            f"New version {version} must be greater than current version {current_version}"
        )
    return VERSION_PATTERN.sub(f'__version__ = "{version}"', content, count=1)


def update_release_notes(
    content: str, version: str, release_date: date, release_notes_file: Path
) -> str:
    if not content.startswith(RELEASE_NOTES_HEADER):
        raise RuntimeError(
            f"{release_notes_file} must start with {RELEASE_NOTES_HEADER!r}"
        )
    if re.search(rf"^## {re.escape(version)}(?: \([^)]+\))?$", content, re.M):
        raise RuntimeError(f"Release notes already contain a section for {version}")

    latest_header = f"{RELEASE_NOTES_HEADER}{LATEST_CHANGES_HEADER}\n"
    if not content.startswith(latest_header):
        raise RuntimeError(f"{release_notes_file} must start with {latest_header!r}")

    release_header = f"## {version} ({release_date.isoformat()})"
    return content.replace(
        latest_header,
        f"{RELEASE_NOTES_HEADER}{LATEST_CHANGES_HEADER}\n\n{release_header}\n",
        1,
    )


def get_release_notes_body(content: str, version: str, release_notes_file: Path) -> str:
    version_heading = re.compile(rf"(?m)^## {re.escape(version)}(?: \([^)]+\))?$")
    match = version_heading.search(content)
    if not match:
        raise RuntimeError(
            f"Could not find release notes section for {version} in {release_notes_file}"
        )

    next_match = VERSION_HEADING_PATTERN.search(content, match.end())

View on GitHub (pinned to 3e8d1526d8)

Solutions

  1. Open the release notes file and ensure '## Latest Changes' appears immediately after the '# Release Notes' header.
  2. If prior version sections sit above it, move '## Latest Changes' back to the top (just under the H1).
  3. Re-run prepare.

Example fix

<!-- before -->
# Release Notes

## 1.0.0 (2026-01-01)
- ...

## Latest Changes
<!-- after -->
# Release Notes

## Latest Changes

## 1.0.0 (2026-01-01)
- ...
Defensive patterns

Strategy: validation

Validate before calling

from pathlib import Path

RELEASE_NOTES_HEADER = "---\nhide:\n  - navigation\n---\n\n# Release Notes\n\n"
LATEST_CHANGES_HEADER = "## Latest Changes"

def latest_header_ok(path: Path) -> bool:
    return path.read_text(encoding="utf-8").startswith(
        RELEASE_NOTES_HEADER + LATEST_CHANGES_HEADER + "\n"
    )

Try / catch

try:
    updated = update_release_notes(content, version, d, release_notes_file)
except RuntimeError as e:
    if "Latest Changes" in str(e):
        raise SystemExit(f"Restore '## Latest Changes' under the header: {e}") from e
    raise

Prevention

When it happens

Trigger: Calling prepare when the release-notes file is missing the '## Latest Changes' heading, or it has been moved below existing version sections, or its text was altered.

Common situations: A previous release moved '## Latest Changes' but did not restore it. Someone renamed the heading. The file was hand-edited to put a version section above '## Latest Changes'.

Related errors


AI-assisted analysis of tiangolo/fastapi@3e8d1526d8 (2026-08-11). Data as JSON: /api/errors/5bf0d8a4b1e66e24. Report an issue: GitHub.