← All decisions

Split the live demo into pinned stable and always-latest edge release channels, promoted via a committed ref file

architecturedeploymentwebsiterelease-channel

Context

docs/technical-architecture.md §3.4 (decision 0042) required the live demonstration surface to be split into a pinned, deliberately promoted /play (stable) and a directly-tracking /play-edge or equivalent (edge) channel, decoupling what's publicly demonstrated from active development -- tracked as roadmap item RM-025. The site's build is fully stateless (lustre_ssg regenerates the whole dist/ tree from scratch every run, no incremental state carried between deploys), so the real design problem was finding a persistence mechanism for "what glemy commit is currently promoted to stable" that survives across independent, from-scratch CI runs.

Options considered

Store the promoted commit as a GitHub Actions repository variable or secret — rejected
Would move a real, meaningful piece of project state outside version control -- changing it wouldn't show up in git history/blame, wouldn't go through a normal commit/PR, and wouldn't be visible to someone reading the repo. Git already solves exactly this problem (a versioned, auditable, diffable record of what changed and when); reaching for GitHub-specific config storage instead would be solving an already-solved problem with a less transparent tool.
Have the deploy workflow itself decide what counts as "stable" (e.g. the latest glemy tag, or glemy's state as of N days ago) — rejected
docs/technical-architecture.md §3.4 specifically requires promotion to be a deliberate action, not an automatic one -- an automatic rule (latest tag, oldest-by-N-days) is still automatic, just on a different trigger, and doesn't give a human an actual decision point before something goes live as the public-facing demo.
A single /play page showing only the stable build, with edge accessible only by a separate, unlinked /play-edge route — rejected
Both channels are shown together on the same game card (a primary "Play Tiers" button for stable, a smaller "Try the latest build (edge)" link beside it) rather than as two separate pages. Splitting into two full pages would duplicate the whole games-catalog page structure for what's really one piece of information (this game, two builds of it) -- unnecessary structure for what a single card component can express with one more link.

Decision

Added STABLE_GLEMY_REF (repo root, one line, a glemy commit SHA) as the persistence mechanism: promoting a new stable build is a real, committed, pushed change to this file, exactly matching technical-architecture.md's "deliberate action" requirement and using git's own history as the audit trail rather than inventing one. .github/workflows/deploy.yml now checks out glemy twice per run -- glemy-edge always at the default branch (also the source for decisions.jsonl/development-plan.jsonl, since those track main regardless of which build is currently stable), glemy-stable at whatever ref STABLE_GLEMY_REF names -- builds both JS targets, and copies each into its own static/play-demo-{stable,edge}/ directory. build.gleam's existing directory-content-hashing (decision 0045, RM-024) was generalized to take a base directory name parameter rather than being hardcoded to one path, so both channels get independently hashed and renamed with no duplicated logic. game_card.gleam gained a DemoPaths record (stable/edge path pair) threaded through home.gleam and play.gleam alongside the existing base_url/style_hash parameters, and now renders both links on the one game card rather than adding a second page.

Verification

gleam test passes (17/17, unchanged -- this is infrastructure/wiring, not new pure logic needing new unit tests). A full local build simulating both channels produced correctly distinct, independently hashed paths (play-demo-stable-<hash>, play-demo-edge-<hash>) with both buttons on the rendered game card linking to the right one. A real headless-browser pass against both channel URLs directly showed both genuinely rendering via WebGPU (119 and 120 real frames respectively) with zero errors beyond the same pre-existing, already-diagnosed glemy favicon gap. The real CI workflow change (dual checkout, dual build, dual copy) was written to reuse the exact same copy-step shape as the existing single-channel version, just once per channel, keeping the risk of a CI-only failure low despite not being able to fully simulate GitHub's cross-repo checkout locally.

Consequences

Closes RM-025 and, with it, every concretely buildable item from the original technical-architecture.md deployment requirements (RM-017 remains correctly policy-only, with nothing to implement until persistence is actually needed). Deploys now check out and build glemy twice instead of once, roughly doubling that portion of CI time (still well under a minute total, confirmed by the real run) -- accepted as the honest cost of having two genuinely independent builds rather than one. Promoting a new stable build is now a real, if manual, step someone has to remember to take (documented in the README) -- the intended behavior, not an oversight: a stale stable build is a visible, git-diffable fact (STABLE_GLEMY_REF's own age), not a silent one.

References