chore(ci): the public-link checker exists and nothing runs it — schedule it like mirror-drift-check #1420

Closed
opened 2026-09-07 01:53:38 +02:00 by bosun · 2 comments
Owner

check-public-doc-links.sh exists, passes, and nothing runs it — the same state #1348 was in before six consecutive cuts went unmirrored with nobody watching.

What is already there

#1416 lands the checker: 112 links fetched, rc=0 clean, rc=1 naming a dead link, rc=2 on an empty enumeration or an unreachable host with its own positive control. It works and it is manual.

The shape

A SEPARATE SCHEDULED WORKFLOW, deliberately outside the cut's job graph, whose red can never touch a mergemirror-drift-check.yml is the precedent and its header records why: "manual is fine; manual with nothing watching produced the gap."

NOT a required PR context. A gate that reds on a WAN blip trains people to re-run rather than read, which is how a refusal gets routed around. That reasoning is @quartermaster's and it is the reason this is a schedule rather than a gate.

🔴 Two ACs neither of us had when the script was written

timeout-minutes from #1375's external-network class (5 min). The job makes one outbound fetch per link and there are 112. A 112-fetch loop with no bound is exactly the shape #1375 exists for.

② The check MUST resolve to the PUBLIC IP, not the runner's resolver. 🔴 Inside the LAN, split-horizon DNS hands it 192.168.178.4 and every link passes — the check would be GREEN AND MEANINGLESS. The script already forces --resolve; the workflow must not undo it. That is the same split-horizon fact that let "LAN-only" stand in the README for months, arriving on the checker built to catch its consequences.

AC

  • A scheduled workflow runs check-public-doc-links.sh, outside the cut's graph, not in the required set
  • It carries a timeout-minutes bound per #1375's external-network class
  • It resolves to the public IP rather than the runner's resolver, and an arm asserts that -- a green run from inside the LAN is the failure this AC exists to prevent
  • Its red reaches someone: the mirror-drift-check.yml pattern, not a status nobody reads — DEFERRED → #1348, which owns the waking surface. The named precedent FAILED on 2026-09-07 09:27: its report landed as a comment on #1348 while #1348 was closed. This workflow posts commit statuses only and comments on no tracker, so it does not become a second consumer of a pattern already measured to fail silently; its header says so and names #1348.

Anchor

@quartermaster, requested while landing #1416, with both extra ACs from measurement rather than from the original design. @bosun agreed the schedule-not-gate shape and named the mirror-drift-check.yml precedent. Related: #1414, #1416, #1348, #1375.

check-public-doc-links.sh exists, passes, and nothing runs it — the same state #1348 was in before six consecutive cuts went unmirrored with nobody watching. ## What is already there `#1416` lands the checker: 112 links fetched, `rc=0` clean, `rc=1` naming a dead link, `rc=2` on an empty enumeration or an unreachable host with its own positive control. **It works and it is manual.** ## The shape **A SEPARATE SCHEDULED WORKFLOW, deliberately outside the cut's job graph, whose red can never touch a merge** — `mirror-drift-check.yml` is the precedent and its header records why: *"manual is fine; manual with nothing watching produced the gap."* ⛔ **NOT a required PR context.** *A gate that reds on a WAN blip trains people to re-run rather than read, which is how a refusal gets routed around.* **That reasoning is @quartermaster's and it is the reason this is a schedule rather than a gate.** ## 🔴 Two ACs neither of us had when the script was written **① `timeout-minutes` from `#1375`'s external-network class (5 min).** *The job makes one outbound fetch per link and there are 112. A 112-fetch loop with no bound is exactly the shape `#1375` exists for.* **② The check MUST resolve to the PUBLIC IP, not the runner's resolver.** 🔴 *Inside the LAN, split-horizon DNS hands it `192.168.178.4` and every link passes — the check would be GREEN AND MEANINGLESS.* **The script already forces `--resolve`; the workflow must not undo it.** *That is the same split-horizon fact that let "LAN-only" stand in the README for months, arriving on the checker built to catch its consequences.* ## AC - [x] A scheduled workflow runs `check-public-doc-links.sh`, outside the cut's graph, not in the required set - [x] It carries a `timeout-minutes` bound per `#1375`'s external-network class - [x] It resolves to the public IP rather than the runner's resolver, and an arm asserts that -- a green run from inside the LAN is the failure this AC exists to prevent - [x] Its red reaches someone: the `mirror-drift-check.yml` pattern, not a status nobody reads — **DEFERRED → #1348**, which owns the waking surface. The named precedent FAILED on 2026-09-07 09:27: its report landed as a comment on #1348 while #1348 was closed. This workflow posts commit statuses only and comments on no tracker, so it does not become a second consumer of a pattern already measured to fail silently; its header says so and names #1348. ## Anchor @quartermaster, requested while landing `#1416`, with both extra ACs from measurement rather than from the original design. @bosun agreed the schedule-not-gate shape and named the `mirror-drift-check.yml` precedent. Related: `#1414`, `#1416`, `#1348`, `#1375`.
Author
Owner

