ci(campaigns): every campaign in the pinned content repo builds, or the board is red (task #44) - #322
Merged
Merged
Conversation
…he board is red (task #44) Every gate in CI built a FIXTURE. Nothing ever built a real campaign, so the product could stop compiling with the whole board green — PR #260 reached 10/10 while stopping the flagship released campaign `nobodys-cave-island` from building at all (26 x DW0364 on standable cells at the ocean line). New required status check `campaign builds (every campaign in the content repo)` builds every campaign the pinned content checkout carries, in every language its world.json declares, on every push. Campaigns are DISCOVERED, never listed, so the next content re-pin gates a new campaign with nobody remembering. A campaign that cannot build today is recorded in .github/campaign-build-exclusions.toml, which INVERTS the assertion instead of removing it: still built, must still fail, and must fail with exactly the recorded codes — an extra code is a new break that was hiding behind the exclusion, and a success is an expired exclusion. Both red. One entry today: hollow-vigil, DW0331 (task #34). Validator-only: no crate, no emission path and no prefab is touched, so player-facing output is byte-identical. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AjQ5p1Kv5MrkGPumi7yXWL
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
The gap
Every gate in CI builds a fixture. Nothing has ever built a real campaign.
Fixtures under
crates/dsl/fixtures/validandcrates/compiler/tests/fixturesexercise one verb each. A campaign is the only place the verbs meet a real prefab
library, a real layout solve and a real translation sidecar — and that is the
surface an engine change breaks. So the product could stop compiling entirely
with the whole board green, and it did: PR #260 is 10/10 green and stops
nobodys-cave-island, the flagship released campaign, from building at all.The job that would have caught this exists as content-repo PR #26, blocked
downstream of the gap it closes. This lands the gate here instead, against
engine
main, where the campaigns that exist today do build — which breaks thecycle.
What changed (design level)
New required status check
campaign builds (every campaign in the content repo),on every push (not PR-only: the window between push and PR is exactly where a
break would sit unseen).
<content>/campaigns/holding aworld.jsonis a campaign. No list in this repo to forget to update — acampaign added to content
mainis gated by the next re-pin.versions.toml[content].shavia the existingcheckout-contentaction, LFS on (a campaignbuild copies the prefab
.nbtbodies). Never a movingmain.enplus every code inworld.jsonlanguages.A campaign ships its localized output too (
out-zh/), so a sidecar that stopssatisfying the coverage checks breaks a released artifact exactly as an English
break does.
--delvecis required and never inferred. The gate's whole subject iswhich engine built the campaign, so the caller names the binary.
Exclusions invert the assertion — they do not remove it
A silently skipped campaign is a vacuous green and is worse than no job. So
.github/campaign-build-exclusions.tomldoes not switch a campaign off:expect_codes— any othercode is a new break that was hiding behind the exclusion, and reds;
in the PR that fixed the campaign, or the next regression it suffers is excused
by a stale line.
Two honest states, both printed in the job's own output:
builtandknown-red for exactly this reason.Proof 1 — the control: the same gate against PR #260, RED
worker/spec-0026-foundation@6e446ba, built in its own detached worktree,run through the identical script at that branch's own content pin
(
5a23d90, which is what itscheckout-contentwould fetch):Exit 1. The 26
DW0364cells are in the output. A gate that has never been seenfailing is not known to bind; this one has been.
Green on this branch, at the exact pinned content SHA (
87a8ade), same script,same command CI runs:
Proof 2 — the binding count
Discovered on content
main@87a8ade: 2 campaigns. Both are handled; neitheris skipped.
nobodys-cave-islanden,zh-cnhollow-vigilen,zh-cnDW0331(task #34): two dialogue option labels render 179 px and 157 px wide against the 146 px a vanilla dialog button fits. Vanilla scrolls an over-wide caption rather than wrapping it, so the fix is a content rewrite of the labels, not an engine change — out of scope here, and named in the job's output on every runNot built, and why — the honest full list.
campaigns/nobodys-caveandcampaigns/the-drowned-bellappear in a local content checkout but carry no DSLsources on content
main; they exist only on the unmerged branchescampaign/nobodys-caveandcampaign/the-drowned-bell(locally, only theirgitignored
out/trees). They are not discoverable and are not excluded — theyare not there. They come under the gate automatically the day their sources reach
content
main, with no change to this repo.So the binding today is one campaign, honestly stated. That is the flagship, and it
is the one PR #260 breaks.
Proof 3 — the exclusion mechanism cannot degrade into a skip
Every way this gate could go green having proven nothing, each provoked and each
observed red (exit 1):
BUILDS now, but is still listed … Delete the exclusionalso failed with ['DW0331']. A new break was hiding behind the exclusion.names no campaign … a stale exclusion pre-excuses any future campaign that takes the name0 of 2 campaigns were built — every one is excluded. This job would be green having compiled no product at all.campaign sources not found at … every campaign below would be 'skipped'discovered 0 campaigns … A gate over nothing is not a pass.Required-status-check lockstep
.github/required-status-checks.txtgains the exact jobname:string, andtools/check-required-contexts.pyis green both directions:Per that file's own procedure, adding a job is a two-part change: this merges
first, then
campaign builds (every campaign in the content repo)is added tobranch protection's required contexts. One PR may block while it first reports;
that is the accepted cost and is why the name is pinned in a comment above the job.
Wall-clock
Measured on the dev profile (the workspace's
opt-level = 1, PR #313), thebinary built once and handed to the driver:
nobodys-cave-island, per languagehollow-vigil(fails at validate)On the real runner: the whole job is 3 m 32 s,
cargo buildincluded — thethird-fastest of the four jobs over a minute (
rust5 m 33 s,tier 24 m 22 s,harness3 m 50 s), so it does not extend the critical path at all.No split is needed and nothing is sampled: the gate builds everything, every
push. If the campaign count grows enough to matter, the honest split is by
campaign into a matrix — not by sampling — and it can be made then, from a
measurement.
Its first live run, at the pinned SHA, checked out and bound exactly as designed:
Merge class
Validator-only. No crate, no emission path, no prefab, no DSL surface is
touched —
git statusover the diff matches zero files undercrates/,prefabs/orpacktest/. Player-facing output is byte-identical, so the mergegate is the machine red→green demonstration above (Proof 1), not the owner's
playtest.
Out of scope, deliberately
DW0331inhollow-vigil(task #34) is not fixed here — it is declared, and thegate names it in its own output on every run. The shoreline/
floodwork ondocs/spec-shallowsis untouched, andworker/spec-0026-foundationis untouched:this PR only makes its breakage visible.
Docs
docs/reference/tools.mdgains the full entry fortools/build-every-campaign.py(tooling-sync rule). No skill workflow is touched — this gate is for engine
changes, not for generating a delve.
🤖 Generated with Claude Code
https://claude.ai/code/session_01AjQ5p1Kv5MrkGPumi7yXWL