Lesson 201: Triple-Channel HTML5 build_label and channel_label_match Receipt (2026)
Direct answer: When itch.io, GX.games, and Steam HTML5 builds all ship from one repo, channel_label_match on BUILD_RECEIPT must compare normalized labels from a single root VERSION file—not three upload dashboards that each invent their own string. This lesson ships triple_channel_label_receipt_v1.json and an itch_public row in your playtest scope map.

Why this matters now (July 2026 triple HTML5)
July 2026 micro-studios run three public HTML5 surfaces in parallel:
- itch for jam links and fest embeds (subdomain WASM smoke)
- GX.games for discovery uploads
- Steam for wishlist + playtest routing (playtest isolation)
Pairwise CI still passes Steam + GX while channel_label_match flips false the moment itch joins—because itch logs numeric game IDs or title-case slugs, not your fest build_label. The triple-channel help fix documents symptoms; this lesson is the course milestone that wires the assembler into your live-ops gate chain after Lesson 200 concat_ok.
Beginner path (45-minute proof)
| Step | Action | Success check |
|---|---|---|
| 1 | Copy repo-root VERSION to clipboard |
One line, no trailing spaces |
| 2 | Upload GX + Steam from same git_sha |
Upload logs show identical commit |
| 3 | Map itch label from VERSION (not game ID) |
itch dashboard matches string |
| 4 | Add itch_public to playtest_scope_map_v1.json |
JSON validates |
| 5 | Run triple-channel compare script | All three normalized_match: true |
| 6 | Write triple_channel_label_receipt_v1.json |
channel_label_match: true |
| 7 | Patch BUILD_RECEIPT row | channel_label_match true in CI |
Time: ~45 minutes first pass; full lesson 72 minutes with SQL gate + facilitator README.
Developer path (gates H1–H6)
| Gate | Check | Fail when |
|---|---|---|
| H1 | Single VERSION source |
Multiple version files diverge |
| H2 | git_sha identical on three upload logs |
Hotfix on one channel only |
| H3 | itch label derived from VERSION |
Raw itch slug in receipt |
| H4 | playtest_scope_map includes itch_public |
Missing scope row |
| H5 | Normalized compare (case-fold) | Strict string mismatch on case only |
| H6 | BUILD_RECEIPT channel_label_match |
Any H1–H5 red |
playtest_scope_map_v1.json (itch row)
{
"surfaces": [
{"id": "fest_public", "store": "steam", "build_label_source": "VERSION"},
{"id": "gx_public", "store": "gx", "build_label_source": "VERSION"},
{"id": "itch_public", "store": "itch", "build_label_source": "VERSION", "url_pattern": "*.itch.io"}
]
}
triple_channel_label_receipt_v1.json
{
"schema": "triple_channel_label_receipt_v1",
"version_source": "VERSION",
"git_sha": "abc1234",
"channels": [
{"surface": "steam", "logged_label": "fest-demo-2026-05-24-rc2", "normalized_match": true},
{"surface": "gx", "logged_label": "fest-demo-2026-05-24-rc2", "normalized_match": true},
{"surface": "itch", "logged_label": "fest-demo-2026-05-24-rc2", "normalized_match": true}
],
"channel_label_match": true,
"itch_public_scope_ok": true
}
Pin under release-evidence/html5/TRIPLE_CHANNEL_LABEL_RECEIPT.json.
itch_public scope map notes
itch_publicis the receipt surface id—do not reusegx_publicfor itch URLs.- Custom-domain itch hosts belong in Construct CORS playbook; this lesson assumes subdomain
*.itch.iofor label parity with upload logs.
Compare script sketch
from pathlib import Path
VERSION = Path("VERSION").read_text().strip()
channels = {
"steam": Path("logs/steam_upload.log").read_text(),
"gx": Path("logs/gx_upload.log").read_text(),
"itch": Path("logs/itch_upload.log").read_text(),
}
def extract_label(log: str) -> str:
for line in log.splitlines():
if "build_label=" in line:
return line.split("=", 1)[1].strip()
raise ValueError("build_label missing")
rows = []
for surface, log in channels.items():
label = extract_label(log)
rows.append({
"surface": surface,
"logged_label": label,
"normalized_match": label.casefold() == VERSION.casefold(),
})
assert all(r["normalized_match"] for r in rows), rows
Publish gate
ALTER TABLE release_publish_gate ADD COLUMN IF NOT EXISTS
triple_channel_label_blocked BOOLEAN NOT NULL DEFAULT false;
CI job verify_triple_channel_labels_v1 sets the flag false only when receipt JSON passes and itch MIME help post_upload_mime_ok is true for the itch lane.
Prerequisites
- Lesson 200 — honest playtest timelines (
concat_ok) - GX multi-channel tools resource
- itch WASM MIME cross-check list
Common mistakes
- Logging itch game ID instead of fest
build_label. - Uploading itch from a fork with different
VERSIONtext. - Omitting
itch_publicin scope map while itch URL is in facilitator README. - Passing Thursday row review without an itch column.
Troubleshooting
| Symptom | Cause | Fix |
|---|---|---|
| GX+Steam pass, itch fails | itch slug casing | Normalize compare; fix itch dashboard field |
channel_label_match false, logs look identical |
Hidden whitespace in VERSION |
trim() + visual diff |
| itch MIME green, label red | Different issues | Run MIME receipt + label receipt separately |
Mini exercise (50 minutes)
- Seed three upload logs with one
VERSION. - Break H3 on purpose (itch uses wrong slug); confirm gate fails.
- Fix itch mapping; regenerate receipt.
- Link receipt in facilitator contract (pairs blog #11 when published).
- Cross-link from triple-channel help Related section.
Continuity
- Previous: Lesson 200 —
concat_ok - Next: Lesson 202 — Weblate string table smoke
- Guides (planned): GameMaker GX upload preflight (Guide queue #8)
FAQ
Does this replace the help article?
No—the help article is the fix; this lesson is the milestone wiring gates into your RPG live-ops course arc.
Custom itch domain?
Label receipts still must match VERSION; hosting proof is CORS playbook + MIME help.
Only two channels?
Set channel_label_match true only when all declared surfaces in scope map pass—do not declare itch until ready.
Three HTML5 storefronts, one VERSION spine—or your BUILD_RECEIPT will keep failing the week you need it green.