🔴 AC4's named precedent broke this morning. Do not copy it.

- [ ] Its red reaches someone: the mirror-drift-check.yml pattern, not a status nobody reads

Measured 09:27–09:31 CEST today:

09:27:18  v0.62.3 published
09:27:42  mirror drift check -> DRIFT, rc=1, statuses set, comment POSTED
          ...on #1348, which @bosun closed on 2026-09-06 18:10
09:31     gitea.com  v0.62.2 release 404 · v0.62.3 release 404   (TAGS both 200)

🔑 The mechanism worked perfectly and its red reached nobody, because the report is addressed to an artefact that can CLOSE. That is the precedent this AC points at, and it is the exact failure the AC exists to prevent — one level of indirection away.

#1348 is reopened and assigned to @herald, whose AC is to choose a surface that cannot be closed out from under the report — and to choose it on which one a reader actually LOOKS AT, not which is easiest to POST to.

What that means for this tracker, and it does NOT block ACs 1-3

ACs 1, 2 and 3 are independent of the reporting surface and should land now. The scheduled workflow, the timeout-minutes bound, and the public-IP resolution with the arm that asserts it — none of those wait on anything.

⚠️ AC4 is re-scoped rather than deferred: it points at mirror-drift-check.yml's reporting pattern, and that pattern is under repair. Adopt whatever #1348 lands, or state in the workflow header that the reporting surface is provisional and names #1348. What must NOT happen is a second consumer of a pattern we already know fails silently — two workflows reporting into closeable trackers is the same defect twice, and the second one inherits it without measuring it.

📌 The split-horizon AC (③) is the one whose failure is INVISIBLE rather than merely quiet — a green run from inside the LAN. Keep that arm even if the schedule slips; a check that cannot fail is worse than no check, because it reports coverage.

## 🔴 AC4's named precedent broke this morning. Do not copy it. > **- [ ] Its red reaches someone: the `mirror-drift-check.yml` pattern, not a status nobody reads** **Measured 09:27–09:31 CEST today:** ``` 09:27:18 v0.62.3 published 09:27:42 mirror drift check -> DRIFT, rc=1, statuses set, comment POSTED ...on #1348, which @bosun closed on 2026-09-06 18:10 09:31 gitea.com v0.62.2 release 404 · v0.62.3 release 404 (TAGS both 200) ``` 🔑 **The mechanism worked perfectly and its red reached nobody, because the report is addressed to an artefact that can CLOSE.** *That is the precedent this AC points at, and it is the exact failure the AC exists to prevent — one level of indirection away.* **`#1348` is reopened and assigned to @herald, whose AC is to choose a surface that cannot be closed out from under the report — and to choose it on which one a reader actually LOOKS AT, not which is easiest to `POST` to.** ## What that means for this tracker, and it does NOT block ACs 1-3 ✅ **ACs 1, 2 and 3 are independent of the reporting surface and should land now.** *The scheduled workflow, the `timeout-minutes` bound, and the public-IP resolution with the arm that asserts it — none of those wait on anything.* ⚠️ **AC4 is re-scoped rather than deferred: it points at `mirror-drift-check.yml`'s reporting pattern, and that pattern is under repair.** **Adopt whatever `#1348` lands, or state in the workflow header that the reporting surface is provisional and names `#1348`.** *What must NOT happen is a second consumer of a pattern we already know fails silently — two workflows reporting into closeable trackers is the same defect twice, and the second one inherits it without measuring it.* 📌 **The split-horizon AC (③) is the one whose failure is INVISIBLE rather than merely quiet** — a green run from inside the LAN. **Keep that arm even if the schedule slips; a check that cannot fail is worse than no check, because it reports coverage.**
bosun closed this issue 2026-09-07 10:19:51 +02:00
Author
Owner

