Skip to content

ci: format Markdown with mdformat instead of Prettier - #55

Merged
atdr merged 2 commits into
mainfrom
ci/replace-prettier-with-mdformat
Sep 9, 2026
Merged

ci: format Markdown with mdformat instead of Prettier#55
atdr merged 2 commits into
mainfrom
ci/replace-prettier-with-mdformat

Conversation

@atdr

@atdr atdr commented Sep 9, 2026

Copy link
Copy Markdown
Owner

What changed

Prettier is gone. mdformat formats Markdown instead, markdownlint-cli2 stays exactly as it was, and setup-node leaves ci.yml entirely.

Why

npx prettier@3.9.6 --check refetched Prettier from the npm registry on every run, and that fetch intermittently sat silent for exactly 301 seconds — npm's default fetch-timeout expiring, the retry then succeeding — in a job that takes twelve seconds when it is clean. atdr/contrail-gh#8 measured three stalls in six runs. The ten-minute timeout-minutes bound cannot catch it and never could: a run that recovers at 301s is under the bound by design.

Prettier also carried the last hand-edited version pin in CI, because it has no first-party action. mdformat is a Python tool, so the pin moves into pyproject.toml's dev extra where the pip Dependabot ecosystem already bumps everything else.

Refs atdr/contrail-gh#8. The matching change in the template is atdr/contrail-gh#16, and the two should land close together — docs/contrail-gh.md says these config files are copies, so a gap means the two repos disagree about what correct Markdown is. Land this one first; that PR's message says it matches this.

Why markdownlint-cli2 stays

Its action is JavaScript, so GitHub runs it on the runner's own Node with no toolchain to install and nothing fetched from a registry — it already sat before setup-node, which is the proof. Replacing it too would mean pymarkdownlnt, which has no per-file rule configuration, and two files here depend on a markdownlint-configure-file comment: README.md (MD013 at 100) and .github/pull_request_template.md (MD041 off, because it opens with a comment by design).

"No Node in CI" was never achievable anyway — actions/checkout and actions/setup-python are themselves JavaScript actions. "No Node toolchain to install, pin and fetch" is, and that is what this does.

Changes

Tooling (4aec3ff)

  • ci.yml docs job: setup-node + npx prettier@3.9.6 out; setup-python at 3.13, pip install -e ".[dev]" and mdformat --check . in
  • .prettierrc.json and .prettierignore deleted; .mdformat.toml added as their successor
  • .pre-commit-config.yaml: rbubley/mirrors-prettierhukkin/mdformat (the official Prettier mirror is archived, which is why we were on a fork)
  • pyproject.toml: mdformat, mdformat-gfm and mdformat-frontmatter in the dev extra
  • AGENTS.md, .markdownlint-cli2.yaml, docs/contrail-gh.md updated to match

Mechanical pass (8f800fa) — 5 files, 17 insertions, 6 deletions, no prose changed. Kept separate so the first commit stays reviewable, same shape as #23.

Two things worth a reviewer's attention

Python 3.13 for the formatter only. .mdformat.toml's exclude key hard-errors below 3.13, so ci.yml's docs job and the pre-commit hook both pin it, and the dev venv moves with them. requires-python is untouched and the matrix still tests 3.11, 3.12 and 3.13 — the interpreter that formats Markdown has nothing to do with the one a user installs contrails on. The alternative was writing the same three exclusions twice in two syntaxes, a shell filter and a pre-commit regex, which is the hazard .pre-commit-config.yaml already warns about, bought for nothing but the ability to format on an interpreter nobody here runs.

mdformat-frontmatter is a guard, not a fix. Without it mdformat does not recognise YAML frontmatter at all and rewrites the block into a horizontal rule and a heading. It did exactly that to a committed skill file in contrail-gh, caught by reading the diff. Nothing tracked here carries frontmatter today, but the failure is silent and the first .md that needs it would not announce itself.

.mdformat.toml also has to name venv/, which .prettierignore did not: Prettier and markdownlint-cli2 both read .gitignore, and mdformat does not. Without it a full-tree run walks into site-packages and finds hundreds of other projects' READMEs.

Checks

  • ./venv/bin/ruff check . and ./venv/bin/ruff format . pass
  • ./venv/bin/pytest -q passes
  • Docs updated in this PR, if this changes a config option, an importer, or an emissions provider
  • Tests added or updated, if this changes behaviour — n/a, no behaviour changed
  • Skimmed gh issue list for open issues this change touches — none of the six are affected

Test plan

