You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
A >>>>>>> theirs conflict marker reached main in CHANGELOG.md and was served to readers at /docs/changelog until #1881 removed it. Nothing in the repo would have caught it, and nothing would catch the next one.
What happened
9da64a10 (the #1839 stack tip) added the runtime_command changelog block from a botched resolution: the resolver kept theirs wholesale, dropped the <<<<<<< and ======= lines, and left the trailing >>>>>>> theirs. No content was lost, so nothing read as broken in the diff.
CHANGELOG.md is a published docs page — Fountain.Docs declares it on extra_resources: (apps/fountain/lib/fountain/docs.ex:48), docs/changelog.md snippet-includes it, and docs/nav.yml:150 mounts it. >>>>>>> is seven nested blockquotes in markdown, so it rendered in production as an indented box holding the word "theirs" in the middle of the unreleased notes:
Full CI was green on that merge, and full CI is green on #1881. Neither run had anything to say about it.
Why no gate saw it
Two independent holes, and the marker fell through both.
There is no conflict-marker check anywhere. A grep for the three marker forms over .ex, .exs, .sh, .yml, .py and .mjs finds no test, no script and no CI step that looks for them. This is a class gap, not a changelog one: a marker in an Elixir source file fails to compile, but one in markdown, JSON fixtures, .env.example, or a docs page under apps/*/docs/ is invisible to every gate we have.
CHANGELOG.md sits in a seam of the docs gates. The changes job keeps it off the docs-only allowlist deliberately (ci.yml:115) so the release-bump flow cannot skip the suite. That is the right call. The side effect is that docs_touched stays false for a changelog-only diff, so Docs and Docs prose gates both skip — as they did on #1881. And the prose linters take docs/ and apps/*/docs/ as their path arguments, not the repo root. So the one file that is both a published docs page and prose-gate-exempt is exactly where this landed.
What to build
A tree-wide marker check, not a changelog one. Two candidate homes:
A scripts/ gate beside coverage-gate.exs and hex-audit-gate.exs, run from the elixir-static job or CI policy tests, grepping the tracked tree. Catches every file type, including ones no Elixir test reads.
docs_test.exs, which already walks the manual and the declared external_resources/0. Narrower — it would have caught this instance, since CHANGELOG.md is a declared resource — but it leaves fixtures and config files uncovered.
(1) is the one that closes the class. Worth considering whether it also belongs in mix precommit, so the marker dies before the push rather than in CI.
Two things for whoever picks it up:
The backlog is empty. The tree is clean today, so the guard can go in with no allowlist and no ratchet file — the shape the docs gates had to grow into over Site-wide style pass, plus a CI style check #911 and the destink work.
Watch the self-match. The check's own source, and any doc that explains git conflict resolution, will contain the patterns it searches for. Anchor to line start and exclude the gate's own path rather than adding a general allowlist, so the list cannot start growing.
A
>>>>>>> theirsconflict marker reachedmaininCHANGELOG.mdand was served to readers at/docs/changeloguntil #1881 removed it. Nothing in the repo would have caught it, and nothing would catch the next one.What happened
9da64a10(the #1839 stack tip) added theruntime_commandchangelog block from a botched resolution: the resolver kept theirs wholesale, dropped the<<<<<<<and=======lines, and left the trailing>>>>>>> theirs. No content was lost, so nothing read as broken in the diff.CHANGELOG.mdis a published docs page —Fountain.Docsdeclares it onextra_resources:(apps/fountain/lib/fountain/docs.ex:48),docs/changelog.mdsnippet-includes it, anddocs/nav.yml:150mounts it.>>>>>>>is seven nested blockquotes in markdown, so it rendered in production as an indented box holding the word "theirs" in the middle of the unreleased notes:Full CI was green on that merge, and full CI is green on #1881. Neither run had anything to say about it.
Why no gate saw it
Two independent holes, and the marker fell through both.
There is no conflict-marker check anywhere. A grep for the three marker forms over
.ex,.exs,.sh,.yml,.pyand.mjsfinds no test, no script and no CI step that looks for them. This is a class gap, not a changelog one: a marker in an Elixir source file fails to compile, but one in markdown, JSON fixtures,.env.example, or a docs page underapps/*/docs/is invisible to every gate we have.CHANGELOG.mdsits in a seam of the docs gates. Thechangesjob keeps it off the docs-only allowlist deliberately (ci.yml:115) so the release-bump flow cannot skip the suite. That is the right call. The side effect is thatdocs_touchedstays false for a changelog-only diff, so Docs and Docs prose gates both skip — as they did on #1881. And the prose linters takedocs/andapps/*/docs/as their path arguments, not the repo root. So the one file that is both a published docs page and prose-gate-exempt is exactly where this landed.What to build
A tree-wide marker check, not a changelog one. Two candidate homes:
scripts/gate besidecoverage-gate.exsandhex-audit-gate.exs, run from theelixir-staticjob orCI policy tests, grepping the tracked tree. Catches every file type, including ones no Elixir test reads.docs_test.exs, which already walks the manual and the declaredexternal_resources/0. Narrower — it would have caught this instance, sinceCHANGELOG.mdis a declared resource — but it leaves fixtures and config files uncovered.(1) is the one that closes the class. Worth considering whether it also belongs in
mix precommit, so the marker dies before the push rather than in CI.Two things for whoever picks it up:
Found while reviewing #1881.