{"record":{"id":"ceb72cfc02b4f439","repo":"vectordotdev/vector","slug":"invalid-breaking-fragment-filename-title-must-not-be-empty","errorCode":null,"errorMessage":"invalid breaking fragment '{filename}': title must not be empty.","messagePattern":"invalid breaking fragment '(.+?)': title must not be empty\\.","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"vdev/src/commands/check/changelog_fragments.rs","lineNumber":267,"sourceCode":"///\n/// ## Migration\n///\n/// <markdown — \"Action needed\" body for the upgrade guide, or \"N/A\">\n///\n/// authors: <name> [<name> ...]\n/// ```\nfn validate_breaking_fragment(content: &str, filename: &str) -> Result<()> {\n    // Strip the trailing `authors:` line before parsing — it's not part of the markdown\n    // structure and would otherwise land inside the Migration section.\n    let body_before_authors = content\n        .rsplit_once(\"\\nauthors: \")\n        .map_or(content, |(before, _)| before);\n\n    let sections = parse_breaking_sections(body_before_authors)\n        .map_err(|e| anyhow::anyhow!(\"invalid breaking fragment '{filename}': {e}\"))?;\n\n    if sections.title.is_empty() {\n        bail!(\"invalid breaking fragment '{filename}': title must not be empty.\");\n    }\n    if sections.title.contains(\"TODO\") {\n        bail!(\n            \"invalid breaking fragment '{filename}': title still contains the scaffolder placeholder 'TODO'.\"\n        );\n    }\n    if let Some(anchor) = sections.anchor.as_deref()\n        && !is_valid_anchor(anchor)\n    {\n        bail!(\n            \"invalid breaking fragment '{filename}': anchor must be a lowercase kebab-case slug (a-z, 0-9, hyphens); got '{anchor}'.\"\n        );\n    }\n\n    Ok(())\n}\n\n#[cfg(test)]","sourceCodeStart":249,"sourceCodeEnd":285,"githubUrl":"https://github.com/vectordotdev/vector/blob/bdb87aeaa4c4ff27c0ba643c1c77b21bf2ef4013/vdev/src/commands/check/changelog_fragments.rs#L249-L285","documentation":"`vdev check changelog-fragments` rejects a breaking-change changelog fragment whose title section is empty. The fragment is parsed by `parse_breaking_sections` and the check bails with an `anyhow` error naming the offending file. This enforces the rule that every breaking fragment must have a meaningful title.","triggerScenarios":"Running the changelog fragment check with a `breaking/` fragment file whose title line/section is empty or missing, so `sections.title.is_empty()` is true.","commonSituations":"A contributor created a fragment from the scaffold but deleted the title text without filling it in; an automated file rename left a stub file; an editor stripped the heading content.","solutions":["Add a non-empty title to the fragment file (the section parsed as the title, e.g. the top heading).","Verify the fragment follows the breaking-fragment template in changelog.d/README.md.","Re-run the check: `make check-changelog-fragments`."],"exampleFix":"# before (breaking/1234_fix.md)\n## Breaking change\n\n\n# after\n## Breaking change\n### \"`kafka` sink\": `librdkafka` bumped to X, config option `foo` removed","handlingStrategy":"validation","validationCode":"let body = std::fs::read_to_string(path)?;\nlet title = body.lines().find(|l| l.starts_with(\"### \")).map(|l| l[4..].trim());\nif title.map_or(true, |t| t.is_empty()) { return Err(format!(\"{path:?}: empty breaking-change title\")); }","typeGuard":"fn has_title(content: &str) -> bool {\n    content.lines().any(|l| l.starts_with(\"### \") && !l[4..].trim().is_empty())\n}","tryCatchPattern":null,"preventionTips":["Never commit scaffolded fragments without filling the title.","Lint fragments locally with `make check-changelog-fragments` before pushing.","Add a pre-commit hook running the fragment check."],"tags":["changelog","validation","rust","ci"],"backgroundTag":"empty-required-field","analyzedSha":"bdb87aeaa4c4ff27c0ba643c1c77b21bf2ef4013","analyzedAt":"2026-09-16T02:53:35.741Z","contentChangedAt":"2026-09-16T02:53:35.741Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}