Skip to content

fix(ci): check CONTRIBUTING links, not just the README's - #353

Merged
vyncint merged 2 commits into
vyncint:mainfrom
jarik2014:fix/check-links-contributing
Sep 13, 2026
Merged

vyncint merged 2 commits into
vyncint:mainfrom
jarik2014:fix/check-links-contributing

Conversation

@jarik2014

Copy link
Copy Markdown
Contributor

What was broken

.github/scripts/check-readme-links.sh read README.md and nothing else, so
CONTRIBUTING.md — the file a first-time contributor reads, and the one most
likely to accumulate link rot because it points at scripts and workflows that
get renamed — had no link check at all. #351 proved it: a link one letter
short of the repository name sat in CONTRIBUTING.md and all sixteen checks
reported success.

What this changes

The script now takes a file list and gives each file the rule that applies:

  • an in-repo target must exist — for every file read. Relative links are
    resolved against the repository root, which is where both files live.
  • no relative targets — still README.md only. That rule is crates.io's:
    crates/termlens/Cargo.toml sets readme = "../../README.md", so a relative
    link is rewritten against the crate directory. CONTRIBUTING.md is not
    packaged and legitimately links AGENTS.md, docs/RELEASING.md and
    .github/workflows/ci.yml relatively — pointing the old script at it would
    have failed on all of those, which is why #351 did not simply do it.
  • a URL naming no in-repo pathgithub.com/vyncint/<repo> is asserted
    against the four repositories in the org, offline. That is what catches
    temlens. It stays offline on purpose: a link checker that asks GitHub is
    online, flaky, and can report a 200 for the wrong reason, which is the
    failure mode this gate exists to avoid.

Why the two rules differ is now written down in the script header, next to the
reason for each, and the two commands are listed in CONTRIBUTING.md §1 with
the rest of the gates.

tools/link-gate-selftest/run.sh proves the gate can fail before it is trusted
to pass, in the shape tools/semver-gate-selftest/ already uses: five fixtures
that must go red, and two runs that must stay green — the fixture that models
what CONTRIBUTING.md legitimately does, and this repository's own two files.
CI runs it in gates-listed, which is shell-only and single-OS.

Evidence

