Skip to content

Harden deploy: prevent a develop push from being able to change production bytes, and make prod stability legible in the Deployments UI #117

Description

@DocGerd

Goal

A push to develop should not be able to change production content, and the deployment history should make production's stability legible at a glance.

Today production stability on develop-triggered runs rests on build determinism re-established by convention (a manual byte-for-byte check at #96 time), not on anything enforced by CI. The aim of this backlog item is to move that guarantee from convention to enforcement, and to set correct expectations in the GitHub Deployments UI.

Current state

This is documented, deliberate #96 behavior, not a regression — recording it so it can be hardened, not reverted.

GitHub Pages serves a single deployment artifact per origin, so deploy.yml builds both refs on every push to either branch: the build job checks out main -> site root (production) and develop -> /uat/ (unreleased preview), assembles one combined site/ artifact, and the deploy job (actions/deploy-pages) publishes it. actions/deploy-pages always records its deployment in the github-pages environment regardless of the triggering branch; the separate uat-environment bookkeeping job (added in #106, gated if: github.ref == 'refs/heads/develop') additionally records a uat environment entry.

Observed consequence (the trigger for this issue): a develop push refreshes both the github-pages and the uat environments at essentially the same instant, so the Deployments UI reads as if production redeployed on a develop-only change. Verified via the API — the two most recent develop pushes:

Env ref sha created
github-pages develop c7458c9 2026-07-22T21:56:57Z
uat develop c7458c9 2026-07-22T21:57:09Z
github-pages develop 5a20940 2026-07-22T21:43:16Z
uat develop 5a20940 2026-07-22T21:43:31Z

Note the github-pages entries carry the triggering develop sha, not a main sha — the deploy step is inherently branch-agnostic.

What is actually safe today, and why it is only safe by convention:

  • The production output is rebuilt from the unchanged main ref with SC_DEPLOY_ENV unset, and was verified byte-for-byte identical to the pre-Deploy develop to a UAT/staging environment on GitHub Pages (sub-path) #96 build at that time.
  • But nothing in the pipeline re-verifies that on each develop-triggered run. If a future dependency bump, toolchain change, or non-deterministic build step made the main build differ, a develop push would silently republish altered production bytes. The guarantee is a manual check frozen in time, not an enforced invariant.

Proposed direction (to be designed at implementation time)

  • (a) Don't rebuild prod on develop-triggered runs. Reuse or cache the production build keyed by the main commit sha (e.g. actions/cache or an artifact keyed on the main HEAD sha), and only rebuild the main half when that sha actually changes. A develop-only run would then reassemble the artifact from the cached prod bytes, so it is structurally incapable of altering them.
  • (b) Enforce byte-stability in CI. Add a deploy-job step that asserts the assembled production root is byte-identical to the last main-triggered production build (compare a manifest of file hashes against a stored baseline), and fails the run loudly on any drift instead of silently publishing. This makes "a develop push cannot change prod bytes" a checked invariant rather than a convention.
  • (c) Document the Deployments-UI semantics. The github-pages entries appearing on develop pushes are inherent to Pages' single-artifact model and cannot be suppressed; the uat env record (ci(deploy): record develop deploys against a distinct uat environment #106) is what meaningfully marks develop-relevant deploys. A short note in CLAUDE.md / the workflow comments should set this expectation so the near-simultaneous refresh is not misread as an accidental prod redeploy.

Honest limitation: GitHub Pages exposes one deployment target per origin, so genuinely separate prod/uat deployment environments (distinct hosting per sub-path, each with its own independent history and gates) are not achievable on Pages. The sub-path split (/ vs /uat/) plus the uat bookkeeping env is the closest available; options (a)/(b) harden the coupling that this constraint forces, they do not remove it.

Acceptance criteria

  • A develop-triggered run cannot alter production bytes — enforced by CI (option (a) and/or (b)), not by convention.
  • If the assembled production root ever drifts from the last main-triggered build, the deploy run fails loudly rather than publishing the change.
  • The Deployments-UI semantics (why github-pages refreshes on develop pushes; what the uat env record means) are documented where a maintainer will see them.
  • Production remains byte-for-byte identical across a develop-only push, demonstrated by the new check (the Deploy develop to a UAT/staging environment on GitHub Pages (sub-path) #96 guarantee, now automated).
  • No new backend and no change to the offline/PWA behavior; the sub-path hosting model is unchanged.

Backlog item, filed 2026-07-22. Not scheduled for a specific session.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions