Repository navigation
Conversation
clawgenti
left a comment
There was a problem hiding this comment.
Solid design (default-off flag, shadow mode, unchecked bucket) with thorough tests, but the macOS CI leg is red and the new tests never run in CI.
- must-fix
scripts/link-health-scanner.sh:157— the empty-array expansion"${CONFIG_ARGS[@]}"underset -uis fatal on the macOS runner's bash 3.2, so the scanner dies for any repo without.lychee.toml;test-link-health-dryrun-baseline.shfails on macOS (ubuntu's bash 5 passes). - must-fix
tests/test-link-health-unchecked.sh:1— this test andtests/test-link-check.pyare missing from.github/workflows/tests.yml's explicit list (and the shellcheck step), so they silently never run in CI. - suggestion
tests/test-link-check.py:412— the optional lychee-parity test fails on any host with lychee installed (reference-definition URLs). - suggestion
scripts/link-check.py:326— with a proxy configured, any connection reset is classified "unchecked", including origin-side resets after the CONNECT tunnel is up. - nit
scripts/link-check.py:543—--exclude-all-privatefilters the original host only; redirect targets are not re-checked.
Reviewed by clawgenti using the github-pr-review skill
| @@ -0,0 +1,182 @@ | |||
| #!/usr/bin/env bash | |||
There was a problem hiding this comment.
must-fix — this test (and tests/test-link-check.py) is not in .github/workflows/tests.yml's explicit test list, nor in the shellcheck step's list. The workflow's own comment warns: "A new test file must be added here (and to the relevant shellcheck step) or it silently never runs in CI." Right now neither new test executes on any CI leg. Notably, had this one run on macOS it would have failed non-vacuously and caught the bash 3.2 regression: case 1 asserts unchecked_count == 1 in a latest.json that the crashing scanner never writes.
| # We only require our extractor URLs ⊆ lychee's for the shared formats; | ||
| # lychee may also find mailto:/etc. that we skip by scheme later. | ||
| missing = ours - lychee_urls | ||
| self.assertFalse(missing, f"URLs we extract that lychee did not: {missing}") |
There was a problem hiding this comment.
suggestion — test_optional_lychee_extraction_compare fails on any host with lychee installed (verified locally with lychee 0.24.2): lychee does not extract unused reference-definition URLs like [ref]: https://extract.example/reference, while MD_REF_DEF_RE in link-check.py does, so missing is non-empty and the assertion trips. The test skips in CI (no lychee there), so it only bites shadow-mode hosts — exactly where the parity signal matters. Either drop reference definitions from the extractor for parity, exclude them from this comparison, or document the divergence as intentional.
| if url_excluded(url, exclude_res): | ||
| continue | ||
| host = urllib.parse.urlparse(url).hostname or "" | ||
| if exclude_private: |
There was a problem hiding this comment.
nit — --exclude-all-private filters on the original URL's hostname only; urllib then follows redirects without re-validating, so a public URL that 302s to a private/loopback address is still fetched. Worth confirming whether lychee re-checks redirect targets against exclude_all_private and matching that behavior before LINK_CHECKER=python becomes the default.
|
Addressed the review in 7ad4a53.
Locally: Assisted-By: Claude Code |
clawgenti
left a comment
There was a problem hiding this comment.
All five prior findings are addressed and verified (bash 3.2-safe CONFIG_ARGS expansion, both new tests wired into CI, egress refusal now requires tunnel-failure text, redirect targets re-checked for private hosts, unused reference definitions dropped); CI is green on both legs and the full suite — including the lychee parity test, which runs on this host with lychee 0.24.2 — passes locally.
Findings:
- suggestion
scripts/link-check.py:147— 4-space-indented lines are always treated as code and blanked, but in CommonMark they are usually nested list items or lazy continuations whose URLs lychee extracts (details inline).
Reviewed by clawgenti using the github-pr-review skill
7ad4a53 to
d8cca92
Compare
clawgenti
left a comment
There was a problem hiding this comment.
Follow-up on 7ad4a53: the rebase wires both new test files into the shellcheck and test-suite lists and runs tests/test-link-check.py in CI; both legs are green and DCO is signed. Two extraction-heuristic gaps remain (one carried over, one new) plus a forward-compat note on the test loader.
Findings:
- suggestion
scripts/link-check.py:147— 4-space-indented lines are always blanked as code, but in CommonMark they are usually nested list items whose URLs lychee does extract (carried over from last round; verified against lychee 0.24.2). - suggestion
scripts/link-check.py:101—BARE_URL_RE's lookbehind drops any URL starting immediately after(, so prose like "(https://example.com)" is never checked (lychee extracts it; verified). - nit
tests/test-link-check.py:29—exec_modulewithoutsys.modulesregistration is the pattern that just broke #124's ubuntu leg on Python 3.14; worth fixing here too.
Reviewed by clawgenti using the github-pr-review skill
| spec = importlib.util.spec_from_file_location("link_check_mod", CHECKER) | ||
| mod = importlib.util.module_from_spec(spec) | ||
| assert spec.loader is not None | ||
| spec.loader.exec_module(mod) |
There was a problem hiding this comment.
nit — exec_module on a module that was never registered in sys.modules is the exact pattern that broke #124's ubuntu leg on Python 3.14 (sys.modules.get(cls.__module__) is None inside @dataclass processing, raising AttributeError). link-check.py has no dataclasses today so this passes, but adding sys.modules[spec.name] = mod before exec_module(mod) future-proofs the loader.
…ss bucket Replace the lychee-only path with LINK_CHECKER=python|lychee (default lychee) and a stdlib link-check.py that emits lychee-compatible JSON. Egress-proxy refusals are classified as unchecked so they never open or auto-close issues. Signed-off-by: Jim Cadden <jcadden@ibm.com>
An empty config array aborted the macOS scan, and the new tests never ran. Unused reference definitions and post-tunnel resets are no longer unchecked, and a redirect to a private host is not fetched. Signed-off-by: Jim Cadden <jcadden@ibm.com> Assisted-by: Cursor <cursoragent@cursor.com>
urlsplit raises ValueError on netlocs like "http://[::1" (unclosed IPv6 bracket), and it ran unguarded in the collect filter, the check filter, and the worker loop — one such string in any scanned file aborted the whole repo scan (empty stdout, stderr discarded by the scanner). Route every urlsplit through _safe_split/_host_of. A malformed URL that claims to be a web link (http/https prefix) stays collected so the checker reports it as a real doc bug, matching lychee; malformed non-web schemes are dropped. Assisted-By: Claude (Anthropic AI) <noreply@anthropic.com> Signed-off-by: Jim Cadden <jcadden@ibm.com>
… the accept list The accept-list check ran before the 429 and HEAD-denial branches, so with the scanner's --accept list (which includes 403 and 429) the Retry-After backoff never executed and a HEAD 403 was accepted without consulting the GET answer. Reorder status resolution: 429 first (so the backoff runs even when 429 is accepted), then the HEAD-to-GET fallback for 405/403/501 (the GET answer decides; a HEAD-403/GET-404 link is now correctly reported as broken), then the accept list. When retries are exhausted on 429 and 429 is accepted, the link is success. parse_retry_after now treats naive HTTP-dates as GMT instead of local time. Pins the egress classification (origin resets/aborts/broken pipes behind a proxy are not 'unchecked') with direct assertions. Assisted-By: Claude (Anthropic AI) <noreply@anthropic.com> Signed-off-by: Jim Cadden <jcadden@ibm.com>
The fallback parser silently mis-parsed realistic .lychee.toml files: multi-line arrays (the common formatting) came back empty because only the first line was sliced, a '#' inside a quoted value truncated it and dropped the entry, and an empty quoted string in accept raised an uncaught ValueError from main(). Same config, different behavior depending on interpreter version. Strip comments only outside quoted strings, join multi-line arrays into one logical line before parsing, and coerce config accept values through normalize_accept, which warns about and skips non-integer entries instead of crashing. The fallback is pinned by direct _parse_toml_subset tests so it is covered regardless of runner version. Assisted-By: Claude (Anthropic AI) <noreply@anthropic.com> Signed-off-by: Jim Cadden <jcadden@ibm.com>
…st items Two extraction defects made the python checker noisier and quieter than lychee: - A ')' anywhere in a URL truncated it at the first paren (Wikipedia- style .../Foo_(bar) links produced false 404s and bogus issues). The inline and bare-URL patterns now accept one nesting level of balanced parens, and normalization trims only trailing parens that are not balanced by an opener (prose closers). - Any 4-space-indented line was blanked as a code block, so links in nested lists and indented prose (common README shapes) were silently never checked. Indented-code detection is now CommonMark-approximate: code cannot interrupt a paragraph, and inside a list item it must be indented past the item's content column. Golden fixtures extended with paren URLs, nested-list prose, loose-list continuations, lazy paragraph continuations, and still-skipped code blocks inside list items. Assisted-By: Claude (Anthropic AI) <noreply@anthropic.com> Signed-off-by: Jim Cadden <jcadden@ibm.com>
The checker invocation discarded stderr (2>/dev/null), so a python traceback left nothing to diagnose, and the scanner read .total/.errors with jq under set -e without validating the JSON first — a crash mid-write (truncated output) would abort the entire scan run with no latest.json update. Capture checker stderr to a temp file and print it on failure, treat a non-zero python checker exit as a failed repo (lychee still exempt: it exits 1 on mere broken links), and validate the report with 'jq empty' before parsing, counting any of these as REPOS_FAILED and continuing. Assisted-By: Claude (Anthropic AI) <noreply@anthropic.com> Signed-off-by: Jim Cadden <jcadden@ibm.com>
The stdlib checker needs only python3, so the program now runs on a basic agent image with no lychee install; lychee stays available as an opt-in fallback via LINK_CHECKER=lychee. Every e2e case that drives the lychee stub is pinned to LINK_CHECKER=lychee (they exercise checker-agnostic scanner logic; the real python checker would make them network-dependent), and a new case runs the real link-check.py through the default path with a deterministic malformed-URL finding. Assisted-By: Claude (Anthropic AI) <noreply@anthropic.com> Signed-off-by: Jim Cadden <jcadden@ibm.com>
d8cca92 to
0cf6277
Compare
jimcadden
left a comment
There was a problem hiding this comment.
The design is sound and the hardening commits land well — malformed-URL containment, HEAD→GET under the accept list, Retry-After, the TOML fallback and the stderr/JSON guards are all real fixes, and I re-ran the suite green. Two must-fix issues remain, both in the new default path.
1. lychee accept ranges silently disable 2xx acceptance. lychee's own documented config example is accept = ["200..=204", "429"] (lychee.example.toml:87). normalize_accept drops any non-integer entry, so that config becomes {429} and — because _check_once only accepts codes listed in accept — every 200 link is reported broken. A comma-string scalar (accept = "200, 429", also documented) collapses to the empty set, which flags every link including 200s. Verified locally: check_one(<200 url>, accept={429}) → {"code": 200}. This is the new default checker, so any enrolled repo with a realistic .lychee.toml flips to "everything is broken" and files issues for all of it.
2. A failed repo now auto-closes that repo's issues. The new containment paths (continue at 176/183/192) drop a failed repo from the current findings, but nothing keeps its prior findings out of the diff, so every previously-broken link in that repo counts as "fixed" and its issue is closed — while repos_failed is merely reported. Reproduced with a seeded baseline and a truncated-JSON checker: Delta: +0 new, -1 fixed, Closed issue #9 for README.md:https://dead.example.net/x, and broken: [] in the new latest.json. On main the same input aborted the run (exit 5, nothing closed), so this is a behaviour regression introduced by "fail the repo, not the run". It also contradicts standing-orders/link-health.md: "Do not close issues without evidence the link is fixed". The unchecked path already solves exactly this; failed repos need the same carry-forward.
Suggestion (not blocking): the ( in BARE_URL_RE's lookbehind still means (https://example.com/x) in prose is never extracted (carryover from clawgenti's round 3, unchanged). In shadow mode that was a divergence; now that python is the default those links are simply never checked. Fix: skip only when the ( follows ], or dedupe against MD_INLINE_RE.
Also (not blocking): is_private_host does an uncached getaddrinfo per URL, serially in the main thread before the pool starts, and --exclude-all-private is always passed — a repo with N links pays N DNS lookups even when they share a host (functools.lru_cache would do); WARN lines from the checker (unsupported .lychee.toml keys, bad regexes, non-integer accepts) go to CHECK_STDERR, which is only printed on failure, so config problems are invisible in a healthy run; and standing-orders/link-health.md still describes "Lychee fails on a repo" with no mention of LINK_CHECKER default/opt-in, the unchecked bucket, or shadow mode — worth updating in this PR since it is the program's operational contract.
What I verified
- 8 commits, DCO 8/8 (
Signed-off-byon all), CI 4/4 green at0cf6277, PR title prefix conforms. python3 tests/test-link-check.py→ 39 tests OK (1 skipped: the lychee parity test, lychee not installed here).tests/test-link-health-unchecked.sh16 PASS;test-extract-broken-links.sh26 PASS;test-link-health-dryrun-baseline.sh,test-link-health-subset-carryforward.sh,test-link-health-index-wiring.sh,test-weekly-report.sh,test-automation-health-dashboard.shPASS.shellcheck --severity=warning(0.11.0) clean on the four changed shell files.- Spot-checked behaviour, not just assertions: unchecked never opens/closes and is re-injected as recurring; shadow report is written before the
DRY_RUNearly-exit, so it cannot touch the baseline; malformedhttp://[::1is reported as a finding instead of aborting;[a](…/Foo_(bar))and nested-list prose extract correctly; unused reference definitions are dropped; inline/fenced/indented code are skipped. - No
.claude/or.vscode/paths anywhere in the diff.
Author: jimcadden (MEMBER — maintainer)
Areas reviewed: Python, shell, YAML/CI, docs
Agent/IDE config (.claude/.vscode): none
Commits: 8, all signed-off
CI status: passing (4/4)
Verdict: request changes on the two must-fix items above. Submitted as a COMMENT review because the PR author is the GitHub account this reviewer authenticates as (APPROVE/REQUEST_CHANGES self-reviews 422).
Generated by RepoMan github-pr-review@235a7f7 (https://github.com/rossoctl/agent-skills/blob/235a7f7620139ee3e4ab0872b8439591fe52e8c1/skills/github-pr-review/SKILL.md).
| return codes | ||
|
|
||
|
|
||
| def normalize_accept(val: object) -> Set[int]: |
There was a problem hiding this comment.
must-fix — normalize_accept silently drops non-integer entries, but lychee's documented accept format uses ranges and comma strings: lychee.example.toml:87 ships accept = ["200..=204", "429"], and :88 accept = "200..=204, 429". With a config like that the set collapses to {429} (or set() for the comma-string scalar), and because _check_once only treats codes in accept as success, every 200 link is reported broken — on the new default checker that means a full-repo false-positive run and an issue per link. Verified locally on a 200 endpoint: check_one(url, accept={429}) -> {'code': 200, 'text': '200'}, accept=set() -> same. Fix: expand a..=b / a..b ranges, split comma-separated scalars, and if the normalized set ends up empty, fall back to {200, 204, 206} with a WARN instead of accepting nothing. Worth a unit test for both the range and the comma-string form.
| echo " WARN: $LINK_CHECKER emitted invalid JSON for $full_repo; stderr:" | ||
| head -50 "$CHECK_STDERR" | sed 's/^/ /' >&2 | ||
| REPOS_FAILED=$((REPOS_FAILED + 1)) | ||
| continue |
There was a problem hiding this comment.
must-fix — this (and the two above at :175 and :182) drops a failed repo from the current findings, but its prior findings stay in $PREV_BROKEN, so every previously-broken link in that repo diffs as fixed and its issue is auto-closed. Reproduced with a seeded baseline containing alice/a + https://dead.example.net/x and a checker that emits truncated JSON: Delta: +0 new, -1 fixed, Closed issue #9 for README.md:https://dead.example.net/x, and broken: [] in the freshly written latest.json — the baseline is wiped too, so the next run re-files everything as new. On main the same input aborted the whole scan (exit 5) with nothing closed, so this is a regression from "fail the repo, not the run", and it contradicts standing-orders/link-health.md ("Do not close issues without evidence the link is fixed"). The unchecked path already does the right thing; failed repos need the same treatment — collect the failed full_repo list and re-inject their $PREV_BROKEN records into broken.jsonl (or subtract their keys from the fixed set) before diff_against_previous. A test like case 2 but with a crashing checker and a seeded baseline would pin it.
| MD_REF_USE_RE = re.compile(r"\[([^\]]*)\]\[([^\]]*)\]") | ||
| MD_AUTOLINK_RE = re.compile(r"<(https?://[^>\s]+)>", re.IGNORECASE) | ||
| BARE_URL_RE = re.compile( | ||
| r"(?<![(\[<\"'])(https?://(?:[^\s<>\(\)\]\"']|\([^()\s]*\))+)", re.IGNORECASE |
There was a problem hiding this comment.
suggestion — carryover from clawgenti's round 3, still open: the ( in the lookbehind means a URL that starts immediately after a parenthesis is never extracted, so prose like See (https://example.com/release) for details. yields [] (verified). lychee extracts it. In shadow mode this was only a parity divergence; now that python is the default those links are silently never checked. Suggest skipping only when the ( is preceded by ] (markdown-link target), or deduping against MD_INLINE_RE matches instead of using a lookbehind for (.
| continue | ||
| host = _host_of(url) | ||
| if exclude_private: | ||
| priv = is_private_host(host) |
There was a problem hiding this comment.
suggestion — is_private_host performs a getaddrinfo per URL, uncached, and this loop runs serially in the main thread before the ThreadPoolExecutor starts. The scanner always passes --exclude-all-private, so a repo with N links pays N DNS lookups even when they are all the same host, which is a visible cost now that this replaces a Rust implementation that caches. functools.lru_cache on is_private_host (or a dict memo keyed on the host) fixes it; it also caps the worst case, since a hanging resolver is not covered by --timeout.
| continue | ||
| fi | ||
|
|
||
| # Truncated output (a crash mid-write) must not kill the whole scanner via |
There was a problem hiding this comment.
suggestion — the checker's stderr is now captured (good) but only printed when the repo fails, so its WARN lines are invisible on a healthy run: unsupported .lychee.toml keys (load_lychee_toml), bad exclude regexes, and skipped non-integer accept entries all go there — exactly the diagnostics that explain a silent behaviour difference from lychee. Suggest surfacing them, e.g. if [ -s "$CHECK_STDERR" ]; then sed 's/^/ WARN: /' "$CHECK_STDERR" >&2; fi on the success path.
| spec = importlib.util.spec_from_file_location("link_check_mod", CHECKER) | ||
| mod = importlib.util.module_from_spec(spec) | ||
| assert spec.loader is not None | ||
| spec.loader.exec_module(mod) |
There was a problem hiding this comment.
nit — carryover from round 3 and still unfixed: exec_module on a module that was never registered in sys.modules is the pattern that broke #124's ubuntu leg on Python 3.14 (sys.modules.get(cls.__module__) is None inside @dataclass). link-check.py has no dataclasses today, so this passes; sys.modules[spec.name] = mod before exec_module(mod) future-proofs the loader.
lychee's own documented config examples are accept = ["200..=204",
"429"] and accept = "200, 429". normalize_accept dropped every
non-integer entry, so the first became {429} (every 200 link reported
broken) and the second collapsed to the empty set (every link broken).
A config accept that expands to no codes now keeps the scanner-level
list with a warning instead of flagging everything.
Also registers the checker module in sys.modules before exec_module
(the Python 3.14 loader requirement that broke doc-drift's tests).
Assisted-By: Claude (Anthropic AI) <noreply@anthropic.com>
Signed-off-by: Jim Cadden <jcadden@ibm.com>
…fails The crash-containment paths (non-zero exit, no output, invalid JSON) drop a failed repo from the current findings, but nothing kept its prior findings out of the diff — every previously broken link in the repo counted as fixed and its issue auto-closed while repos_failed merely ticked up. On main the same input aborted the run instead, so this was a regression introduced by 'fail the repo, not the run'. Failed repos are now recorded and their prior broken records are re-injected before the diff (the same rule the unchecked bucket already uses: no evidence of fixing, no close). E2e case 3b reproduces the reviewer's scenario: seeded baseline, truncated-JSON checker, no close, finding carried forward, delta.fixed == 0. Assisted-By: Claude (Anthropic AI) <noreply@anthropic.com> Signed-off-by: Jim Cadden <jcadden@ibm.com>
The bare-URL lookbehind dropped any URL immediately after '(', so
prose like "(see https://example.com/x)" was never checked — a
divergence from lychee that mattered little in shadow mode and matters
a lot now that python is the default. Block only '](' (the inline
markdown link opener, which MD_INLINE_RE owns) instead of any '('.
Assisted-By: Claude (Anthropic AI) <noreply@anthropic.com>
Signed-off-by: Jim Cadden <jcadden@ibm.com>
is_private_host did an uncached getaddrinfo per URL, serially in the main thread before the pool started (--exclude-all-private is always passed), so a repo with N links paid N DNS lookups even when the links share a host; lru_cache dedupes them. The scanner printed CHECK_STDERR only on failure, so checker WARN lines (unsupported .lychee.toml keys, bad exclude regexes, non-integer accept entries) were invisible on a healthy run; they are now shown (capped at 10 lines) for every scanned repo. Assisted-By: Claude (Anthropic AI) <noreply@anthropic.com> Signed-off-by: Jim Cadden <jcadden@ibm.com>
The operational contract still described a lychee-only program. Document the LINK_CHECKER default/opt-in, the unchecked (egress-refusal) bucket and its no-close rule, shadow mode, the .lychee.toml subset the python checker honors, and the failed-repo carry-forward escalation. Assisted-By: Claude (Anthropic AI) <noreply@anthropic.com> Signed-off-by: Jim Cadden <jcadden@ibm.com>
|
Review findings addressed in 3402934..31ebeca — full suite green locally (42 unit tests, 20 e2e assertions across 5 cases, extract-broken-links 26, dashboard/index-wiring/subset-carryforward/dryrun-baseline/weekly-report, shellcheck clean). Must-fix
Suggestions
Also fixed in Assisted-By: Claude Code |
… extraction
Three extraction gaps found in review, each able to make existing
findings look fixed or file false ones:
- Shortcut reference links ("see [docs]." + "[docs]: url") extracted
nothing: the definition was dropped as unused because only
[text][label] uses were recognized. Shortcut references now keep
their definitions alive.
- HTML files went through the markdown code stripper, so a
four-space-indented <a href> after a blank line was blanked as a
code block. Extraction is now format-aware: .html/.htm files skip
the markdown fence/indent/inline-code stripping (indents and
backticks are ordinary formatting in HTML).
- Bare-URL and markdown passes ran before the HTML parser's
<pre>/<code> skip, so example URLs inside HTML code regions were
checked and could file false issues. <pre>/<code> regions are now
blanked for every extraction pass, in both formats.
Assisted-By: Claude (Anthropic AI) <noreply@anthropic.com>
Signed-off-by: Jim Cadden <jcadden@ibm.com>
The Python<3.11 fallback copied double-quoted values verbatim, so an exclusion regex written as "foo\\.net" stayed double-escaped and silently stopped matching — same config, different behavior depending on interpreter version. Basic strings are now decoded (simple escapes plus \uXXXX/\UXXXXXXXX), matching tomllib; single-quoted literal strings are untouched, and unknown escapes are kept literally as a best effort for regex content. Assisted-By: Claude (Anthropic AI) <noreply@anthropic.com> Signed-off-by: Jim Cadden <jcadden@ibm.com>
|
All five findings confirmed and addressed — note the review was taken at P1: Failed scans can auto-close existing issues — already fixed in P2: Shortcut reference links disappear — fixed in P2: Markdown code stripping removes real HTML links — fixed in P2: URLs inside HTML code examples are checked — fixed in P2: Older-Python TOML parsing changes exclusion regexes — fixed in Full suite green locally: 48 unit tests (up from 42), 20 e2e assertions across 5 cases, extract-broken-links 26, dashboard / index-wiring / subset-carryforward / dryrun-baseline / weekly-report all PASS, shellcheck clean. Assisted-By: Claude Code |
Two fallback-parser defects from review:
- An empty quoted string ("accept = [\"\"\"]") produced an all-empty
regex capture tuple that was dispatched to the integer alternative and
crashed on int('') — before normalize_accept's warn-and-skip could
ever run. Token type is now decided by which group matched (finditer),
not by nonempty capture content.
- Escaped quotes split tokens at the \" ("exclude = [\"foo\\\"bar\", \"baz\"]\"
became ['foo\\', ', '] and dropped baz), because tokenization,
comment stripping, and bracket counting all treated \" as closing the
string. The item pattern is now escape-aware for basic strings, and
the comment/bracket scanners skip escaped characters inside them.
Both regressions run explicitly through the fallback path, and the
escaped-quote case asserts parity with tomllib as the reference.
Assisted-By: Claude (Anthropic AI) <noreply@anthropic.com>
Signed-off-by: Jim Cadden <jcadden@ibm.com>
|
Both must-fix findings confirmed and fixed in Empty strings crash the fallback parser — confirmed: Escaped quotes corrupt exclusion arrays — confirmed: Verified on both reviewer repros after the fix: Assisted-By: Claude Code |
Replace the lychee-only path with LINK_CHECKER=python|lychee (default python) and a stdlib link-check.py that emits lychee-compatible JSON. Egress-proxy refusals are classified as unchecked so they never open or auto-close issues. The link-health program now needs only python3 (plus gh/jq/bash) — no lychee install — so it runs on a basic agent image (rossoctl/repoman#3).
What's in the branch
scripts/link-check.py— stdlib-only checker (urllib, concurrent.futures, html.parser, re, ipaddress, tomllib when available; Python 3.8+). Scanner arg parity,.lychee.tomlsubset (exclude/exclude_path/accept with a Python<3.11 fallback parser), per-host concurrency cap, Retry-After on 429, HEAD→GET fallback, redirect cap, real User-Agent.LINK_CHECKERtoggle (default python, lychee opt-in), shadow mode (LINK_HEALTH_SHADOW_OUT), unchecked split/reporting (count + top hosts in latest.json, history, dashboard), and no auto-close of previously-broken links that come back unchecked.extract-broken-links.sh:uncheckedstatus token (mapped before the error catch-all).tests/test-link-check.py(39 unit tests: extraction goldens, code-block skipping, 429/Retry-After, HEAD→GET, proxy refusal → unchecked, TOML fallback, CLI smoke),tests/test-link-health-unchecked.sh(e2e: unchecked never opens/closes issues, crash containment, default-python path), fixtures undertests/fixtures/link-check/.Hardening added on top of the original two commits
http://[::1ran unguarded in three places); they are reported as findings like lychee.#-inside-values, and non-integer accept entries (warn+skip instead of crash)..../Foo_(bar)) are no longer truncated (Wikipedia-style false 404s), and links in nested lists / indented prose are no longer skipped as code blocks (CommonMark-approximate).Verification
python3 tests/test-link-check.py— 39 tests OK./bin/bash tests/test-link-health-unchecked.sh(bash 3.2) — all cases pass, including default-python and crash containment.env -iwith a minimal PATH (no lychee present) —Link checker: python, repos scanned 1/0 failed, exit 0.Assisted-By: Claude Code