vectordotdev/vector · error

first line must be an H1 title (`# ...`)

Error message

first line must be an H1 title (`# ...`)

What it means

Parse error from parse_breaking_sections: the breaking-change fragment body must begin with an H1 markdown title ('# ...'), and this bail fires when the first section marker extracted from the (CRLF-normalized, padded) body is not an H1 heading — i.e. the file starts with body text, a smaller heading, or omits the title entirely. The H1 becomes the release-note entry title.

Solutions

  1. Add '# <title>' as the first line of the breaking fragment
  2. Move any intro text below the H1 title
  3. Ensure no leading blank lines or front matter precede the H1
Defensive patterns

Strategy: validation

When it happens

Trigger: Thrown at vdev/src/commands/changelog/mod.rs:124 when the library encounters an invalid state.

Common situations: See trigger scenarios.


AI-assisted analysis of vectordotdev/vector@bdb87aeaa4 (2026-09-16). Data as JSON: /api/errors/dd25df201a78f37d. Report an issue: GitHub.

Appendix: source

Thrown at vdev/src/commands/changelog/mod.rs:124

/// Parse a breaking-fragment body (H1 title + `## Summary` + `## Migration`) via plain
/// string splits. Only the H1 title, the `## Summary` header, and the `## Migration` header
/// are mandatory; content inside each section is returned verbatim (any sub-headings the
/// author wrote — `### Old`, `### New`, whatever — pass through untouched).
pub(crate) fn parse_breaking_sections(body: &str) -> Result<BreakingSections> {
    // Normalize CRLF → LF so Windows checkouts parse identically. Also pad the input so
    // section markers like `\n## Migration\n` still match when the last section is empty
    // and the file ends right after the header line.
    let mut normalized = body.replace("\r\n", "\n");
    if !normalized.ends_with('\n') {
        normalized.push('\n');
    }
    let body: &str = &normalized;

    // 1) File must begin with an H1 title on the first line — no leading blank lines.
    let title_body = body
        .strip_prefix("# ")
        .ok_or_else(|| anyhow::anyhow!("first line must be an H1 title (`# ...`)"))?;
    let (title_line, after_title) = title_body.split_once('\n').unwrap_or((title_body, ""));
    let (title_text, anchor) = split_title_and_anchor(title_line);
    let title = title_text.to_string();
    if title.is_empty() {
        bail!("H1 title must not be empty");
    }

    // 2) Exactly one `## Summary` and one `## Migration`, in that order.
    if after_title.matches(SUMMARY_MARKER).count() != 1 {
        bail!("exactly one `## Summary` section is required");
    }
    if after_title.matches(MIGRATION_MARKER).count() != 1 {
        bail!("exactly one `## Migration` section is required");
    }
    let s_pos = after_title.find(SUMMARY_MARKER).unwrap();
    let m_pos = after_title.find(MIGRATION_MARKER).unwrap();
    if s_pos >= m_pos {
        bail!("`## Summary` must come before `## Migration`");

View on GitHub (pinned to bdb87aeaa4)