Done when item by item, from this branch (tools/link-gate-selftest/run.sh):

  ok    CONTRIBUTING.md: relative links, org link, fragment, external exit 0
  ok    CONTRIBUTING.md: a deleted in-repo target fails            exit 1
  ok    CONTRIBUTING.md: a misspelled org repository fails (#351)  exit 1
  ok    README.md: a relative target fails (crates.io rewrites it) exit 1
  ok    README.md: a missing absolute target fails                 exit 1
  ok    this repository's own two files pass                       exit 0
link gate selftest: 6 expectation(s), the gate fails when it should

And the same gate against the real files with each fault introduced by hand
and reverted, which is the deliberate failing run the issue asks for:

$ sed -i 's|/vyncint/termlens/labels|/vyncint/temlens/labels|' CONTRIBUTING.md
$ .github/scripts/check-readme-links.sh README.md CONTRIBUTING.md
README.md: 24 distinct link target(s)
CONTRIBUTING.md: 15 distinct link target(s)
CONTRIBUTING.md LINK: github.com/vyncint/temlens names no repository in the org
$ echo $?
1

$ sed -i 's|](docs/RELEASING.md)|](docs/RELEASING-old.md)|' CONTRIBUTING.md
$ .github/scripts/check-readme-links.sh README.md CONTRIBUTING.md
CONTRIBUTING.md LINK: "docs/RELEASING-old.md" is linked but not in the repository
$ echo $?
1

$ sed -i 's|https://github.com/vyncint/termlens/blob/main/docs/DESIGN.md|docs/DESIGN.md|' README.md
$ .github/scripts/check-readme-links.sh
README LINK: relative target "docs/DESIGN.md" — crates.io rewrites it against
  crates/termlens/, where it does not exist. Use https://github.com/vyncint/termlens/blob/main/docs/DESIGN.md
$ echo $?
1

With the three files restored, the gate and the docs-consistency gate CI already
runs both stay green:

$ .github/scripts/check-readme-links.sh README.md CONTRIBUTING.md
README.md: 24 distinct link target(s)
CONTRIBUTING.md: 15 distinct link target(s)
markdown links: 39 target(s) across 2 file(s), every in-repo target present   # exit 0

$ .github/scripts/check-ci-gates-listed.sh
every CI gate is listed in CONTRIBUTING.md §1 (19 commands, zizmor==1.29.0)   # exit 0

$ .github/scripts/check-dco.sh main..HEAD && .github/scripts/check-no-ai-attribution.sh main..HEAD
check-dco: OK
check-no-ai-attribution: OK

What I did not do

  • No CHANGELOG.md entry: nothing here is user-facing, it is the contributor
    gate itself. Say the word if you want one anyway.
  • The file is still called check-readme-links.sh although it now reads two
    files. A rename touches ci.yml, CONTRIBUTING.md and a CHANGELOG.md
    entry that names the old spelling, so I kept it out of this diff — happy to
    do it in a follow-up if you prefer the honest name.
  • The org list is data in the script (known_repos), not a network call. If a
    fifth repository ever lands, the gate fails on its links until the list grows
    by one word, which seemed the right trade against a flaky external check.

Closes #352

check-readme-links.sh read README.md and nothing else, so a dead link in
CONTRIBUTING.md — the file a first-time contributor reads — passed all
sixteen checks. Two fixtures make the gate fail on the shapes that slipped
through: a misspelled org repository (vyncint#351) and a renamed in-repo target.

The README's absolute-only rule is crates.io's and stays on README.md
alone: CONTRIBUTING is not packaged and legitimately links AGENTS.md,
docs/RELEASING.md and .github/workflows/ci.yml relatively, so pointing the
old script at it would have failed on all three. Both files now get the
rule that generalises — an in-repo target must exist — and a link naming
no in-repo path is checked against the org list, offline.

Signed-off-by: jarik2014 <46443045+jarik2014@users.noreply.github.com>
@jarik2014
jarik2014 requested a review from vyncint as a code owner September 13, 2026 11:03
The allow-list held four names and the comment called them "the four that
share this contributor pattern". Two more projects have joined the pattern
since (oxmera, oxidelake), and `docs/DESIGN.md` already links
`vyncint/termlens-demo` for the coverage study. So a real, already-linked
repository was reported as "names no repository in the org" — a false claim,
and the kind a gate should never make.

It costs nothing today, because the gate reads README.md and CONTRIBUTING.md
and neither links those three. It would cost the next person something: the
file list is a parameter now precisely so the gate can be pointed at more
files, and DESIGN.md is the obvious next one.

The list stays deliberately short rather than becoming every repository the
account holds — a list of everything would wave through the typo this exists
to catch. What changes is that the message no longer claims the repository
does not exist, only that this gate has not been told about it, and says how
to add one. The good fixture links termlens-demo, so the distinction is
pinned rather than argued.

Signed-off-by: Vyncint Ng <vyncint@icloud.com>

@vyncint vyncint left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks — this is an unusually complete piece of work, and it closes #352 on every point.

I checked each Done-when item against your branch rather than taking the evidence in the description on trust:

  • A misspelled vyncint/temlens link in CONTRIBUTING.md fails, and the selftest pins it.
  • A renamed in-repo target linked from CONTRIBUTING.md fails.
  • The absolute-only rule still applies to README.md alone, and CONTRIBUTING's six legitimate relative links still pass.
  • The reason the two rules differ is in the script header, where the next person editing either half will actually read it.

The selftest is the part I liked most. Proving the gate can go red before trusting it to be green is what tools/semver-gate-selftest/ established, and following that shape without being asked is the right instinct. Running it in gates-listed is the right home for it too, since it is shell-only and single-OS.

One thing I pushed a commit for

known_repos held four names and the comment called them "the four that share this contributor pattern". That was true when the pattern was written down and is not any more: oxmera and oxidelake have since joined it, and docs/DESIGN.md already links vyncint/termlens-demo for the coverage study. So the gate reported a real, already-linked repository as names no repository in the org.

It costs nothing today, because the gate reads two files and none of those three appear in them. It would have cost the next person, though, and precisely because of what you built: the file list is a parameter now, so pointing the gate at docs/DESIGN.md is the obvious next step, and it would have failed on a correct link with a message asserting something false.

What I changed:

  • The list gains oxmera, oxidelake and termlens-demo. It stays deliberately short rather than becoming every repository the account holds, for your reason: a list of everything waves through the typo the gate exists to catch.
  • The message no longer claims the repository does not exist. It says the gate has not been told about it, and how to add one. A gate should not assert a falsehood while failing.
  • The good fixture links termlens-demo, so the distinction between "typo" and "real but unlisted" is pinned instead of argued.

Your temlens fixture still fails, which is the thing that matters.

Two notes on what you said you did not do, and I agree with both. No CHANGELOG entry is right, since nothing here is user-facing. And leaving the rename for a follow-up is right too, since it touches ci.yml, CONTRIBUTING.md and a CHANGELOG entry naming the old spelling. If you want to do that follow-up, I will take it.

@vyncint
vyncint merged commit 96cefdd into vyncint:main Sep 13, 2026
16 checks passed
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.

check-readme-links only reads README.md, so a broken link in CONTRIBUTING.md passes every gate

2 participants