fastapi/fastapi · error · RuntimeError

{release_notes_file} must start with {RELEASE_NOTES_HEADER!r

Error message

{release_notes_file} must start with {RELEASE_NOTES_HEADER!r}

What it means

Format check in scripts/prepare_release.py update_release_notes (:60-67): the release-notes markdown file must begin with the exact two headers '# Release Notes' followed immediately by '## Latest Changes' (RELEASE_NOTES_HEADER + LATEST_CHANGES_HEADER + newline). The first check (:61-63) fails if the file does not start with '# Release Notes' at all; the new release section is spliced right after '## Latest Changes', so a reordered/missing header breaks the splice and the script refuses to edit the file.

Source

Thrown at scripts/prepare_release.py:66

    if bump == "minor":
        return f"{major}.{minor + 1}.0"
    return f"{major}.{minor}.{patch + 1}"


def update_version_file(content: str, version: str, version_file: Path) -> str:
    current_version = get_current_version(content, version_file)
    if parse_version(version) <= parse_version(current_version):
        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:

View on GitHub (pinned to a1fa70d423)

Solutions

  1. Make docs/en/docs/release-notes.md start with exactly '# Release Notes\n\n## Latest Changes\n' (then the existing changelog body).
  2. Remove any content above '# Release Notes' (move badges/notes into the body).
  3. Restore via git: git checkout HEAD -- docs/en/docs/release-notes.md if the edit was accidental (after saving any wanted entries).
  4. Re-run the release command once the header block matches.

Example fix

# docs/en/docs/release-notes.md (before)
<!-- banner -->
# Release Notes
## Latest Changes

# after
# Release Notes

## Latest Changes
Defensive patterns

Strategy: validation

Validate before calling

EXPECTED_HEAD = "# Release Notes\n\n## Latest Changes\n"

def release_notes_wellformed(text: str) -> bool:
    return text.startswith("# Release Notes") and text.startswith(EXPECTED_HEAD)

Prevention

When it happens

Trigger: Running release preparation when docs/en/docs/release-notes.md was edited to remove or reorder the leading headers, when a stray line (badge, blank intro, HTML comment) was added above '# Release Notes', or when the wrong file was passed as release_notes_file.

Common situations: Contributors prepending banners or acknowledgements to the top of release-notes.md; tools that regenerate the file without the header; checking out a branch where the header was refactored.

Related errors


AI-assisted analysis of fastapi/fastapi@a1fa70d423 (2026-08-14). Data as JSON: /api/errors/45159a16068fb438. Report an issue: GitHub.