Check Links #589
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: Check Links | |
| # Lychee is the EXTERNAL link checker. It makes real network requests and is | |
| # flaky by nature. Site-internal routes (`/docs/...`) are a different problem | |
| # with a different tool — `scripts/check-doc-links.mjs`, run by | |
| # `docs-links.yml`, which reads the checkout and nothing else and is therefore | |
| # the one that gates pull requests. The two are complementary, not redundant; | |
| # do not move either one's job onto the other. | |
| # | |
| # SCAN SCOPE — stated as populations, never counted (objectui#7448, | |
| # objectui#7825). What gets swept is decided by the `args` glob list below and | |
| # by nothing else: the published documentation tree `content/docs/**` (the | |
| # fumadocs content source `apps/site/source.config.ts` declares — | |
| # `dir: '../../content/docs'`, baseUrl `/docs`), the repo-root INTERNAL tree | |
| # `docs/**` (ADRs, audits), and `README.md`. | |
| # | |
| # ⛔ Never write the size of either tree back into this comment. Two hand-copied | |
| # sizes stood here, one per tree, and both had drifted by the time anyone | |
| # looked, with nothing red over the whole distance — nothing fails on a stale | |
| # number written in a comment, which is exactly why it rots. Point at the | |
| # reading instead: `find content/docs docs -type f \( -name '*.md' -o -name | |
| # '*.mdx' \) | wc -l` in any checkout, and Lychee's own run summary, which | |
| # reports what it actually scanned. `check-links-workflow.test.ts` enforces | |
| # this — it reds on the next numeral that qualifies a document population | |
| # anywhere in this header. | |
| # | |
| # HISTORY, past tense on purpose — none of the following describes the scope | |
| # today. Until PR #3449 the `args` list named only the repo-root `docs/**`, so | |
| # not one published page had ever been scanned: this workflow was green because | |
| # of what it was not looking at. #3449 pointed it at both trees, and | |
| # `scripts/__tests__/check-links-workflow.test.ts` derives the expected scope | |
| # from the site config rather than restating it, so moving the content tree | |
| # turns that test red instead of blinding the sweep a second time. | |
| on: | |
| workflow_dispatch: | |
| # Weekly sweep, Sundays at 04:17 UTC. | |
| # | |
| # The PR that fixed #3213 deliberately did NOT add a cron, and it was right to | |
| # hold off: with the scope pointed at the wrong tree, a schedule only turns | |
| # one false-green report into a periodically produced false-green report. Now | |
| # that the scope is correct, #3213's ruling-B intent — a periodic sweep — is | |
| # real, so the schedule lands in the same change that makes it meaningful. | |
| # | |
| # Timing: off the top of the hour, because GitHub's scheduled-workflow queue | |
| # is most congested (and most delayed) at :00; Sunday because that is when the | |
| # repo contends least for runners. | |
| schedule: | |
| - cron: '17 4 * * 0' | |
| # ⛔ Do NOT enable the two triggers below — #3213's ruling B still stands. | |
| # An external link check goes over the network: one 502, rate-limit or | |
| # anti-scraping response from a third-party site turns an unrelated pull | |
| # request red, and its author can do nothing about it. `schedule` + | |
| # `workflow_dispatch` do not have that problem: this workflow blocks nobody, | |
| # so flakiness costs a second look and nothing else. | |
| # push: | |
| # branches: | |
| # - main | |
| # pull_request: | |
| jobs: | |
| check-links: | |
| runs-on: ubuntu-latest | |
| # ── The job ceiling, DERIVED FOR THIS JOB (objectui#7956) ──────────── | |
| # Before this line the only backstop was GitHub's 360-minute default, on the | |
| # one job in this repository that deliberately goes out over the network. | |
| # | |
| # ⚠️ The stakes here are NOT objectui#7270's. That card's argument leaned on | |
| # two of its three jobs producing a REQUIRED context on a shared serial | |
| # queue. Re-measured on `GET /repos/objectstack-ai/objectui/rules/branches/ | |
| # main` at 2026-09-06T17:20Z, the required checks are `Lint`, `Type Check`, | |
| # `Build & E2E`, `Test (shard 1/4)` through `(4/4)`, `Build Docs` and | |
| # `Changeset Declaration` — this job produces none of them, and the header | |
| # above says it blocks nobody on purpose. What is left is the generic | |
| # exposure: a sweep wedged on an unresponsive host holds a runner for six | |
| # hours and then reports `cancelled`. | |
| # | |
| # ⛔ NOT inherited from `ci.yml`'s 10/15/20/30/40, nor from the ceilings | |
| # objectui#7270 derived — objectui#7048 fences exactly that. The arithmetic | |
| # below is this job's own. | |
| # | |
| # ⚠️ THE SAMPLE HAS TO BE CHOSEN, and the obvious choice is the wrong one. | |
| # `status=success` returns a `total_count` of 217 for this workflow, and | |
| # every single one of those runs is from 2026-01-24 .. 2026-01-28 on `push` | |
| # and `pull_request` — triggers this workflow no longer declares, under the | |
| # narrower scan scope it had before #3449. Those runs measure a different | |
| # job wearing this job's name, and deriving from them would put the ceiling | |
| # under today's honest slowest run, which is the hazard objectui#7048 | |
| # fenced. They are recorded here and NOT used. | |
| # | |
| # - Population: every run under today's configuration — | |
| # `?event=schedule`, whose `total_count` is 5, window 2026-08-09T04:43Z | |
| # .. 2026-09-06T04:28Z. That is the entire population since the cron | |
| # landed, not a sample cut short; paging further buys the January runs | |
| # described above. Per-job wall clock from the Actions jobs endpoint | |
| # (`completed_at` minus `started_at`), never the run's total. | |
| # - min 15s / median 24s / p95 30s / max 31s. | |
| # - max/p95 = 1.03 — no fat tail inside the sample. | |
| # - ⚠️ All five ended `failure`, and that does NOT make them unusable: the | |
| # step conclusions show Lychee running 9s .. 24s and the job reaching its | |
| # post steps every time, so the failure is the sweep's own verdict | |
| # (`fail: true`, broken links found), not an abort partway through the | |
| # work. It is also the CONSERVATIVE side of the measurement — a link | |
| # that cannot be resolved is retried (`max_retries` in `lychee.toml`), | |
| # so a sweep that finds breakage does strictly more work than a clean | |
| # one. The max above therefore over-states a healthy run rather than | |
| # under-stating it. | |
| # - ⚠️ That every scheduled sweep so far has been red is a defect in its | |
| # own right and is filed as objectui#8126's sibling, objectui#8128. It | |
| # is deliberately NOT fixed by the same change that sets this ceiling: | |
| # the ceiling is about what happens when a sweep wedges, and the red is | |
| # about what the sweep found. | |
| # | |
| # Ceiling = the smallest round number that is both >= 3x max (1.6min) and | |
| # >= max + 15min (15.5min) => 20. That is ~39x the slowest sweep observed | |
| # under this configuration; a wedge dies in 20 minutes instead of 6 hours. | |
| # | |
| # ⚠️ The residual this does NOT bound, named rather than hidden: | |
| # `lychee.toml` declares a per-request `timeout` and `max_retries` at a | |
| # `max_concurrency` of 10, so a whole-run worst case scales with how much | |
| # this workflow sweeps — and that size is exactly the number the header | |
| # above forbids writing down, because it rots (objectui#7448, | |
| # objectui#7825). The additive 15 minutes is where that residual is | |
| # absorbed, not a proof against it. If a network-weather run ever does hit | |
| # this ceiling, the cost is a `cancelled` weekly sweep that blocks nobody — | |
| # which the header already accepts as this workflow's failure currency. | |
| # | |
| # ⚠️ Four of the six jobs objectui#7956 bounded land on 20 by this same | |
| # arithmetic. That is NOT one number copied across them, which precondition | |
| # ② of that card's triage forbids: for any job whose slowest run is under | |
| # 7.5 minutes the additive term dominates the multiplicative one, so the | |
| # rule puts every sub-minute job on the same rung. Each of the four carries | |
| # its own sample and its own derivation beside its own key. | |
| # | |
| # ⛔ Raising this toward 360 is the ruled-out non-fix (objectui#6577, | |
| # objectui#7048): the hang just runs longer and the gate still reports | |
| # `cancelled`. | |
| timeout-minutes: 20 | |
| steps: | |
| - name: Checkout repository | |
| uses: actions/checkout@v7 | |
| - name: Check links with Lychee | |
| uses: lycheeverse/lychee-action@v2 | |
| with: | |
| # Both trees: | |
| # content/docs/** — the published site's documentation (the fumadocs | |
| # content source). Never scanned before #3449; the whole point. | |
| # docs/**, README.md — the repo-root internal material (ADRs, | |
| # audits). Their external links are worth sweeping too and cost | |
| # nothing extra, so they stay. | |
| # Site-absolute routes (`/docs/...`) are NOT judged here — see the | |
| # `root_dir` section of lychee.toml. | |
| args: | | |
| --verbose | |
| --no-progress | |
| --config lychee.toml | |
| 'content/docs/**/*.md' | |
| 'content/docs/**/*.mdx' | |
| 'docs/**/*.md' | |
| 'docs/**/*.mdx' | |
| 'README.md' | |
| # Fail the action on broken links | |
| fail: true | |
| env: | |
| GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}} |