Check the deployed apps against the roster daily and ticket the drift #11

Merged
geusebio merged 1 commit from feature/brandkit-conformance-ci into main 2026-08-09 10:00:16 +00:00
Owner

Nothing enforced that the apps serving the grey.ooo chrome were serving the current one. A consumer pinned to an older grey-ooo/brand-kit keeps rendering the roster as it was the day it deployed, and looks entirely healthy doing so — so the estate had quietly drifted apart.

This adds a daily job that drives a browser at every app in GreyOooApps::ALL, screenshots it, checks what it renders against this repo's roster and --gooo-* tokens, and keeps one ticket open here per app that has fallen behind.

What it finds today

Run against production from this branch:

App Mode State
printshop.grey.ooo bundle conforms
grey.video injected conforms
stuff.grey.ooo bundle missing code; Projects still points at projects.grey.ooo
someones.computer bundle missing grey.video and code; Niall.chat still href="#"
niall.chat bundle missing Projects, grey.video, code; Niall.chat still href="#"
git.grey.ooo injected no chrome at all — the bar.js injection was never extended to it

Each staleness is datable against this repo's own history: niall.chat predates 4b25aed, someones.computer predates 494be0b, stuff.grey.ooo predates 731500e.

How it works

file role
tools/conformance/targets.mjs which repo to blame per roster key, and how the chrome gets there
tools/conformance/audit.mjs drives Chromium at each app, screenshots, checks
tools/conformance/report.mjs reconciles the result against the issue tracker

A browser is not optional: grey.video's bar is injected client-side from brandkit.grey.ooo/bar.js, so the HTML on the wire carries no chrome and a curl-based check would report every proxied host as unbranded.

The checker reads the roster by require-ing GreyOooApps.php and the tokens out of brand-kit.css, rather than keeping copies. That is the point — a stale hand-written mirror of the roster is the exact bug being hunted (bar.js keeps one, by necessity, and says so).

Tickets are filed here, not on each app's repo, because this is where the roster lives and where the whole estate is visible at once; each one names and links the repo the fix belongs in and says whether that fix is a composer update or a change under Infrastructure's services/brandkit/.

Two properties worth preserving

Red means "did not find out", not "found something". Only an unreachable app, a dead browser, or a targets.mjs/roster mismatch fails the run. Drift exits 0 and reports itself as tickets, because drift is fixed by someone else redeploying on their own schedule — a scheduled job that stays red until they do stops being read within a week.

Findings are confirmed before they are believed. Any app with failures is visited twice and only failures present in both are reported, intersected on a stable check id rather than on the rendered message. PrintShop showed why this matters and why a second visit alone is not enough: it serves its assets through PHP with the profiler on (note its _wdt route), so its stylesheet can still be in flight at measurement time. That measures identically to a 404 — auto-height bar, every --gooo-* token empty, nothing logged as an error — and it reproduces across visits. Measurements are therefore gated on the page reaching load, and an app that does not get there is recorded as unreachable rather than broken. Without that, the first run would have filed a false ticket against a healthy app.

Verification

  • Full workflow run end to end under act in the pinned Playwright container (/ci-local): PHP install, npm ci, audit against the live sites, reporter, artifact upload — job succeeded.
  • report.mjs exercised in dry run; --preview renders the exact ticket body it would file.
  • Both guard paths tested: a >6h-old summary.json refuses to file (exit 2); a targets.mjs/roster mismatch aborts before checking anything (exit 2).
  • composer validate --strict --no-check-publish passes.

A subagent review of the harness caught four real bugs before this landed, all fixed here: a close path that could re-comment indefinitely if the close failed, an asset sweep that would permanently break the images in a closed ticket, a pagination short-circuit that could file duplicate tickets nightly on an instance with a lower MAX_RESPONSE_ITEMS, and a confirmation pass that intersected on prose rather than check ids — that last could have closed a legitimate ticket.

Notes for review

  • No pull_request trigger, by design — this workflow writes to the issue tracker, so it must not run per-PR. It is schedule (05:43 daily) plus workflow_dispatch, which also keeps it off the single shared runner during working hours. It follows that CI is not green on this PR because there is nothing here for CI to run; act is the gate instead.
  • php-cli is installed into a Node container. Deliberate, and the alternative is worse: it buys the roster being read from its source instead of duplicated. The house Alpine act-runner image cannot be used because Playwright does not support Alpine.
  • New repo secret is optional. FORGEJO_TOKEN falls back to github.token; set BRANDKIT_BOT_TOKEN if that turns out to lack issue or attachment write scope. Without either, the reporter prints its intent and changes nothing.
  • git.grey.ooo's ticket is expected on the first run and is not a false positive — the bar.js injection genuinely has not been applied to it yet.
Nothing enforced that the apps serving the grey.ooo chrome were serving the *current* one. A consumer pinned to an older `grey-ooo/brand-kit` keeps rendering the roster as it was the day it deployed, and looks entirely healthy doing so — so the estate had quietly drifted apart. This adds a daily job that drives a browser at every app in `GreyOooApps::ALL`, screenshots it, checks what it renders against this repo's roster and `--gooo-*` tokens, and keeps one ticket open here per app that has fallen behind. ## What it finds today Run against production from this branch: | App | Mode | State | | --- | --- | --- | | printshop.grey.ooo | bundle | conforms | | grey.video | injected | conforms | | stuff.grey.ooo | bundle | missing `code`; Projects still points at `projects.grey.ooo` | | someones.computer | bundle | missing `grey.video` and `code`; Niall.chat still `href="#"` | | niall.chat | bundle | missing `Projects`, `grey.video`, `code`; Niall.chat still `href="#"` | | git.grey.ooo | injected | no chrome at all — the `bar.js` injection was never extended to it | Each staleness is datable against this repo's own history: niall.chat predates 4b25aed, someones.computer predates 494be0b, stuff.grey.ooo predates 731500e. ## How it works | file | role | | --- | --- | | `tools/conformance/targets.mjs` | which repo to blame per roster key, and how the chrome gets there | | `tools/conformance/audit.mjs` | drives Chromium at each app, screenshots, checks | | `tools/conformance/report.mjs` | reconciles the result against the issue tracker | A browser is not optional: grey.video's bar is injected client-side from `brandkit.grey.ooo/bar.js`, so the HTML on the wire carries no chrome and a `curl`-based check would report every proxied host as unbranded. The checker reads the roster by `require`-ing `GreyOooApps.php` and the tokens out of `brand-kit.css`, rather than keeping copies. That is the point — a stale hand-written mirror of the roster is the exact bug being hunted (`bar.js` keeps one, by necessity, and says so). Tickets are filed **here**, not on each app's repo, because this is where the roster lives and where the whole estate is visible at once; each one names and links the repo the fix belongs in and says whether that fix is a `composer update` or a change under Infrastructure's `services/brandkit/`. ## Two properties worth preserving **Red means "did not find out", not "found something".** Only an unreachable app, a dead browser, or a `targets.mjs`/roster mismatch fails the run. Drift exits 0 and reports itself as tickets, because drift is fixed by someone else redeploying on their own schedule — a scheduled job that stays red until they do stops being read within a week. **Findings are confirmed before they are believed.** Any app with failures is visited twice and only failures present in both are reported, intersected on a stable check id rather than on the rendered message. PrintShop showed why this matters and why a second visit alone is not enough: it serves its assets through PHP with the profiler on (note its `_wdt` route), so its stylesheet can still be in flight at measurement time. That measures *identically* to a 404 — auto-height bar, every `--gooo-*` token empty, nothing logged as an error — and it reproduces across visits. Measurements are therefore gated on the page reaching `load`, and an app that does not get there is recorded as unreachable rather than broken. Without that, the first run would have filed a false ticket against a healthy app. ## Verification - Full workflow run end to end under `act` in the pinned Playwright container (`/ci-local`): PHP install, `npm ci`, audit against the live sites, reporter, artifact upload — job succeeded. - `report.mjs` exercised in dry run; `--preview` renders the exact ticket body it would file. - Both guard paths tested: a >6h-old `summary.json` refuses to file (exit 2); a `targets.mjs`/roster mismatch aborts before checking anything (exit 2). - `composer validate --strict --no-check-publish` passes. A subagent review of the harness caught four real bugs before this landed, all fixed here: a close path that could re-comment indefinitely if the close failed, an asset sweep that would permanently break the images in a closed ticket, a pagination short-circuit that could file duplicate tickets nightly on an instance with a lower `MAX_RESPONSE_ITEMS`, and a confirmation pass that intersected on prose rather than check ids — that last could have *closed* a legitimate ticket. ## Notes for review - **No `pull_request` trigger, by design** — this workflow writes to the issue tracker, so it must not run per-PR. It is `schedule` (05:43 daily) plus `workflow_dispatch`, which also keeps it off the single shared runner during working hours. It follows that CI is not green on this PR because there is nothing here for CI to run; `act` is the gate instead. - **`php-cli` is installed into a Node container.** Deliberate, and the alternative is worse: it buys the roster being read from its source instead of duplicated. The house Alpine act-runner image cannot be used because Playwright does not support Alpine. - **New repo secret is optional.** `FORGEJO_TOKEN` falls back to `github.token`; set `BRANDKIT_BOT_TOKEN` if that turns out to lack issue or attachment write scope. Without either, the reporter prints its intent and changes nothing. - `git.grey.ooo`'s ticket is expected on the first run and is not a false positive — the `bar.js` injection genuinely has not been applied to it yet.
Nothing enforced that the apps actually serving the grey.ooo chrome were
serving the *current* one. A consumer pinned to an older grey-ooo/brand-kit
keeps rendering the roster as it was the day it deployed and looks entirely
healthy doing so, so the estate had drifted apart unnoticed: of the six apps
in GreyOooApps::ALL, only printshop.grey.ooo and grey.video render the roster
this repo defines. someones.computer is missing two entries and still points
Niall.chat at `#`; stuff.grey.ooo is missing one and still points Projects at
projects.grey.ooo; niall.chat is missing three; git.grey.ooo has no chrome at
all, the bar.js injection never having been extended to it.

