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.

Lesson hero for triple-channel HTML5 channel_label_match receipt

Why this matters now (July 2026 triple HTML5)

July 2026 micro-studios run three public HTML5 surfaces in parallel:

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_public is the receipt surface id—do not reuse gx_public for itch URLs.
  • Custom-domain itch hosts belong in Construct CORS playbook; this lesson assumes subdomain *.itch.io for 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

Common mistakes

  • Logging itch game ID instead of fest build_label.
  • Uploading itch from a fork with different VERSION text.
  • Omitting itch_public in 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)

  1. Seed three upload logs with one VERSION.
  2. Break H3 on purpose (itch uses wrong slug); confirm gate fails.
  3. Fix itch mapping; regenerate receipt.
  4. Link receipt in facilitator contract (pairs blog #11 when published).
  5. Cross-link from triple-channel help Related section.

Continuity

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.