cross-rs/cross · error

changelog entry " " without header

Error message

changelog entry "{line}" without header

What it means

read_changelog parses the changelog line by line. A '- ' entry line is only accepted after a '### <section>' header has established the current ChangelogType; if an entry appears before any header (or after the parser lost track), it bails with this error. It enforces that every entry belongs to a declared section.

Solutions

  1. Move the '- ' entry lines under the appropriate '### Added'/'### Fixed'/etc. header
  2. Re-add the missing '### <section>' header above the orphaned entries
  3. Regenerate the changelog with the xtask tooling instead of hand-editing

Example fix

// before
## [Unreleased] - ReleaseDate
- my change
// after
## [Unreleased] - ReleaseDate
### Added
- my change
Defensive patterns

Strategy: validation

Validate before calling

let mut saw_header = false;
for line in text.lines() {
    if line.starts_with("### ") { saw_header = true; }
    if line.starts_with("- ") && !saw_header {
        panic!("entry before any section header: {line}");
    }
}

Try / catch

match read_changelog(dir) {
    Ok(changes) => changes,
    Err(e) => { eprintln!("changelog format error, fix headers before entries: {e}"); std::process::exit(1); }
}

Prevention

When it happens

Trigger: CHANGELOG.md has '- some entry' lines at the top of the file or directly after '## [Unreleased]' without a preceding '### Section' header; or an earlier invalid header line aborted kind assignment (note from_header's ? already propagates that).

Common situations: Hand-edited changelog where a contributor appends bullet points above the section headers, merges conflict resolution dropped a '### Fixed' line, or a template copied without its headers.

Understand the failure class

Background: Schema validation failed / invalid input schema: payload rejected because its shape doesn't match the expected schema — this error's family across 28 libraries.

Related errors


AI-assisted analysis of cross-rs/cross@8c1a8aa4b6 (2026-09-13). Data as JSON: /api/errors/467d77a1bcde4772. Report an issue: GitHub.

Appendix: source

Thrown at xtask/src/changelog.rs:419

        .ok_or(eyre::eyre!("could not find the next release section"))?;
    let (section, footer) = rest.split_at(last_index);

    // the unreleased should have the format:
    //  ## [Unreleased] - ReleaseDate
    //
    //  ### Added
    //
    //  - #905 - ...
    let mut kind = None;
    let mut changes = Changes::default();
    for line in section {
        let line = line.trim();
        if let Some(header) = line.strip_prefix("### ") {
            kind = Some(ChangelogType::from_header(header)?);
        } else if let Some(entry) = line.strip_prefix("- ") {
            match kind {
                Some(kind) => changes.push(ChangelogEntry::parse(entry, kind)?),
                None => eyre::bail!("changelog entry \"{line}\" without header"),
            }
        } else if !(line.is_empty() || line == "## [Unreleased] - ReleaseDate") {
            eyre::bail!("invalid changelog entry, got \"{line}\"");
        }
    }

    Ok((header.join("\n"), changes, footer.join("\n")))
}

fn delete_changes(root: &Path) -> cross::Result<()> {
    // move all files to the denoted version release
    for entry in fs::read_dir(root.join(".changes"))? {
        let entry = entry?;
        let file_type = entry.file_type()?;
        let srcpath = entry.path();
        let ext = srcpath.extension();
        if file_type.is_file() && ext.is_some_and(|v| v == "json") {
            fs::remove_file(srcpath)?;

View on GitHub (pinned to 8c1a8aa4b6)