Skip to content

ci: build the bgfx examples from their pins, and run the shader pipeline e2e (#732, #735) - #745

Merged
apotema merged 1 commit into
mainfrom
fix/732-735-ci-coverage
Sep 18, 2026
Merged

apotema merged 1 commit into
mainfrom
fix/732-735-ci-coverage

Conversation

@apotema

@apotema apotema commented Sep 18, 2026 •

Copy link
Copy Markdown
Contributor

Grouped, not split: both issues rewire the same lanes of .github/workflows/ci.yml (the examples lane and the material lane), and the #735 job reuses the pinned/scratch-LABELLE_HOME scaffolding introduced for #732. As two PRs they would conflict on merge. #741 is in its own PR (#743).


#732 — CI generated the bgfx examples but never built them

The defect (both halves)

It hides breaks. generate proves the manifest parsed. It compiles no backend source, so every defect that lives in a dependency — an exhaustive switch, a moved contract type, a raised floor — passes. That is how labelle-core v1.32.0's MaterialEffect.pixel_water reached main: material-demo and pack-city both pinned a bgfx with no arm for it, and CI said nothing. Only sprite-runner (sokol) was built, so the signal read "one backend broken" instead of "three examples broken, two untested" (#731).

It fabricates breaks. The same lane clones core/engine/gfx/null at main as siblings, and the assembler's monorepo auto-discovery builds those instead of each example's pins. Mid-release-train the four mains disagree for the minutes between merges, so an unrelated PR goes red: #730 died on backend_contract has no member named 'PixelWaterRgba', and a re-run of the identical commit passed.

The fix

New job: bgfx examples (PINNED resolution) — ubuntu-latest, scratch LABELLE_HOME, and deliberately no runtime sibling checkouts. It generates and builds material-demo and pack-city against their declared project.labelle pins, and asserts the pins are what got used rather than trusting a green build:

  • fails up front if labelle-core/-engine/-gfx/-null is present as a sibling (that absence is the test);
  • asserts each pinned version was actually fetched into the scratch home;
  • greps the install/generate logs for the assembler's own warnings — a mechanism assertion, not a value one. Verified against a real sibling checkout, both strings are real:
    install:  core: using LOCAL sources from '/private/tmp/pinlane/labelle-core'
              — the pinned 1.32.0 will NOT be built (#685)
    generate: labelle: core is built from LOCAL sources, not the pinned 1.32.0
              — source '/private/tmp/pinlane/labelle-core' (rev 3e5f5eb…)
    

The sibling lane is renamed to Examples integration (siblings @ main — cross-repo, may be mid-train) and now prints, before anything builds, a banner of every sibling's SHA / ref / git describe, plus a note that these override the examples' pins and that the pinned job is the one that tests what the PR declares.

Why Linux-only, and why every PR (not path-gated)

Path-gating on examples/** / src/config.zig / backend pins was the tempting option and it does not cover the defect that motivated the issue. The v1.32.0 break arrived from a dependency's release; no file in this repo changed. A path filter would have skipped the job on every PR while main stayed broken — a check that skips exactly when the break lands is the same class of defect being fixed here. One platform is enough to catch a compile break in backend source, so: Linux only, always. Cost is bounded by sharing one zig cache between the two examples — bgfx's C++ objects compile once and pack-city links against them.

Proof I broke it and CI notices

Run on a mirrored checkout outside prj/labelle-toolkit/ with a scratch LABELLE_HOME, so pins genuinely resolve from the registry (the assembler's findRepoRoot walks up from the executable, so an in-tree run would have silently used siblings):

result
material-demo @ its own pins install: fetching core 1.32.0 … bgfx 0.20.0 (remote); zig build → exit 0, zig-out/bin/material_demo (36s)
pack-city @ its own pins zig build → exit 0 (25s)

Then re-pinned material-demo's backend to the known-bad bgfx 0.13.1 — the exact #731 break:

$ labelle-assembler generate --project-root .
info: labelle-assembler: generated .labelle/bgfx_desktop/
GENERATE_EXIT=0                      <-- the OLD job stops here. GREEN.

$ zig build
labelle-bgfx/src/gfx/texture.zig:469:12: error: switch must handle all possibilities
labelle-core/src/backend_contract.zig:308:5: note: unhandled enumeration value: 'pixel_water'
BUILD_EXIT=1                         <-- the NEW job. RED.

That is the defect, reproduced, and the new lane catching it.


#735 — provision shaderc so the material pipeline e2e actually runs

The defect

test/material_pipeline_e2e.py is the only test covering the producer side of game-owned materials: descriptor → shaderc → packaged variants → a material that compiles and validates against core's contract. It was opt-in behind three -D flags because CI had no shaderc, so it ran nowhere. Everything else in CI covers consumers of already-compiled shaders (goldens, the bgfx probe with checked-in .bin fixtures) — and pre-compiled fixtures are structurally incapable of noticing a pipeline regression.

The fix

New job: Material shader pipeline e2e (real shaderc) (Linux). It provisions shaderc by reusing the existing toolchain source, not adding a second one — the chain is derived entirely from this repo's own pins:

src/config.zig  builtinProvider(.bgfx).version          -> 0.21.0
labelle-bgfx@v0.21.0/build.zig.zon  .zbgfx.url          -> …/zbgfx/archive/934372f1….tar.gz
that archive, `zig build -Dwith_shaderc=true`           -> zig-out/bin/shaderc
build.zig  core_version default                          -> 2.0.0  (--material-test-core)

All three -D flags are derived by the job; nobody passes one by hand. The shaderc build is cached on the zbgfx URL (~2.5 min cold locally, 560s CPU; seconds on a hit). Each extraction was verified against the real files before committing.

Already independent of examples/condenser — the driver synthesizes its own three-material fixture (shared includes, an escaping include, a deliberately broken .sc) under .zig-cache/.

It fails closed, not just open. The driver asserts five rejection paths beside the happy one: an include escaping the material dir (EscapingShaderInclude), an uncompilable .sc, a descriptor missing the variant the host renderer requires, and two component-collision cases in the emitted comptime guard.

Bug found and fixed while wiring it up: the missing-target assertion hard-coded target requires 'spv', which is the Linux answer. material_build.create picks the required variant per host (darwin -> mtl), so the test could not pass on macOS at all — it ran 12 real shader compiles and then died on the last line. It now follows the host.

Proof I broke the pipeline and only this test noticed

Disabled include-tree tracking in material_build.zig (trackTree(b, run, b.path("materials"), true)), so editing a color.sh no longer invalidates the shader cache — a producer-side regression with no consumer-side symptom:

zig build test-materials             -> EXIT 0   (448 tests, all green)
zig build test-material-integration  -> EXIT 1
    AssertionError: shader fog (spv) (fog.spv.bin) success   [it was 'cached'; not rebuilt]

The entire in-repo suite stays green. Only the e2e catches it. That is the coverage gap, demonstrated.

Second probe — disabled the escaping-include rejection in material_schema.zig so the pipeline stops failing closed:

zig build test-material-integration  -> EXIT 1
    File "test/material_pipeline_e2e.py", line 83, in <module>
      build(False, "EscapingShaderInclude")

Clean run on this branch, with a shaderc built from the pinned zbgfx:

PASS: 12 real shader variants, generated core descriptors, cached rebuild,
include invalidation, escaping include rejection, compiler failure,
missing target diagnostic

CI cost added (MEASURED on this PR's own run, not estimated)

job platform measured notes
bgfx examples (PINNED resolution) ubuntu 3m46s material-demo 2m18s (cold bgfx C++ compile), pack-city 30s — it links the same objects. No cache, deliberately: a cold registry-resolved build is the point.
Material shader pipeline e2e (real shaderc) ubuntu 11m01s cold Almost all of it is the one-time shaderc build, cached on the zbgfx URL; steady state is the ~35s e2e plus checkout.

Both run in parallel with the existing jobs. Longest pre-existing job on this run was Windows at 9m38s, so wall-clock CI goes from ~9m38s to ~11m01s (+1m23s); ~15 runner-minutes added on a cold run, ~5 warm.

Verified green end to end on this PR (run 35376494395, all 8 jobs success):

bgfx examples (PINNED resolution)
  No runtime siblings present; pins will resolve from the registry.
  material-demo: core 1.32.0 / engine 2.12.2 / gfx 1.30.1 resolved from the registry
  PASS: material-demo BUILT against its declared pins
  pack-city: core 1.32.0 / engine 2.12.2 / gfx 1.30.1 resolved from the registry
  PASS: pack-city BUILT against its declared pins

Material shader pipeline e2e (real shaderc)
  builtin bgfx provider: 0.21.0
  pinned zbgfx: https://github.com/labelle-toolkit/zbgfx/archive/934372f1….tar.gz
  curated core: 2.0.0
  PASS: 12 real shader variants, generated core descriptors, cached rebuild,
  include invalidation, escaping include rejection, compiler failure,
  missing target diagnostic

Examples integration (siblings @ main — cross-repo, may be mid-train)
  REPO                   REVISION                                       DESCRIBE
  labelle-core           3e5f5eb18001d9ef556fbc852fa307e10e854b69 (main) v2.0.0
  labelle-engine         6b98478fb32b7a58c1461f6af4f0c16b4934172b (main) v3.0.0
  labelle-gfx            3dcfc6384edc659ad7962ff6d90b0f2fe4613bd5 (main) 3dcfc63
  labelle-null           91a858eaafc55996203237976ad322b4fa76588b (feat/scripting-touchpoint)
  labelle-cli            c54ee0a5c7900ffdbc3a70c0440851bb0ba19afd (main) c54ee0a
  labelle-gui            823e3671c88c2e423b92a25f19253ec1ab420d0a (main) 823e367
  labelle-engine-727     c75a1ca852e3f603dca44de40c2c8f5845ca380f (feat/727-receiver-id-table)

Files touched

.github/workflows/ci.yml, test/material_pipeline_e2e.py. No overlap with src/material_pipeline.zig or src/init_cmd.zig (concurrent work in #739/#742). The two probes above touched src/material_build.zig and src/material_schema.zig temporarily and were reverted — neither file is modified by this PR.

Closes #732
Closes #735

https://claude.ai/code/session_01XBGRLojiysEr5zy1U5mbP6


View with [code]smith Autofix with [code]smith
Need help on this PR? Tag @codesmith-bot with what you need. Autofix is disabled.

…ine e2e (#732, #735)

Two coverage gaps in one workflow, grouped because they rewire the same
lanes of `.github/workflows/ci.yml` and would conflict as separate PRs.

── #732: the bgfx examples were generated, never built ──────────────────

`generate` proves the manifest parsed. It does not compile a line of
backend source, so every defect that lives in a DEPENDENCY — an exhaustive
switch, a moved contract type, a raised floor — passed. That is how core
v1.32.0's `MaterialEffect.pixel_water` reached main: `material-demo` and
`pack-city` both pinned a bgfx with no arm for it and CI said nothing. Only
`sprite-runner` (sokol) was built, so the signal read "one backend broken"
instead of "three examples broken, two untested" (#731).

The same lane also FABRICATES breaks: it clones core/engine/gfx/null at
`main` as siblings, and the assembler's auto-discovery builds those instead
of the pins, so mid-release-train the four mains disagree and an unrelated
PR goes red (#730, `backend_contract has no member named 'PixelWaterRgba'`;
a re-run of the identical commit passed).

  * New job `bgfx examples (PINNED resolution)` — ubuntu, scratch
    LABELLE_HOME, NO runtime sibling checkouts — GENERATES AND BUILDS both
    bgfx examples against their declared `project.labelle` pins. It refuses
    to start if a runtime sibling exists, asserts each pinned version was
    fetched into the scratch home, and greps the install/generate logs for
    the assembler's own "using LOCAL sources" / "built from LOCAL sources"
    warnings — a mechanism assertion, not a value one.
  * The sibling lane is RENAMED "Examples integration (siblings @ main —
    cross-repo, may be mid-train)" and prints a banner of every sibling
    SHA/ref/describe before anything builds, so a mid-train red is
    recognised at a glance instead of debugged from scratch.

Linux only (one platform catches a compile break in backend source), and on
every PR rather than path-gated: the v1.32.0 break arrived from a
dependency's release with no file in this repo changed, so a path filter
would have skipped the job exactly when the break landed.

Proof, on a mirrored checkout outside the toolkit tree with a scratch
LABELLE_HOME (so pins really resolve from the registry):

  material-demo @ its own pins (core 1.32.0 / bgfx 0.20.0)
      install: fetching core 1.32.0, bgfx 0.20.0 (remote)   generate: ok
      zig build -> exit 0, zig-out/bin/material_demo        (36s)
  pack-city    @ its own pins -> zig build exit 0           (25s)

  material-demo with bgfx re-pinned to the known-bad 0.13.1 (the #731 break)
      $ labelle-assembler generate  -> exit 0   <-- old job: GREEN
      $ zig build                   -> exit 1   <-- new job: RED
        labelle-bgfx/src/gfx/texture.zig:469:12: error: switch must handle
        all possibilities
        labelle-core/src/backend_contract.zig:308:5: note: unhandled
        enumeration value: 'pixel_water'

And the local-sources assertion, verified against a real sibling checkout:
  install:  "core: using LOCAL sources from '…' — the pinned 1.32.0 will
             NOT be built (#685)"
  generate: "labelle: core is built from LOCAL sources, not the pinned
             1.32.0 — source '…' (rev …)"

── #735: nothing ran the shader BUILD pipeline ──────────────────────────

`test/material_pipeline_e2e.py` is the only test covering the PRODUCER side
of game-owned materials (descriptor -> shaderc -> packaged variants -> a
material that validates against core's contract). It was opt-in behind
three `-D` flags because CI had no shaderc, so it never ran. Everything
else covers CONSUMERS of already-compiled shaders, and the fixtures are
pre-compiled — structurally unable to notice a pipeline regression.

  * New job `Material shader pipeline e2e (real shaderc)` provisions
    shaderc by REUSING the existing toolchain source rather than adding a
    second one: it reads the builtin bgfx provider version out of
    `src/config.zig`, reads that release's `zbgfx` archive URL out of
    labelle-bgfx's `build.zig.zon`, and builds `-Dwith_shaderc=true` from
    exactly that archive (cached on the URL; ~2.5 min cold locally).
    `--core` comes from the curated `core_version` default in `build.zig`,
    so there is no second pin to drift.
  * Nobody passes a flag by hand — the job derives all three.
  * Already independent of `examples/condenser`: the driver synthesizes its
    own three-material fixture under `.zig-cache/`.
  * `test/material_pipeline_e2e.py`: the missing-target assertion hard-coded
    `target requires 'spv'`, which is the LINUX answer. `material_build`
    picks the required variant per host (darwin -> mtl), so the test could
    not pass on macOS at all. It now follows the host.

The driver already exercises five REJECTION paths beside the happy one, so
it proves the pipeline fails closed: an include escaping the material dir,
an uncompilable `.sc`, a descriptor missing the host's required variant,
and two component-collision cases in the emitted comptime guard.

Proof it catches a real producer-side regression that nothing else does —
disabled include-tree tracking in `material_build.zig` so a changed
`color.sh` no longer invalidates the shader cache:

  zig build test-materials          -> exit 0  (448 tests, all green)
  zig build test-material-integration -> exit 1
      AssertionError: shader fog (spv) (fog.spv.bin) success  [not rebuilt]

and, with the escaping-include rejection disabled:

  zig build test-material-integration -> exit 1 at build(False, "EscapingShaderInclude")

Clean run on this branch: "PASS: 12 real shader variants, generated core
descriptors, cached rebuild, include invalidation, escaping include
rejection, compiler failure, missing target diagnostic".

Closes #732
Closes #735

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XBGRLojiysEr5zy1U5mbP6
@coderabbitai

coderabbitai Bot commented Sep 18, 2026

Copy link
Copy Markdown

Warning

Review limit reached

  • Ask an admin to enable usage-based reviews

Open in CodeRabbit

Reviews can continue after your included limit without a manual trigger. An admin must approve usage-based billing.

Next included review available in 28 minutes.

Check out review usage here.

View limit details

Limit details: You’ve used all 5 included reviews currently available. Your 55 included PR review attempts over the past 7 days set your current allowance at 5 reviews per hour.

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

Learn how review limits work.

Review configuration:

⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Team

Run ID: dbf40b9a-9809-4449-8633-f306a32f3d53

📥 Commits

Reviewing files that changed from the base of the PR and between 3cc6510 and b7bed62.

📒 Files selected for processing (2)
  • .github/workflows/ci.yml
  • test/material_pipeline_e2e.py

Comment @coderabbitai help to get the list of available commands.

@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Sep 18, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-09-18T17:51:55.837541Z b7bed62 PR opened
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

1 participant