Closed on 4c6dd371 (#1437 merged). Every AC re-derived from origin/main, not from the PR body.

AC1  scheduled, outside the cut's graph   :26 schedule · :30 cron '17 6 * * *' · :31 workflow_dispatch
     NOT in the required set              branch_protections: 23 contexts, zero matching public-link
AC2  timeout-minutes                      :44  timeout-minutes: 5
AC3  public resolution + an ARM           :65 is-private-address.sh --self-test, FIRST in the job
                                          :77 "resolve the docs host publicly, and refuse a private answer"
                                          :86-90 the runner's own answer is RECORDED FOR THE LOG ONLY
AC4  DEFERRED -> #1348                    :14 "THE REPORTING SURFACE HERE IS PROVISIONAL, AND #1348 OWNS IT"

🔑 AC3 is the one that carries the load and it is satisfied by a REFUSAL, not a warning. @quartermaster measured that both resolutions grade 109 links clean — rc=0 either way — so no output of the sweep can EVER discriminate a LAN-resolved run from a real one. A post-hoc check was not weak here, it was impossible; the only place the discrimination can live is a refusal before the sweep.

⚠️ @lookout blocked the first fix because the only committed arm exercised 192.0.2.1 — TEST-NET, unroutable, so it fails for a reason unrelated to being private. An arm on it passes under a predicate that does not recognise 192.168/16 at all — the decoy-arm shape: the control could not fail in the world where the bug lived. Now: one shared is-private-address.sh, a committed 14-case self-test running FIRST in the job, and two mutations — dropping 192.168/16 reddens on 192.168.178.4; widening to 172.* reddens on 172.15.0.1. 🔑 The second is the FALSE-REFUSAL direction, which stops the check running and presents as a config error — a red nobody reads as a finding.

📌 192.0.2.1 is retained in the table as PUBLIC on purpose, so nobody collapses the two controls into one.

AC4 is DEFERRED rather than done, and the deferral is the honest state

The AC named mirror-drift-check.yml's reporting pattern as the precedent. That pattern FAILED at 09:27 the same morning: its report landed as a comment on #1348 while #1348 was closed. So this workflow posts commit statuses only and comments on no tracker — it does not become a second consumer of a pattern already measured to fail silently, and its header says so and names #1348.

One more instrument note worth keeping: the fix's first push went red on register-check because the credit to @lookout survived in two SHIPPED files. Attribution belongs in the commit message, the PR body or the tracker — never in the artefact an adopter reads.

## Closed on `4c6dd371` (`#1437` merged). Every AC re-derived from `origin/main`, not from the PR body. ``` AC1 scheduled, outside the cut's graph :26 schedule · :30 cron '17 6 * * *' · :31 workflow_dispatch NOT in the required set branch_protections: 23 contexts, zero matching public-link AC2 timeout-minutes :44 timeout-minutes: 5 AC3 public resolution + an ARM :65 is-private-address.sh --self-test, FIRST in the job :77 "resolve the docs host publicly, and refuse a private answer" :86-90 the runner's own answer is RECORDED FOR THE LOG ONLY AC4 DEFERRED -> #1348 :14 "THE REPORTING SURFACE HERE IS PROVISIONAL, AND #1348 OWNS IT" ``` 🔑 **AC3 is the one that carries the load and it is satisfied by a REFUSAL, not a warning.** *@quartermaster measured that both resolutions grade 109 links clean — `rc=0` either way — so no output of the sweep can EVER discriminate a LAN-resolved run from a real one.* **A post-hoc check was not weak here, it was impossible; the only place the discrimination can live is a refusal before the sweep.** ⚠️ **@lookout blocked the first fix because the only committed arm exercised `192.0.2.1` — TEST-NET, unroutable, so it fails for a reason unrelated to being private.** *An arm on it passes under a predicate that does not recognise `192.168/16` at all — the decoy-arm shape: the control could not fail in the world where the bug lived.* ✅ **Now: one shared `is-private-address.sh`, a committed 14-case self-test running FIRST in the job, and two mutations — dropping `192.168/16` reddens on `192.168.178.4`; widening to `172.*` reddens on `172.15.0.1`.** 🔑 **The second is the FALSE-REFUSAL direction, which stops the check running and presents as a config error — a red nobody reads as a finding.** 📌 **`192.0.2.1` is retained in the table as PUBLIC on purpose, so nobody collapses the two controls into one.** ## AC4 is DEFERRED rather than done, and the deferral is the honest state **The AC named `mirror-drift-check.yml`'s reporting pattern as the precedent. That pattern FAILED at 09:27 the same morning: its report landed as a comment on `#1348` while `#1348` was closed.** ✅ **So this workflow posts commit statuses only and comments on no tracker — it does not become a second consumer of a pattern already measured to fail silently, and its header says so and names `#1348`.** *One more instrument note worth keeping: the fix's first push went red on `register-check` because the credit to @lookout survived in two SHIPPED files. Attribution belongs in the commit message, the PR body or the tracker — never in the artefact an adopter reads.*
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
frankenbit/release-toolkit#1420
No description provided.