feat(ci): web-gate bouwt de webbundel op een PR die hem kan breken (#1888-staart) #1905

Merged
brenno merged 3 commits from ci/web-gate into main 2026-08-31 23:57:19 +00:00
Owner

Wat dit toevoegt

make check-web is de enige controle die naar een gebouwde webbundel kijkt. Hij draaide op twee plekken: lokaal in check-full, en in release.yml op een v*-tag. Tussen die twee bouwde niets de web.

Dat is het gat waar #1888 doorheen viel. De dotfile-opruiming daar hield als "bewust meegenomen" alleen aan wat releaseArtefacten noemt — een lijst die beschrijft wat de inpakstap er zelf bij legt, niet wat de bundel bevat. De twee dotfiles die er wél horen komen uit web/ en worden door flutter build web gekopieerd. Weg waren build/web/.htaccess (#849) en build/web/.well-known/security.txt (RFC 9116). De eerste machine die dat merkte was fase 1 van de v0.5.0-release, weken na de merge. Alle poorten stonden intussen groen.

De reparatie zelf zit in #1903. Dit is het tweede stuk: de bouw naar links schuiven.

De tegenproef

web-gate zou zijn afgegaan op de PR van #1888 zelf — die raakt tool/pack_web_release.dart, dat in het padfilter staat — en make check-web was daar rood geworden op de ontbrekende .htaccess. Vóór de merge, niet weken later.

$ git diff --name-only 25aba3288^1 25aba3288 | grep -E '<padfilter>'
tool/pack_web_release.dart

Wat het kost

Over de laatste 200 merges naar main zou hij 31 keer zijn afgegaan (15%, ongeveer 1 op 7). Per ingang:

ingang merges
pubspec.yaml 18 een plugin zonder web-implementatie breekt pas bij het bouwen
Makefile 10 houdt de hardingsvlaggen (--no-web-resources-cdn, --csp)
pubspec.lock 10 de opgeloste afhankelijkheden
tool/pack_web_release.dart 3 de inpakstap — hier ging #1888 mis
tool/check_web_hardening.dart 1 de poort zelf
.tool-versions 1 andere compiler, andere web-engine, andere loader
web/** 0 de bron die letterlijk gekopieerd wordt
tool/check_bundled_docs_fresh.dart 0 het derde been van check-web
.forgejo/ci-image/** 0 het image waarin dit draait

docs/** staat er bewust niet op: de gebundelde-docs-controle bestaat voor incrementele builds, en CI bouwt altijd schoon.

Bewust géén vereiste check

Branch-bescherming wacht op elke context die zij eist. Een vereiste check met een padfilter meldt zich niet op een PR die die paden niet raakt, en dan hangt die PR eeuwig pending. web-gate gaat dus niet in status_check_contexts; static-gate en scans blijven de vereiste, filterloze poorten. Rood is hier in de praktijk nog steeds blokkerend — je merget er niet overheen — het is alleen niet het mechanisme waar de bescherming op wacht. Dat staat in de kop van de workflow én in docs/CHECKS.md, want het is precies het soort ding dat iemand over een jaar "netjes" wil aanzetten.

Het padfilter is zelf bewaakt

Een poort die niet afgaat bewaakt niets — zo viel .tool-versions ooit uit het filter van linux-build.yml. test/web_gate_triggers_test.dart ontleedt de workflow en eist per trekker dat elke ingang op de lijst staat, met de reden erbij, plus dat de twee lijsten niet uiteenlopen.

Dat "per trekker" is geen netheid. Mijn eerste versie deed tekst.contains(...) over het hele bestand, en die overleefde de mutatie: een pad weghalen uit alleen het pull_request-filter bleef groen omdat het bij push nog stond. Zeven mutaties na de herschrijving, zeven keer rood:

mutatie
pad alleen uit het PR-filter rood
pad alleen uit het push-filter rood
web/** uit beide filters rood
padfilter bij push weg rood
eigen stappen i.p.v. make check-web rood
kaal ubuntu i.p.v. het CI-image rood
pull_request-trekker weg rood

Herstel telkens uit een kopie, niet met git checkout; het bestand is na afloop byte-identiek.

En dezelfde fout die #1888 maakte, één laag hoger

test/dartcv_cache_key_test.dart had een handgepinde lijst van drie workflows. Mijn nieuwe workflow gebruikt diezelfde cache, dus ik kon er een vierde regel bij zetten — en dan staat de volgende er weer buiten. Dat is letterlijk de faalvorm van #1888: een lijst die het domein beschrijft maar niet afleidt.

De lijst wordt nu afgeleid uit de workflowmap. Een goedkoop tekstaftasten bepaalt het domein, het ontleden doet de controle, en dat die twee gelijk blijven is zelf een test — anders slaagt de invariant vacuüm zodra de ontleder stil een bestand laat vallen. Er staat ook een isNotEmpty-regel onder: zonder die regel slaagt de hele suite zodra de afleiding niets meer vindt.

Bewijs dat de afleiding de nieuwe workflow écht dekt: restore-keys terugzetten in web-gate's cachestap maakt de test rood op precies die workflow, zonder dat er een regel is bijgezet.

Toetsing

  • Test eerst rood: alle 8 (nu 11) tests vielen tegen de nog niet bestaande workflow.
  • 7 mutaties op de workflow, 7 keer rood; 1 mutatie op de afgeleide cachelijst, rood op de nieuwe workflow.
  • Alle 49 interne ankerlinks in docs/CHECKS.md lossen op, inclusief de nieuwe.
  • make check groen.
  • Smoke-run: deze PR raakt .forgejo/workflows/web-gate.yml, dat in zijn eigen padfilter staat — de poort gaat dus af op zijn eigen PR. Dat is de enige echte proef dat Forgejo de workflow accepteert en make check-web in dat image draait; ik merge pas als die run groen is.

Bewaker

Raakt geen formaat, geen opslag, geen afhankelijkheid. Uitgaand verkeer: geen nieuwe partij — het CI-image, actions/checkout@v4/actions/cache@v4 en de Flutter-webengine-download waren al in gebruik in static-gate.yml en release.yml.

Wat het wél raakt is een publieke belofte in de documentatie: docs/CHECKS.md beweert vanaf nu dat de webbundel per PR gebouwd en gecontroleerd wordt. Die claim is precies zoveel waard als de workflow echt draait, en een claim over een poort is niet zelf gepoort — dat is de blinde vlek uit #1886–#1895. Daarom hangt het merge-moment aan de smoke-run hierboven en niet aan mijn lezing van de YAML. De richting is verder herstellend: er wordt een controle toegevoegd, niets versoepeld, en de reden waarom deze poort géén vereiste check is staat opgeschreven in plaats van stilzwijgend te blijven.

Co-Authored-By: Claude Opus 5 noreply@anthropic.com

## Wat dit toevoegt `make check-web` is de enige controle die naar een **gebouwde** webbundel kijkt. Hij draaide op twee plekken: lokaal in `check-full`, en in `release.yml` op een `v*`-tag. Tussen die twee bouwde niets de web. Dat is het gat waar #1888 doorheen viel. De dotfile-opruiming daar hield als "bewust meegenomen" alleen aan wat `releaseArtefacten` noemt — een lijst die beschrijft wat de inpakstap er *zelf bij legt*, niet wat de bundel *bevat*. De twee dotfiles die er wél horen komen uit `web/` en worden door `flutter build web` gekopieerd. Weg waren `build/web/.htaccess` (#849) en `build/web/.well-known/security.txt` (RFC 9116). De eerste machine die dat merkte was fase 1 van de v0.5.0-release, weken na de merge. Alle poorten stonden intussen groen. De reparatie zelf zit in #1903. Dit is het tweede stuk: de bouw naar links schuiven. ## De tegenproef `web-gate` zou zijn afgegaan op de PR van #1888 zelf — die raakt `tool/pack_web_release.dart`, dat in het padfilter staat — en `make check-web` was daar rood geworden op de ontbrekende `.htaccess`. Vóór de merge, niet weken later. ``` $ git diff --name-only 25aba3288^1 25aba3288 | grep -E '<padfilter>' tool/pack_web_release.dart ``` ## Wat het kost Over de laatste 200 merges naar `main` zou hij **31 keer** zijn afgegaan (15%, ongeveer 1 op 7). Per ingang: | ingang | merges | | | --- | ---: | --- | | `pubspec.yaml` | 18 | een plugin zonder web-implementatie breekt pas bij het bouwen | | `Makefile` | 10 | houdt de hardingsvlaggen (`--no-web-resources-cdn`, `--csp`) | | `pubspec.lock` | 10 | de opgeloste afhankelijkheden | | `tool/pack_web_release.dart` | 3 | de inpakstap — hier ging #1888 mis | | `tool/check_web_hardening.dart` | 1 | de poort zelf | | `.tool-versions` | 1 | andere compiler, andere web-engine, andere loader | | `web/**` | 0 | de bron die letterlijk gekopieerd wordt | | `tool/check_bundled_docs_fresh.dart` | 0 | het derde been van `check-web` | | `.forgejo/ci-image/**` | 0 | het image waarin dit draait | `docs/**` staat er bewust **niet** op: de gebundelde-docs-controle bestaat voor *incrementele* builds, en CI bouwt altijd schoon. ## Bewust géén vereiste check Branch-bescherming wacht op elke context die zij eist. Een vereiste check met een padfilter meldt zich niet op een PR die die paden niet raakt, en dan hangt die PR eeuwig `pending`. `web-gate` gaat dus **niet** in `status_check_contexts`; `static-gate` en `scans` blijven de vereiste, filterloze poorten. Rood is hier in de praktijk nog steeds blokkerend — je merget er niet overheen — het is alleen niet het mechanisme waar de bescherming op wacht. Dat staat in de kop van de workflow én in `docs/CHECKS.md`, want het is precies het soort ding dat iemand over een jaar "netjes" wil aanzetten. ## Het padfilter is zelf bewaakt Een poort die niet afgaat bewaakt niets — zo viel `.tool-versions` ooit uit het filter van `linux-build.yml`. `test/web_gate_triggers_test.dart` ontleedt de workflow en eist **per trekker** dat elke ingang op de lijst staat, met de reden erbij, plus dat de twee lijsten niet uiteenlopen. Dat "per trekker" is geen netheid. Mijn eerste versie deed `tekst.contains(...)` over het hele bestand, en die **overleefde de mutatie**: een pad weghalen uit alleen het `pull_request`-filter bleef groen omdat het bij `push` nog stond. Zeven mutaties na de herschrijving, zeven keer rood: | mutatie | | | --- | --- | | pad alleen uit het PR-filter | rood | | pad alleen uit het push-filter | rood | | `web/**` uit beide filters | rood | | padfilter bij `push` weg | rood | | eigen stappen i.p.v. `make check-web` | rood | | kaal ubuntu i.p.v. het CI-image | rood | | `pull_request`-trekker weg | rood | Herstel telkens uit een kopie, niet met `git checkout`; het bestand is na afloop byte-identiek. ## En dezelfde fout die #1888 maakte, één laag hoger `test/dartcv_cache_key_test.dart` had een **handgepinde** lijst van drie workflows. Mijn nieuwe workflow gebruikt diezelfde cache, dus ik kon er een vierde regel bij zetten — en dan staat de volgende er weer buiten. Dat is letterlijk de faalvorm van #1888: een lijst die het domein beschrijft maar niet afleidt. De lijst wordt nu afgeleid uit de workflowmap. Een goedkoop tekstaftasten bepaalt het domein, het ontleden doet de controle, en dat die twee gelijk blijven is zelf een test — anders slaagt de invariant vacuüm zodra de ontleder stil een bestand laat vallen. Er staat ook een `isNotEmpty`-regel onder: zonder die regel slaagt de hele suite zodra de afleiding niets meer vindt. Bewijs dat de afleiding de nieuwe workflow écht dekt: `restore-keys` terugzetten in `web-gate`'s cachestap maakt de test rood op precies die workflow, zonder dat er een regel is bijgezet. ## Toetsing - Test eerst rood: alle 8 (nu 11) tests vielen tegen de nog niet bestaande workflow. - 7 mutaties op de workflow, 7 keer rood; 1 mutatie op de afgeleide cachelijst, rood op de nieuwe workflow. - Alle 49 interne ankerlinks in `docs/CHECKS.md` lossen op, inclusief de nieuwe. - `make check` groen. - **Smoke-run:** deze PR raakt `.forgejo/workflows/web-gate.yml`, dat in zijn eigen padfilter staat — de poort gaat dus af op zijn eigen PR. Dat is de enige echte proef dat Forgejo de workflow accepteert en `make check-web` in dat image draait; ik merge pas als die run groen is. ## Bewaker Raakt geen formaat, geen opslag, geen afhankelijkheid. **Uitgaand verkeer:** geen nieuwe partij — het CI-image, `actions/checkout@v4`/`actions/cache@v4` en de Flutter-webengine-download waren al in gebruik in `static-gate.yml` en `release.yml`. Wat het wél raakt is een **publieke belofte in de documentatie**: `docs/CHECKS.md` beweert vanaf nu dat de webbundel per PR gebouwd en gecontroleerd wordt. Die claim is precies zoveel waard als de workflow echt draait, en een claim over een poort is niet zelf gepoort — dat is de blinde vlek uit #1886–#1895. Daarom hangt het merge-moment aan de smoke-run hierboven en niet aan mijn lezing van de YAML. De richting is verder herstellend: er wordt een controle toegevoegd, niets versoepeld, en de reden waarom deze poort géén vereiste check is staat opgeschreven in plaats van stilzwijgend te blijven. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
`make check-web` is de enige controle die naar een gebouwde bundel kijkt, en
draaide alleen lokaal in `check-full` en op een `v*`-tag. Tussen die twee bouwde
niets de web — het gat waardoor #1888 de release van v0.5.0 omgooide: de
dotfile-opruiming daar sloopte `.htaccess` en `.well-known/security.txt` uit de
bundel, en fase 1 van de releaseketen was weken later de eerste die het merkte.

Deze workflow draait dezelfde `make check-web` op het gepinde CI-image, maar
alleen op de invoeren die de bundel kunnen raken. Over de laatste 200 merges zou
hij 31 keer zijn afgegaan (15%) — waaronder de merge van #1888 zelf.

Bewust géén vereiste check: een vereiste context met een padfilter meldt zich
niet op een PR die die paden niet raakt en laat die eeuwig `pending` hangen.

`web_gate_triggers_test.dart` bewaakt het padfilter per trekker, met de reden per
ingang. Per trekker, niet over de bestandstekst: een eerste versie met
`contains` overleefde de mutatie waarbij een pad alleen uit het
`pull_request`-filter verdween en bij `push` bleef staan.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
De invariant uit #1748 stond over een handgeschreven lijst van drie workflows.
`web-gate.yml` gebruikt dezelfde cache, en er een vierde regel bij zetten laat de
vijfde er weer buiten vallen — letterlijk de faalvorm van #1888: een lijst die
het domein beschrijft in plaats van het af te leiden.

De lijst komt nu uit de workflowmap. Het goedkope tekstaftasten bepaalt het
domein, het ontleden doet de controle, en dat die twee gelijk blijven is zelf een
test — anders slaagt de invariant vacuüm zodra de ontleder stil een bestand laat
vallen. Een `isNotEmpty`-regel vangt het geval dat de afleiding niets meer vindt.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
docs(CHECKS): web-gate, en waarom hij geen vereiste check is
All checks were successful
scans / scans (pull_request) Successful in 4m35s
static-gate / static-gate (pull_request) Successful in 9m48s
web-gate / web-gate (pull_request) Successful in 10m48s
893c2abb18
Eigen sectie, matrixrij, voetnoot en een `conditional`-regel in de legenda: die
stond er nog niet, want dit is de eerste poort die per PR draait zonder vereist te
kunnen zijn. De reden staat erbij, want het is precies het soort ding dat iemand
over een jaar "netjes" wil aanzetten — waarna elke PR die de paden niet raakt
blijft hangen op een status die nooit komt.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
brenno force-pushed ci/web-gate from 893c2abb18
All checks were successful
scans / scans (pull_request) Successful in 4m35s
static-gate / static-gate (pull_request) Successful in 9m48s
web-gate / web-gate (pull_request) Successful in 10m48s
to cb97e5dc28
All checks were successful
scans / scans (pull_request) Successful in 3m56s
static-gate / static-gate (pull_request) Successful in 10m37s
web-gate / web-gate (pull_request) Successful in 10m34s
2026-08-31 23:43:55 +00:00
Compare
brenno merged commit 56aa107b9d into main 2026-08-31 23:57:19 +00:00
Sign in to join this conversation.
No description provided.