Run locally on Python 3.13, before and after the mechanical pass:

  • markdownlint-cli2 v0.23.2 reports 0 issues in 13 files against mdformat's output. This is the one that matters — it proves the two tools agree rather than fight. Baseline on the Prettier-formatted tree was also 0
  • mdformat --check . exits 0, and re-running changes nothing, proving idempotence
  • pre-commit run --all-files passes every hook, with the mdformat hook env reporting Python 3.13.15
  • pytest -q — 319 passed
  • No table in the repo moved. mdformat-gfm pads cells byte-identically to Prettier
  • The two release-please version pins in README.md are untouched by the formatting pass
  • CLAUDE.md is still a symlink afterwards, not replaced by a regular file
  • Negative tests, matching ci: check Markdown with markdownlint-cli2 and Prettier #23's: a deliberate 116-character line makes markdownlint exit 1, and an unpadded table row makes mdformat exit 1. Neither passes by failing to look
  • CI green on this PR — every check passes. The markdown job ran in 18s, and all three pytest legs (3.11, 3.12, 3.13) still pass, confirming the dev-tooling Python bump did not touch the supported range

🤖 Generated with Claude Code

https://claude.ai/code/session_011mJ5NaA5DdPxqa1Mb4UJhT

atdr and others added 2 commits September 10, 2026 00:05
`npx prettier@3.9.6 --check` fetched Prettier from the npm registry on every
run, and that fetch intermittently sat silent for exactly 301 seconds — npm's
default fetch-timeout expiring and the retry then succeeding — in a job that
takes twelve seconds when it is clean. The ten-minute bound cannot catch it: a
run that recovers at 301s is under the bound and always will be.

Prettier also carried the last hand-edited version pin in CI, because it has no
first-party action. mdformat is a Python tool, so its pin moves to
pyproject.toml's dev extra where the pip Dependabot ecosystem already bumps
everything else, and setup-node leaves the workflow.

markdownlint-cli2 stays. Its action is JavaScript, so GitHub runs it on the
runner's own Node with no toolchain to install and nothing to fetch — it
already sat before setup-node, which is the proof. Replacing it too would mean
giving up per-file rule config, and README.md and pull_request_template.md both
depend on a markdownlint-configure-file comment.

mdformat needs two extension plugins to match what Prettier did unasked.
mdformat-gfm pads table cells, byte-identically, so no table in the repo moves.
mdformat-frontmatter keeps a YAML block at the top of a file intact; without it
mdformat does not recognise frontmatter at all and rewrites the block into a
horizontal rule and a heading. It did exactly that to a skill file in
contrail-gh. Nothing tracked here carries frontmatter today, so that plugin is
a guard rather than a fix — but the failure is silent, and the first .md that
needs it would not announce itself.

`number = true` is load-bearing too: without it every ordered list is
renumbered to `1.`.

.mdformat.toml succeeds .prettierignore as the one ignore list. Its `exclude`
key errors below Python 3.13, so both call sites pin 3.13 and the dev venv
moves with them; `requires-python` is untouched and the matrix still tests
3.11. Writing those entries twice instead, as a shell filter and a pre-commit
regex, would buy only the ability to format Markdown on an interpreter nobody
here runs. It also has to name `venv/`, which .prettierignore did not: Prettier
and markdownlint-cli2 read .gitignore, and mdformat does not.

One line in AGENTS.md hard-wrapped an inline code span across a line break.
mdformat cannot break inside a span, so it joined the paragraph into 150
characters and MD013 failed. Shortened the span and rewrapped.

The mechanical reformat is the next commit, so this one stays reviewable.

Refs atdr/contrail-gh#8

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015sxtf4vfR3TyVrQXdixJXq
Mechanical, kept out of the previous commit so that one stays reviewable. No
prose changed.

Every table is byte-identical: mdformat-gfm pads cells exactly as Prettier did.
What moved is smaller and in four shapes.

Blank lines between the items of an already-loose list, in AGENTS.md and
docs/resync.md. CommonMark makes a list loose if any item is, and mdformat
renders that consistently rather than leaving some items separated and some
not. Renders the same either way.

A blank line between two adjacent HTML comments, in README.md and
pull_request_template.md. Both files keep their markdownlint-configure-file
comment working; markdownlint reports 0 issues in 13 files.

Two-space continuation indent on a task-list item in pull_request_template.md,
where Prettier aligned under the text after `- [ ] `.

Reference link labels lowercased and sorted in CODE_OF_CONDUCT.md, which is
vendored Contributor Covenant text. mdformat normalises them and offers no way
to opt out. Link targets are unchanged.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011mJ5NaA5DdPxqa1Mb4UJhT
@atdr
atdr merged commit 092d0b1 into main Sep 9, 2026
11 checks passed
@atdr
atdr deleted the ci/replace-prettier-with-mdformat branch September 9, 2026 23:41
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant