ci: validate generated markup against Gutenberg's own block validator #137

Merged
peterswimm merged 6 commits from ci/gutenberg-block-validation into main 2026-09-14 19:02:21 +00:00
Owner

#136 fixed five pages whose wrappers WordPress would not accept. This is the gate that would have caught them.

WordPress rebuilds a block's markup from its attributes and compares the two. When they disagree the editor offers "Attempt Block Recovery", and recovering in the Site Editor saves a copy into the database that then shadows the theme's own file — which is how /news/ ended up rendering from an override. A mismatch forks the page; it does not stay an editor warning.

Nothing here could have caught it. copy_coverage counts text, forgeblock_validate checks the ForgeBlock contract, and neither knows what WordPress does with a block's attributes. This asks WordPress's own packages.

Scope

  • .forgejo/block-validation/validate-blocks.cjs — parses markup with @wordpress/blocks, registers the core blocks, and registers every toilville/* block from the plugin's real block.json, so it compares against what the site registers. Each block's save shape is read off its index.js rather than kept in a list here that would drift.
  • .forgejo/workflows/block-validation.yml — runs it twice:
    • what this repo generates (the brand previews, the five theme patterns, the templates and the site's header and footer): must be clean, or the job fails.
    • what came with the third-party theme: reported only. 47 blocks today — invalid core/cover and blocks the Getwid plugins own. Printed so the number stays visible instead of growing quietly.
  • .npmrc pins legacy-peer-deps: npm's strict resolver spends minutes backtracking over the @wordpress peer ranges and then rejects them. npm ci reads the file too, so CI cannot forget it.
  • Lives under .forgejo/ so the JS stays with CI rather than landing in the repo proper.

Testing

  • Against main as it stands: the 31 generated files validate clean; the inherited patterns report 47 blocks and exit 0.
  • Failure path checked by putting the #136 bug back — stripping the style from one generated pattern. The job fails and names the file, the block and the attribute:

    core/group is invalid — Expected attributes [["class","wp-block-group alignfull"],["style","padding-top:var(--wp--preset--spacing--lg);…"]], instead saw [["class","wp-block-group alignfull"]]

  • npm ci from clean: 3s, 324 packages.
  • test_workflow_invariants.py passes on the new workflow (57 tests); yamllint clean; the full suite is 327 passing.

Impact Statement

CI only — no change to any generated artifact, the theme, or the site. Adds one job to pull requests, roughly the cost of npm ci plus a parse of 31 files. It can fail a PR that would otherwise merge, which is the point: the failure it catches is one that reaches readers as a recovery prompt instead of a page.

One caveat, in the README too: these packages track Gutenberg's releases while the site runs the blocks bundled with its WordPress. They agree about the mismatch this exists to catch, but core blocks gain deprecations over time, so a finding against an inherited block may reflect a newer core than the site runs. That is part of why the inherited set only reports.

#136 fixed five pages whose wrappers WordPress would not accept. This is the gate that would have caught them. WordPress rebuilds a block's markup from its attributes and compares the two. When they disagree the editor offers "Attempt Block Recovery", and recovering in the Site Editor saves a copy into the database that then shadows the theme's own file — which is how `/news/` ended up rendering from an override. A mismatch forks the page; it does not stay an editor warning. Nothing here could have caught it. `copy_coverage` counts text, `forgeblock_validate` checks the ForgeBlock contract, and neither knows what WordPress does with a block's attributes. This asks WordPress's own packages. ## Scope - `.forgejo/block-validation/validate-blocks.cjs` — parses markup with `@wordpress/blocks`, registers the core blocks, and registers every `toilville/*` block from the plugin's real `block.json`, so it compares against what the site registers. Each block's save shape is read off its `index.js` rather than kept in a list here that would drift. - `.forgejo/workflows/block-validation.yml` — runs it twice: - **what this repo generates** (the brand previews, the five theme patterns, the templates and the site's header and footer): must be clean, or the job fails. - **what came with the third-party theme**: reported only. 47 blocks today — invalid `core/cover` and blocks the Getwid plugins own. Printed so the number stays visible instead of growing quietly. - `.npmrc` pins `legacy-peer-deps`: npm's strict resolver spends minutes backtracking over the `@wordpress` peer ranges and then rejects them. `npm ci` reads the file too, so CI cannot forget it. - Lives under `.forgejo/` so the JS stays with CI rather than landing in the repo proper. ## Testing - Against `main` as it stands: the 31 generated files validate clean; the inherited patterns report 47 blocks and exit 0. - Failure path checked by putting the #136 bug back — stripping the `style` from one generated pattern. The job fails and names the file, the block and the attribute: > `core/group` is invalid — Expected attributes `[["class","wp-block-group alignfull"],["style","padding-top:var(--wp--preset--spacing--lg);…"]]`, instead saw `[["class","wp-block-group alignfull"]]` - `npm ci` from clean: 3s, 324 packages. - `test_workflow_invariants.py` passes on the new workflow (57 tests); `yamllint` clean; the full suite is 327 passing. ## Impact Statement CI only — no change to any generated artifact, the theme, or the site. Adds one job to pull requests, roughly the cost of `npm ci` plus a parse of 31 files. It can fail a PR that would otherwise merge, which is the point: the failure it catches is one that reaches readers as a recovery prompt instead of a page. One caveat, in the README too: these packages track Gutenberg's releases while the site runs the blocks bundled with its WordPress. They agree about the mismatch this exists to catch, but core blocks gain deprecations over time, so a finding against an *inherited* block may reflect a newer core than the site runs. That is part of why the inherited set only reports.
ci: validate generated markup against Gutenberg's own block validator
Some checks failed
PR policy check / policy-gate (pull_request) Successful in 1s
Block Validation / validate (pull_request) Failing after 28s
Security Scans / Trivy filesystem scan (pull_request) Successful in 45s
Security Scans / Gitleaks secret scan (pull_request) Successful in 37s
Test Generator / test (pull_request) Failing after 20s
Test Generator / lint-workflows (pull_request) Successful in 18s
Test Generator / lint-prose (pull_request) Successful in 22s
UDG dist drift gate / dist/ matches dtcg + udg sources (pull_request) Successful in 15s
Copy coverage / Design copy reaching rendered HTML (pull_request) Failing after 26s
19ede2c7f7
WordPress rebuilds a block's markup from its attributes and compares the
two. When they disagree the editor offers "Attempt Block Recovery", and
recovering in the Site Editor saves a copy into the database that then
shadows the theme's own file — so a mismatch forks the page rather than
just warning about it. #136 shipped five such pages.

Nothing here could have caught that: copy_coverage counts text,
forgeblock_validate checks the ForgeBlock contract, and neither knows
what WordPress does with a block's attributes. This asks WordPress's own
packages, registering every toilville/* block from the plugin's real
block.json so it compares against what the site registers.

The job holds what this repo generates to a clean bill, and separately
reports the patterns inherited with the third-party theme — 47 blocks
today, invalid core/cover and blocks the Getwid plugins own — so that
number stays visible instead of growing quietly.

npm's strict peer resolver spends minutes backtracking over the
@wordpress ranges and then rejects them, so .npmrc pins legacy-peer-deps
where `npm ci` will read it too.
ci: say which secret is missing when the blocks checkout fails
Some checks failed
PR policy check / policy-gate (pull_request) Successful in 0s
Block Validation / validate (pull_request) Failing after 19s
Security Scans / Trivy filesystem scan (pull_request) Successful in 36s
Security Scans / Gitleaks secret scan (pull_request) Successful in 30s
Test Generator / test (pull_request) Failing after 19s
Test Generator / lint-workflows (pull_request) Successful in 15s
Test Generator / lint-prose (pull_request) Successful in 19s
Copy coverage / Design copy reaching rendered HTML (pull_request) Failing after 22s
UDG dist drift gate / dist/ matches dtcg + udg sources (pull_request) Successful in 14s
b68d356cc8
Toilville_Rituals is private, so the checkout needs CI_TOKEN — and that
secret is not set on this repo. udg-copy-coverage.yml and
test-generator.yml carry the same step and have failed on every run since
at least 2026-09-11 with "Input required and not supplied: token", which
names neither the secret nor the repository.

Let the checkout fail soft and check for the blocks directly, so the log
says what is missing and what to set.
ci: name CI_TOKEN as text, since Forgejo expands the expression anyway
Some checks failed
Block Validation / validate (pull_request) Failing after 1s
PR policy check / policy-gate (pull_request) Successful in 1s
Test Generator / test (pull_request) Failing after 22s
Test Generator / lint-workflows (pull_request) Successful in 24s
Security Scans / Gitleaks secret scan (pull_request) Successful in 27s
Security Scans / Trivy filesystem scan (pull_request) Successful in 32s
UDG dist drift gate / dist/ matches dtcg + udg sources (pull_request) Successful in 14s
Test Generator / lint-prose (pull_request) Successful in 19s
Copy coverage / Design copy reaching rendered HTML (pull_request) Failing after 22s
8fabf27eac
ci: check the blocks out with FORGEJO_TOKEN, the org secret that exists
Some checks failed
Block Validation / validate (pull_request) Failing after 0s
PR policy check / policy-gate (pull_request) Successful in 0s
Test Generator / test (pull_request) Failing after 23s
Test Generator / lint-workflows (pull_request) Successful in 25s
Security Scans / Gitleaks secret scan (pull_request) Successful in 27s
Security Scans / Trivy filesystem scan (pull_request) Successful in 31s
UDG dist drift gate / dist/ matches dtcg + udg sources (pull_request) Successful in 15s
Test Generator / lint-prose (pull_request) Successful in 24s
Copy coverage / Design copy reaching rendered HTML (pull_request) Failing after 22s
65fbd88c91
CI_TOKEN is set on neither toilville-design nor Toilville_Rituals — both
repo secret lists are empty — so every job asking for it fails at
checkout. The Rituals workflows run on FORGEJO_TOKEN, an org-level secret
visible to every repo in the org, this one included. Use that instead of
asking for a credential nobody created.
ci: keep an expression out of the comment that explains expressions
Some checks failed
PR policy check / policy-gate (pull_request) Successful in 1s
Test Generator / test (pull_request) Failing after 18s
Security Scans / Gitleaks secret scan (pull_request) Successful in 31s
Security Scans / Trivy filesystem scan (pull_request) Successful in 34s
Test Generator / lint-workflows (pull_request) Successful in 16s
Test Generator / lint-prose (pull_request) Successful in 18s
UDG dist drift gate / dist/ matches dtcg + udg sources (pull_request) Successful in 14s
Block Validation / validate (pull_request) Failing after 52s
Copy coverage / Design copy reaching rendered HTML (pull_request) Failing after 22s
a8a5ff54d5
Forgejo parses ${{ }} wherever it appears in a run block, comments
included, and an empty one fails to parse — which took the whole workflow
with it (schema validation error, no step run). yamllint and the workflow
invariants both pass on it; only Forgejo's parser objects.
ci: pin the sibling checkout to main
Some checks failed
PR policy check / policy-gate (pull_request) Successful in 0s
Security Scans / Trivy filesystem scan (pull_request) Successful in 33s
Block Validation / validate (pull_request) Failing after 48s
Security Scans / Gitleaks secret scan (pull_request) Successful in 26s
Test Generator / test (pull_request) Failing after 17s
Test Generator / lint-workflows (pull_request) Successful in 15s
Test Generator / lint-prose (pull_request) Successful in 19s
UDG dist drift gate / dist/ matches dtcg + udg sources (pull_request) Successful in 15s
Copy coverage / Design copy reaching rendered HTML (pull_request) Failing after 23s
da6579cd13
Without a ref, the runner resolves the sibling repository at this
workflow's own ref — a branch that exists only here — and checkout reports
"The target couldn't be found".
peterswimm deleted branch ci/gutenberg-block-validation 2026-09-14 19:02:21 +00:00
Sign in to join this conversation.
No description provided.