vectordotdev/vector · error

duplicate upgrade-guide anchor '#

Error message

duplicate upgrade-guide anchor '#{anchor}' shared by breaking fragments '{other}' and '{name}'. Override one with an explicit `{{#unique-slug}}`.

What it means

The changelog check found two breaking-change fragments whose upgrade-guide headings derive the same Markdown anchor slug. Ambiguous anchors break deep links in the rendered upgrade guide. Fix by renaming one heading or adding an explicit `{{#unique-slug}}` override after the title.

Solutions

  1. Rename the heading in one of the two conflicting breaking fragments so the derived anchors differ.
  2. Add an explicit anchor override `{{#some-unique-slug}}` after the title of one fragment's heading.
  3. If the duplicate came from a merge/rebase, consolidate the two fragments into a single breaking fragment.

Example fix

// before: two fragments both titled
## Remove deprecated config option
// after
## Remove deprecated config option {{#remove-deprecated-config-option-sinks}}
Defensive patterns

Strategy: validation

Validate before calling

let anchors: Vec<String> = std::fs::read_dir("changelog.d")?.filter_map(|e| e.ok())
    .filter(|e| e.path().extension().map_or(false, |x| x == "md"))
    .filter_map(|e| std::fs::read_to_string(e.path()).ok())
    .filter_map(|c| c.lines().find(|l| l.starts_with("## ")).map(|h| slugify(h)))
    .collect();
let dupes: Vec<_> = anchors.iter().duplicates().collect();
assert!(dupes.is_empty(), "duplicate upgrade-guide anchors: {dupes:?}");

Prevention

When it happens

Trigger: Running `vdev check changelog-fragments` (via exec) when two `*.breaking.md` fragments contain headings that normalize to the identical kebab-case anchor, and neither has an explicit `{{#slug}}` override.

Common situations: Two contributors independently add breaking-change fragments with identical titles like 'Remove deprecated option' before merging; rebasing multiple PRs each adding the same section title into the same release's changelog.d directory.

Understand the failure class

Background: "invalid id" errors: invalid identifier format — why libraries reject IDs before lookup, and how to fix them — this error's family across 37 libraries.

Related errors


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

Appendix: source

Thrown at vdev/src/commands/check/changelog_fragments.rs:137

            continue;
        };
        if !entry.breaking {
            continue;
        }
        let content = std::fs::read_to_string(&path)?;
        let body_before_authors = content
            .rsplit_once("\nauthors: ")
            .map_or(content.as_str(), |(before, _)| before);
        let sections = parse_breaking_sections(body_before_authors)
            .map_err(|e| anyhow::anyhow!("invalid breaking fragment '{name}': {e}"))?;
        let anchor = sections.anchor.unwrap_or_else(|| slugify(&sections.title));
        if !is_valid_anchor(&anchor) {
            bail!(
                "invalid breaking fragment '{name}': derived anchor '{anchor}' is not a valid kebab-case slug (add an explicit `{{#some-slug}}` after the title)."
            );
        }
        if let Some(other) = seen.insert(anchor.clone(), name.clone()) {
            bail!(
                "duplicate upgrade-guide anchor '#{anchor}' shared by breaking fragments '{other}' and '{name}'. Override one with an explicit `{{#unique-slug}}`."
            );
        }
    }
    Ok(())
}

/// `git diff --name-only --diff-filter=<filter> --merge-base <merge_base> changelog.d`
///
/// `filter` is a `git diff` `--diff-filter` value: `A` for added-only, `M` for
/// modified-only, `AM` for both, etc.
fn diff_fragments(merge_base: &str, filter: &str) -> Result<Vec<PathBuf>> {
    let filter_arg = format!("--diff-filter={filter}");
    let out = git::run_and_check_output(&[
        "diff",
        "--name-only",
        &filter_arg,
        "--merge-base",

View on GitHub (pinned to bdb87aeaa4)