ci(tss): gated TSS shadow publish lane v2 (TIN-3026) - #263
Jesssullivan wants to merge 5 commits into
Conversation
|
Operator ceremony for the first tss publish (run in order; nothing here is performed by an agent):
Merging this PR alone changes nothing on any served surface. |
|
Reworked at HEAD after the adversarial review of the first cut (blocker confirmed: the publish job executed PR-head code in the same job that held the Cloudflare token).
Open from the review, deliberately not changed here: the pre-publish Local: |
|
Three things before this can be dispatched (the job split and served-SHA
|
|
Fixed at HEAD: (1) the kill switch is now re-read by Actions at deploy-job start ( |
tss.tinyland.dev has had no publisher since the legacy shadow workflow was retired on 2026-08-13; it still serves the 2026-07-14 build. This adds the default-branch-owned, dispatch-only lane TIN-3026 names as the separately authorized apply path for the public noindex development shadow: - resolves an exact current-main SHA or an open same-repo PR head, requires a successful canonical CI run at that SHA with build-and-test and bazel-remote-gates green, and fails closed unless deploy=true and BLOG_TSS_PUBLISH_ENABLED=true; - builds with PUBLIC_DEPLOY_TIER=shadow (site-wide noindex + source-sha meta, validated by validate-deploy-tier-output.mjs), records the static digest, revalidates the kill switch and the source immediately before publish, and refuses the production Pages project by name; - publishes to CLOUDFLARE_PAGES_TSS_PROJECT_NAME only (repo var, required). Resolver fixtures cover every refusal path; the workflow-authority contract pins the lane shape and forbids production reach or manual carriers. AGENTS.md registers the lane; docs/tss-shadow-publish.md records the ceremony.
…ified deploy Review finding on the first cut: the publish job ran npm ci / npm run build on the requested PR head in the same job that held the Cloudflare account token, so source-tree code could reach the token and the production-project name guard was decorative. The lane now mirrors shadow-source-build-v2 → shadow-source-publish-v2: the build job has contents:read only and no secret, uploads the built tree with its digest; the deploy job never checks out or executes source-tree code, re-derives the digest from the downloaded artifact, revalidates the kill switch and the source, and only then runs wrangler against the artifact. Also: the Pages project name is validated as a slug and refused when it is the production project (trailing whitespace can no longer slip past an exact compare into a shell word), the project defaults to tss-shadow as recorded in docs/dns-architecture.md, the canonical-CI lookup filters by head_sha, the shadow ships robots.txt Disallow plus an X-Robots-Tag header alongside the HTML meta, the run fails unless tss.tinyland.dev serves the published source SHA within four minutes, the authority test pins the single trigger, the build/deploy split, workflow_id ci.yml and the step order, and AGENTS.md no longer says Cloudflare publication comes only from the production lane.
…iables API Review finding: GITHUB_TOKEN carries no Variables permission, so the getRepoVariable recheck copied from the sibling v2 lanes would 403 and refuse every publish after a full build. The deploy job is now gated with 'if: vars.BLOG_TSS_PUBLISH_ENABLED == true', which Actions resolves when the job is scheduled (after the build), and the pre-publish step keeps the source freshness checks (main head / PR still open at the exact SHA). The authority contract now forbids getRepoVariable in this lane.
2b6f2bd to
8929f27
Compare
8929f27 to
55f3865
Compare
Why
tss.tinyland.devis the operator acceptance URL and public noindex development shadow (TIN-3026), but nothing has published it since #251 retired the legacy shadow workflow on 2026-08-13 — it still serves the 2026-07-14 build (/_app/version.json=1784027104867). Today the operator ruled to iterate on tss before any production promotion; this is the separately authorized apply lane that makes that possible.What
.github/workflows/tss-shadow-publish-v2.yml—repository_dispatch: tss-shadow-publish-v2only. Resolves an exact SHA (currentmain, or an open same-repo PR head viasource_pr), requires a successful canonical CI run at that SHA withbuild-and-test+bazel-remote-gates, refuses unlessclient_payload.deploy == "true"andvars.BLOG_TSS_PUBLISH_ENABLED == "true". Builds withPUBLIC_DEPLOY_TIER=shadow, validates site-wide noindex + source-sha stamping, records the static digest, revalidates kill switch + source immediately beforewrangler pages deploy, and refuses the production project name (transscendsurvival-org). Publishes tovars.CLOUDFLARE_PAGES_TSS_PROJECT_NAME(required) onvars.CLOUDFLARE_PAGES_TSS_BRANCH(defaultmain).scripts/test-tss-shadow-publish-resolver.mjs— fixtures for every refusal path (wrong event/type, non-exact SHA, stale main, closed/fork/drifted PR, missing/red CI, red gates, deploy not requested, kill switch off).scripts/test-workflow-authority.mjs— pins the lane shape; forbids push/PR/workflow_run/workflow_dispatch triggers, production project/kill-switch reach, and unrelated mutation authority.BUILD.bazel— fixture added to//:workflow_authority_tests.AGENTS.mdregister row;docs/tss-shadow-publish.mdceremony.Not in this PR (operator ceremony)
tss.tinyland.dev→ repo varCLOUDFLARE_PAGES_TSS_PROJECT_NAME(no repo records it today).BLOG_TSS_PUBLISH_ENABLED=true, dispatch at current main, verifyversion.json+ thetinyland-source-shameta on every route.Merging this PR publishes nothing: the variable is absent (= false) and the lane has no non-dispatch trigger. Source-only under the TIN-2801 boundary.
Checks
node scripts/test-workflow-authority.mjs→ all 6 fixture suites pass incl. the new TSS resolver fixtures.Refs: TIN-3026, TIN-603, TIN-604, TIN-2727.