A browser is not optional here. grey.video's bar is injected client-side from
brandkit.grey.ooo/bar.js, so the HTML on the wire carries no chrome and a
fetch-based check would report every proxied host as unbranded.

tools/conformance drives Chromium at each app, screenshots it, and compares
what it renders against GreyOooApps::ALL and the --gooo-* tokens in
brand-kit.css — both read at run time rather than copied, since a stale
hand-written mirror of the roster is the exact bug being hunted. report.mjs
keeps one open issue per app here, rewritten in place and closed when the app
comes good; each names and links the repo the fix belongs in and says whether
that fix is a composer bump or a change under Infrastructure services/brandkit.

Two properties are load-bearing:

- Red means "did not find out", not "found something". Only an unreachable
  app, a dead browser or a targets.mjs/roster mismatch fails the run. Drift
  exits 0 and reports itself as tickets, because drift is fixed by someone
  else redeploying on their own schedule, and a job that stays red until they
  do stops being read.
- Failures are confirmed by a second visit before they are believed, and
  measurements are only taken once the page reaches `load`. PrintShop proved
  why: it serves assets through PHP with the profiler enabled, so its
  stylesheet can still be in flight at measurement time, which measures
  identically to a 404 — auto-height bar, every token empty, nothing logged as
  an error — and reproduces across visits. That would have filed a false
  ticket against a healthy app.

Runs daily at 05:43 and on workflow_dispatch. Verified end to end with act,
including the two guard paths (stale summary, roster/targets mismatch); the
workflow has no pull_request trigger by design, since it writes to the tracker.
Sign in to join this conversation.
No reviewers
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
Grey.ooo/symfony-brand-kit!11
No description provided.