Skip to content

link-health: replace lychee with a stdlib Python link checker #3

Open

Description

@clawgenti

link-health: replace lychee with a stdlib Python link checker

Why

lychee is the only RepoMan dependency that isn't already on a typical agent image
(bash, gh, jq, curl, Python ≥ 3.8 are). On DAM it has to be installed with mise
on every VM restart, and it blocks one-click packaging as a starter kit (see the
starter-kit issue #2). It also reports egress-proxy refusals as generic errors, so under a
restricted egress preset every link to an unlisted host would be filed as "broken".

How lychee is used today (engine @ b3f128c)

  • Scanner only. link-health-scanner.sh calls it once per enrolled repo:
    lychee --format json --scheme http --scheme https \
      --exclude localhost|127.0.0.1|localtest.me|example.com|example.org|console.cloud.google.com \
      --exclude-all-private --exclude-path node_modules|vendor|.claude \
      --accept 200,204,206,403,429,502,503 \
      --timeout 10 --max-retries 2 --max-concurrency 8 \
      [--config <repo>/.lychee.toml] <repo_dir>
    
  • lychee does link extraction (md/html/rst, skipping code blocks; pinned ≥ 0.23.0 for
    that), concurrent checking with retries, and the JSON report.
  • extract-broken-links.sh reads only total, errors and error_map from that report,
    normalizes status, categorizes internal/external, and suppresses .svc/.local/RFC1918
    hosts (unit-tested in tests/test-extract-broken-links.sh).
  • The fixer doesn't use lychee (curl re-verify + Wayback API).

Proposal

Add link-check.py (stdlib only: urllib, concurrent.futures, html.parser, re,
tomllib when available) that emits lychee-compatible JSON (total, errors,
error_map), so extract-broken-links.sh and its tests stay unchanged and only the one
call site in the scanner changes.

Requirements

  • Extraction: markdown inline/reference/autolinks + bare URLs, HTML href/src;
    skip fenced and indented code blocks, inline code, <code>/<pre> (the old
    false-positive source).
  • Scanner arg parity: schemes, --exclude regexes, private-IP exclusion,
    --exclude-path, accept list, timeout 10s, 2 retries, concurrency 8.
  • .lychee.toml subset: exclude, exclude_path, accept (warn on and ignore
    other keys). On Python < 3.11 (no tomllib), fall back to a minimal parser for those keys.
  • Politeness: per-host concurrency cap, honor Retry-After on 429, HEAD → GET
    fallback (405/403/501), follow redirects (max 10), a real User-Agent.
  • Egress-refusal detection: recognize the gateway's refusal (proxy 403 / closed
    connection on CONNECT) and report it with a distinct status (unchecked).
    extract-broken-links.sh maps unchecked to its own bucket, which never opens or
    auto-closes an issue. Run summary + automation-health dashboard show the unchecked
    count and top hosts. (This is the "unchecked" work item from the starter-kit issue Package RepoMan as a DAM starter kit (curated/repoman) #2.)
  • Switch: LINK_CHECKER=python|lychee (default lychee until cutover).
  • Tests: fixtures for code-block skipping, excludes, 429/Retry-After, redirect,
    HEAD→GET, proxy refusal → unchecked; golden JSON compared against lychee output.

Rollout

  1. Land in rossoctl/automation (engine source of truth); vendor into RepoMan with
    scripts/vendor-engine.sh.
  2. Shadow mode: run both checkers on the enrolled repos for ~2–3 weekly scans and diff
    the broken sets; investigate every disagreement.
  3. Flip the default to python; keep lychee as an opt-in fallback for one release, then
    drop it from the README prerequisites and the link-health-scanner Requirements.

Alternative considered

Download a pinned, checksum-verified lychee release binary in the kit's install step
(GitHub is reachable under trusted). Less code to own, but keeps the external binary,
needs per-arch handling, and doesn't solve the egress-refusal classification.

Out of scope

Non-HTML/markdown formats lychee supports (rst, etc.) unless an enrolled repo needs them.

Contributions follow repo conventions: DCO sign-off + Assisted-By: trailer.

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

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

No type

Projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions