Skip to content

markdownlint hook's repo-local branch drops MD060:false, blocking commits on terraform-docs tables #309

Description

@twistedmelonman

Summary

The markdownlint hook's has_local_config branch drops --config entirely, so
a repo with its own .markdownlint.yaml gets none of the rule disables in
markdownlint-cli/.markdownlint.json — including MD060: false, which is set
there precisely because MD060 rejects the table style terraform-docs emits.
The two branches therefore disagree about the same file: with our global config
it passes, with a repo-local config it fails. --fix does not repair MD060, so
the commit is simply blocked.

Hit this on beacon-biosignals/infra. A one-line README edit failed the hook on
a terraform-docs-generated table 60 lines away from the change, in a file whose
style matches all 78 generated READMEs in that repo. Beacon's CI does not run
markdownlint at all, so nobody else on the team sees it.

This is the same shape as #290: a global hook default that fights a repo's own
convention, with an escape hatch that works but that nobody knows to reach for
until they've lost an hour.

Reproduction

d=$(mktemp -d) && cd "$d"
printf '%s\n' '# T' '' '| A | B |' '|------|------|' '| 1 | 2 |' > t.md
printf '%s\n' '{ "MD013": false }' > .markdownlint.json   # any repo-local config

markdownlint --fix t.md                                    # has_local_config branch
markdownlint --fix --config "$HOME/.config/markdownlint-cli/.markdownlint.json" t.md

The first command fails, the second passes:

t.md:4:8  error MD060/table-column-style [Table pipe does not align with header for style "aligned"]
t.md:4:15 error MD060/table-column-style [Table pipe does not align with header for style "aligned"]
exit=1

exit=0

Same binary (markdownlint-cli 0.49.1 via Homebrew), same file — the only
variable is whether --config is passed. A clean markdown file passes on the
local-config branch (exit=0), so the failure is MD060 specifically and not
the branch being broken in general.

Note that --fix leaves |------| untouched. MD060 blocks the commit without
offering a fix, so unlike the shfmt case in #290 there is no silent rewrite —
just a hard stop with no way through.

Where

pre-commit/config.yaml:124-132

if [[ "$has_local_config" -eq 1 ]]; then
  markdownlint --fix "$@";
else
  markdownlint --fix --config "$HOME/.config/markdownlint-cli/.markdownlint.json" "$@";
fi

The intent (from #116) is right: a repo with its own conventions should not have
our personal config forced onto it. The gap is that "defer to the repo" is
implemented as "use no config but the repo's," which silently also means "drop
every rule we globally decided is wrong" — MD060: false among them
(markdownlint-cli/.markdownlint.json:12).

MD060 is new enough that no repo-local config predating it mentions it. Beacon's
infra/.markdownlint.yaml configures nine rules and MD060 is not one of them,
so it runs at its default and objects to output the repo generates
mechanically.

Blast radius

  • beacon-biosignals/infra — 78 README.md files with the offending table
    style, all 78 terraform-docs-generated. Any commit touching any of them
    fails.
  • beacon-biosignals/platform-datastore — 2 files, local config, no MD060
    entry. Same trap, not yet triggered.
  • smartwatermelon/personify — 1 file, and its local config does set MD060.
    So this has already been rediscovered and worked around once, per-repo.

That last one is the argument for fixing it centrally: the workaround is being
reinvented in individual repos, including a Beacon repo where committing a
personal-tooling accommodation to shared config would be inappropriate.

Options

  1. Always pass our config, and let markdownlint's own discovery layer on
    top.
    Ruled out by test: --config replaces repo-local discovery rather
    than merging with it. With a repo config setting MD040: true and our global
    config setting MD040: false, passing --config makes the file pass — the
    repo's opinion is discarded, not merged. (Control: the same repo config alone
    fails the file, so the rule does fire.) This is also why the current branch
    exists at all, so it is not a candidate fix.
  2. Keep the branch, but pass our global config as the baseline via a
    generated merge.
    Read both files, deep-merge repo-over-global, write to a
    temp config, pass that. Correct semantics ("repo wins, our disables still
    apply") but adds YAML/JSON merging to a bash hook.
  3. Move the genuinely universal disables out of the global config and into
    hook flags
    that apply on both branches. MD060 is the only one that has
    bitten so far. Smallest change; the rule stays off everywhere without
    touching config discovery.
  4. Warn instead of failing when the only violations come from a repo-local
    branch.
    Matches the flake8 reasoning already in this file (lines 45-46:
    linting reports, it does not rewrite, so the worst case is a message). Would
    have let the infra commit through with a note.
  5. Leave as-is and document it. The escape hatch works — add MD060: false
    to the repo's config. But for a Beacon repo that means committing a change
    to shared config to accommodate one person's local hook, which is the wrong
    place for it.

My preference is 3, falling back to 4. MD060's default assumes hand-written
tables; any repo generating markdown from a tool is a candidate, and
terraform-docs is common enough that this will recur.

Happy to send a PR for whichever you prefer.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions