From b0aa51626561ab6e6af418a8e65de33c3d3e6c38 Mon Sep 17 00:00:00 2001 From: Jess Sullivan Date: Fri, 17 Jul 2026 18:51:13 -0400 Subject: [PATCH 1/5] docs(agents): codify blog delivery ownership (TIN-3026) --- AGENTS.md | 32 ++++++++- docs/blog-shadow-preview.md | 128 ++++++++++++++++++++---------------- 2 files changed, 102 insertions(+), 58 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 13ee45b..3eaab33 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -21,12 +21,42 @@ These rules apply before the writing-style guidance below. - Blog and Pulse broker streams are display/projection contracts only. Do not treat them as public Fediverse delivery, and do not weaken broker hydration tests just because prerendered fallback still paints. - `static/cv` is synced from the private `spear_resumes` repository. Preserve that boundary and use the existing sync/test path instead of hand-editing generated resume artifacts. +## Cross-Repo Delivery Ownership + +- This repo owns blog source, the static build, source-image/digest publication + and protected dispatch, and the production Cloudflare Pages contract. +- `Jesssullivan/jesssullivan-infra` owns the private tailnet acceptance + environment: dispatch and digest validation, private image mirroring, + RustFS-backed OpenTofu state, the `jesssullivan-blog-shadow` workload, apply + workflow, and Tailscale route. +- `https://jesssullivan-blog-shadow.taila4c78d.ts.net` is the approved active + exact-head acceptance and interactive QA route. The Cloudflare Pages shadow + workflow and `https://tss.tinyland.dev` remain active compatibility/build + surfaces, but they do not prove the digest-pinned private exact-head route. + Cloudflare Pages remains production serving. +- `tinyland-inc/GloriousFlywheel` supplies reusable runner/build, Nix/toolchain, + Bazel/cache/RBE, enrollment, and validation substrate. Passing GF checks or + running on GF substrate does not transfer application deployment ownership + to GF. +- `tinyland-inc/tinyland.dev` owns mothership content, broker, producer, and + federation contracts consumed by this spoke. +- `tinyland-inc/blahaj` may provide generic cluster receiver/admission + interfaces, but it does not own this blog's shadow workload or application + lifecycle. `tinyland-inc/lab` may escrow narrowly scoped credentials and + bootstrap hosts, but it is not an application deployment owner. +- Never infer application ownership from the cluster hosting a pod, the repo + escrowing a credential, or the runner executing a build. Follow the + source-to-digest-to-overlay chain above. + ## Build, Test, And Deploy - Normal local development is npm/SvelteKit: `npm ci`, `npm run build`, `npm run lint`, and focused scripts from `package.json`. - Production behavior changes require `npm run test:production-health`. That check covers public DNS, apex/`www` HTTPS, canonical redirects, slash variants, Tinyland broker contract, and browser hydration. - CI has two lanes. `build-and-test` runs hosted checks such as gitleaks, production dependency audit, lint, npm build, bundle reporting, and Lighthouse. `bazel-remote-gates` is the check/test/e2e authority. -- Cloudflare Pages shadow builds come from `.github/workflows/cloudflare-pages-shadow.yml`; PRs are build-only unless the workflow is explicitly eligible to deploy. GitHub Pages deploys come from `.github/workflows/deploy-pages.yml` and are still needed for rollback parity. +- `.github/workflows/cloudflare-pages-shadow.yml` remains the Cloudflare Pages + build/deploy lane for production and its compatibility shadow. It is not the + private exact-head acceptance authority. GitHub Pages deploys come from + `.github/workflows/deploy-pages.yml` and are still needed for rollback parity. - `.github/workflows/production-health.yml` runs every 30 minutes and sends ntfy alerts on failure. Treat a red scheduled monitor as production evidence, not noise. ## Bazel And GloriousFlywheel diff --git a/docs/blog-shadow-preview.md b/docs/blog-shadow-preview.md index 5ae7cef..87c1153 100644 --- a/docs/blog-shadow-preview.md +++ b/docs/blog-shadow-preview.md @@ -1,31 +1,51 @@ # Blog Shadow Preview -The live shadow preview route is owned by `Jesssullivan/jesssullivan-infra`, -but this repo owns the branch build that feeds it. +The approved active development and QA surface is the private exact-head +tailnet route. `Jesssullivan/jesssullivan-infra` owns that acceptance +environment; this repo owns the source and static build that feeds it. -Current review route: +Approved development and QA route: ```text https://jesssullivan-blog-shadow.taila4c78d.ts.net ``` +## Ownership + +- `Jesssullivan/jesssullivan.github.io` owns blog source, the static build, + source-image/digest publication, protected dispatch, and the Cloudflare Pages + production contract. +- `Jesssullivan/jesssullivan-infra` validates the dispatched source evidence + and digest, mirrors that digest privately, and owns the RustFS-backed + OpenTofu state, workload, apply, and private Tailscale route. +- GloriousFlywheel supplies reusable runner/build, Nix/toolchain, + Bazel/cache/RBE, enrollment, and validation substrate. It does not own this + application's deployment. +- Blahaj and Lab do not own this application. Cluster receiver/admission + substrate, credential escrow, or host bootstrap support does not transfer + source, workload, route, or deployment authority. + ## Automatic PR Flow -`.github/workflows/shadow-preview.yml` publishes the newest active same-repo PR -branch to the shared shadow route. +`.github/workflows/shadow-preview.yml` is the automatic exact-head lane for the +newest active same-repo PR branch on the shared shadow route. 1. A same-repo, non-draft PR against `main` is opened, marked ready, or updated. -2. The workflow builds `Dockerfile.shadow` on the configured source runner. - `BLOG_SHADOW_SOURCE_RUNNER` is currently `ubuntu-latest`; manual dispatch - retains `tinyland-dind` as its explicit default. +2. The workflow builds the static output through `Dockerfile.shadow` on the + configured source runner. `BLOG_SHADOW_SOURCE_RUNNER` is currently + `ubuntu-latest`; manual dispatch retains `tinyland-dind` as its explicit + default. 3. The workflow pushes the CI source artifact to - `ghcr.io/jesssullivan/jesssullivan-github-io-shadow-tailnet`. + `ghcr.io/jesssullivan/jesssullivan-github-io-shadow-tailnet` and publishes + the immutable OCI source digest as a job output. 4. A separate, no-checkout job enters the reviewer-gated `blog-shadow-dispatch` environment, mints a short-lived GitHub App token, and starts the named workflow in `Jesssullivan/jesssullivan-infra` with the - exact image digest and source metadata. -5. The private infra workflow mirrors that digest into the private operator - package and applies the RustFS-backed OpenTofu stack. + source repository, workflow run, commit SHA, correlation tag, and exact + source digest. +5. The private infra workflow validates that source workflow evidence and the + tag-to-digest binding, mirrors the exact digest into the private operator + package, and applies the RustFS-backed OpenTofu stack. 6. The public workflow follows the source-SHA/run-ID-correlated private run and fails unless that exact receiver run succeeds. @@ -35,6 +55,12 @@ private dispatches use separate `blog-shadow-preview-build` and true`. The private receiver also serializes applies, so the newest approved PR wins without draft resolve-only runs canceling an in-flight deploy. +The correlated receiver run proves the digest-pinned workload and canonical +service responses through a Kubernetes API port-forward, and requires the +Tailscale Ingress to report a MagicDNS hostname. Direct HTTPS against the +MagicDNS route is an independent tailnet-route QA check from a +tailnet-connected client. Neither proof substitutes for the other. + Fork PRs and draft PRs are ignored. Branch pushes are covered by the PR `synchronize` event so the workflow does not create duplicate push and PR check runs for the same commit. @@ -65,51 +91,39 @@ and is currently `ubuntu-latest`. Manual dispatch can select `tinyland-dind` when an ARC source-build proof is required. A hosted build does not prove the ARC source-build lane. The correlated receiver result still separately proves private GHCR mirroring, -RustFS-backed OpenTofu apply, and tailnet smoke. - -## Manual Shadow Image Build - -`.github/workflows/shadow-image.yml` still supports the older -`shadow-deploy/**` branch flow for explicit operator builds. That workflow only -builds the source image; the private mirror and apply are handled by infra. It -uses the `tinyland-dind` ARC runner by default and accepts the same -`BLOG_SHADOW_SOURCE_RUNNER=ubuntu-latest` fallback, or manual dispatch -`source_runner=ubuntu-latest`, when the ARC source-image lane is unavailable. - -## Cloudflare Pages Shadow - -`.github/workflows/cloudflare-pages-shadow.yml` builds the same static -SvelteKit output and can publish it to Cloudflare Pages by Direct Upload. - -This workflow exercises a separate Cloudflare Pages shadow without changing -production DNS. `https://transscendsurvival.org` is served by Cloudflare Pages; -`https://tss.tinyland.dev` remains the review shadow. The built site is still -static, but current `/blog`, -`/blog/[slug]`, and `/pulse` client code may hydrate from public -`hub.tinyland.dev` broker endpoints at runtime when those endpoints are -available. - -Required repository secrets: - -| Secret | Purpose | -|---|---| -| `CLOUDFLARE_ACCOUNT_ID` | Cloudflare account that owns the Pages project | -| `CLOUDFLARE_API_TOKEN` | Token with Cloudflare Pages edit/deploy access | - -Optional repository variable: - -| Variable | Default | Purpose | -|---|---|---| -| `CLOUDFLARE_PAGES_PROJECT_NAME` | `transscendsurvival-org` | Cloudflare Pages project name | - -PRs and missing-credential runs build and validate the static site, then skip -the Cloudflare deploy with an explicit notice. Manual dispatch accepts -`require_deploy=true` when the operator wants missing credentials to fail -instead of skip. - -Cloudflare Pages is the production serving authority. GitHub Pages remains the -rollback publisher and parity path. Browser validation remains in GitHub -Actions or an approved remote lane. Do not run local Playwright for this slice. +RustFS-backed OpenTofu apply, exact-head runtime attestation, canonical service +smoke, and Tailscale Ingress status. Direct MagicDNS HTTPS remains the +independent tailnet-route QA proof described above. + +## Legacy Manual Shadow Image Build + +`.github/workflows/shadow-image.yml` is a legacy/manual, tag-correlated source +build for `shadow-deploy/**` branches or explicit workflow dispatch. It pushes +a tagged source image only. It does not dispatch +`Jesssullivan/jesssullivan-infra`, and it is not the automatic exact-head lane. + +An operator using this legacy lane must resolve the published tag to an +immutable source digest, mirror that exact digest into the private operator +package with infra-owned authority, and deliberately apply the infra-owned +stack with the digest, source SHA, and source-run correlation. The workflow +uses the `tinyland-dind` ARC runner by default and accepts +`BLOG_SHADOW_SOURCE_RUNNER=ubuntu-latest`, or manual dispatch with +`source_runner=ubuntu-latest`, when the ARC source-build lane is unavailable. + +## Cloudflare Pages Compatibility Shadow + +`.github/workflows/cloudflare-pages-shadow.yml` and +`https://tss.tinyland.dev` remain active Cloudflare Pages build/deploy and +compatibility-shadow surfaces. They exercise the static Pages artifact and are +still referenced by machine production-consumer checks. They do not prove the +private mirror, infra-owned state/apply, digest-pinned workload, or current +MagicDNS route, so do not cite them as exact-head acceptance evidence. + +Cloudflare Pages remains the production serving authority for +`https://transscendsurvival.org` and `https://www.transscendsurvival.org`. +GitHub Pages remains the rollback publisher and parity path. The approved +exact-head acceptance and interactive QA target is the private tailnet route +documented above. ## Pulse Client Smoke From da110d40574abc162721bc80009883bf3f97b5fb Mon Sep 17 00:00:00 2001 From: Jess Sullivan Date: Fri, 17 Jul 2026 19:12:09 -0400 Subject: [PATCH 2/5] fix: bind shadow handoff to immutable source evidence --- .github/workflows/shadow-image.yml | 53 +++++++++++++++++--- AGENTS.md | 5 +- BUILD.bazel | 8 +++ docs/blog-shadow-preview.md | 7 +-- scripts/test-shadow-image-digest-contract.sh | 44 ++++++++++++++++ 5 files changed, 106 insertions(+), 11 deletions(-) create mode 100755 scripts/test-shadow-image-digest-contract.sh diff --git a/.github/workflows/shadow-image.yml b/.github/workflows/shadow-image.yml index c9eb86e..1e78598 100644 --- a/.github/workflows/shadow-image.yml +++ b/.github/workflows/shadow-image.yml @@ -80,6 +80,21 @@ jobs: with: ref: ${{ inputs.ref || github.ref }} + - name: Resolve selected source identity + id: source + run: | + set -euo pipefail + source_sha="$(git rev-parse HEAD)" + if [[ ! "${source_sha}" =~ ^[0-9a-f]{40}$ ]]; then + echo "::error::Selected source did not resolve to a full commit SHA." + exit 1 + fi + { + echo "sha=${source_sha}" + echo "short_sha=${source_sha:0:12}" + echo "workflow_run_id=${GITHUB_RUN_ID}" + } >> "$GITHUB_OUTPUT" + - name: Compute image tag id: tag run: | @@ -89,7 +104,7 @@ jobs: if [ -z "${REF_SLUG}" ]; then REF_SLUG="manual" fi - SHA_SHORT="${GITHUB_SHA:0:12}" + SHA_SHORT="${{ steps.source.outputs.short_sha }}" TAG="shadow-${REF_SLUG}-${SHA_SHORT}-amd64" echo "tag=${TAG}" >> "$GITHUB_OUTPUT" echo "Computed tag: ${TAG}" @@ -105,6 +120,7 @@ jobs: password: ${{ secrets.GITHUB_TOKEN }} - name: Build and push + id: source_image uses: docker/build-push-action@v7 with: context: . @@ -116,6 +132,18 @@ jobs: cache-to: type=gha,mode=max provenance: false + - name: Validate immutable source evidence + id: evidence + env: + SOURCE_DIGEST: ${{ steps.source_image.outputs.digest }} + run: | + set -euo pipefail + if [[ ! "${SOURCE_DIGEST}" =~ ^sha256:[0-9a-f]{64}$ ]]; then + echo "::error::Source image did not publish a valid immutable OCI digest." + exit 1 + fi + echo "source_digest=${SOURCE_DIGEST}" >> "$GITHUB_OUTPUT" + - name: Trivy scan (report-only for M1.5) # v0.36.0; keep this action pinned because the shadow lane has deploy credentials nearby. uses: aquasecurity/trivy-action@ed142fd0673e97e23eac54620cfb913e5ce36c25 @@ -129,13 +157,20 @@ jobs: ignore-unfixed: true - name: Summary + env: + SUMMARY_IMAGE_TAG: ${{ steps.tag.outputs.tag }} + SUMMARY_SOURCE_DIGEST: ${{ steps.evidence.outputs.source_digest }} + SUMMARY_SOURCE_SHA: ${{ steps.source.outputs.sha }} + SUMMARY_SOURCE_RUN_ID: ${{ steps.source.outputs.workflow_run_id }} run: | { echo "## Shadow image build" echo "" - echo "- Image: \`${{ env.IMAGE_NAME }}:${{ steps.tag.outputs.tag }}\`" + echo "- Image tag: \`${IMAGE_NAME}:${SUMMARY_IMAGE_TAG}\`" + echo "- Source digest: \`${SUMMARY_SOURCE_DIGEST}\`" echo "- Branch: \`${{ github.ref_name }}\`" - echo "- Commit: \`${{ github.sha }}\`" + echo "- Selected commit: \`${SUMMARY_SOURCE_SHA}\`" + echo "- Source workflow run: \`${SUMMARY_SOURCE_RUN_ID}\`" echo "- Source runner: \`${{ needs.resolve.outputs.source_runner }}\`" echo "" echo "### Private package mirror" @@ -145,8 +180,11 @@ jobs: echo "\`\`\`bash" echo "gh auth token | docker login ghcr.io -u Jesssullivan --password-stdin" echo "docker buildx imagetools create \\" - echo " -t ghcr.io/jesssullivan/jesssullivan-github-io-shadow-operator:${{ steps.tag.outputs.tag }} \\" - echo " ${{ env.IMAGE_NAME }}:${{ steps.tag.outputs.tag }}" + echo " -t ghcr.io/jesssullivan/jesssullivan-github-io-shadow-operator:${SUMMARY_IMAGE_TAG} \\" + echo " ${IMAGE_NAME}@${SUMMARY_SOURCE_DIGEST}" + echo "operator_digest=\"\$(docker buildx imagetools inspect \\" + echo " ghcr.io/jesssullivan/jesssullivan-github-io-shadow-operator:${SUMMARY_IMAGE_TAG} \\" + echo " --format '{{json .Manifest.Digest}}' | tr -d '\"')\"" echo "docker logout ghcr.io" echo "\`\`\`" echo "" @@ -154,7 +192,10 @@ jobs: echo "From \`~/git/jesssullivan-infra/\`:" echo "" echo "\`\`\`bash" - echo "export TF_VAR_image_tag=${{ steps.tag.outputs.tag }}" + echo "export TF_VAR_image_tag=${SUMMARY_IMAGE_TAG}" + echo "export TF_VAR_image_digest=\"\${operator_digest}\"" + echo "export TF_VAR_source_sha=${SUMMARY_SOURCE_SHA}" + echo "export TF_VAR_source_workflow_run_id=${SUMMARY_SOURCE_RUN_ID}" echo "export TF_VAR_ghcr_token=\"\$(operator-token-source)\"" echo "export TF_VAR_ghcr_username=\"jesssullivan\"" echo "just blog-shadow-plan" diff --git a/AGENTS.md b/AGENTS.md index 3eaab33..9cbf988 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -40,8 +40,9 @@ These rules apply before the writing-style guidance below. to GF. - `tinyland-inc/tinyland.dev` owns mothership content, broker, producer, and federation contracts consumed by this spoke. -- `tinyland-inc/blahaj` may provide generic cluster receiver/admission - interfaces, but it does not own this blog's shadow workload or application +- `tinyland-inc/blahaj` owns cluster-side admission and enforcement. Its + app-specific receivers and reapers are limited to the canonical adopted-live + exception register; none owns this blog's shadow workload or application lifecycle. `tinyland-inc/lab` may escrow narrowly scoped credentials and bootstrap hosts, but it is not an application deployment owner. - Never infer application ownership from the cluster hosting a pod, the repo diff --git a/BUILD.bazel b/BUILD.bazel index f7bce87..3341d36 100644 --- a/BUILD.bazel +++ b/BUILD.bazel @@ -312,6 +312,13 @@ sh_test( visibility = ["//visibility:public"], ) +sh_test( + name = "shadow_image_digest_contract", + srcs = ["scripts/test-shadow-image-digest-contract.sh"], + data = [".github/workflows/shadow-image.yml"], + visibility = ["//visibility:public"], +) + sh_test( name = "cloudflare_cache_purge_contract", srcs = ["scripts/test-cloudflare-cache-purge-contract.sh"], @@ -371,6 +378,7 @@ exports_files( "scripts/serve-static-build.mjs", "scripts/test-cloudflare-cache-purge-contract.sh", "scripts/test-held-post-edge-404.mjs", + "scripts/test-shadow-image-digest-contract.sh", "scripts/test-shadow-preview-digest-contract.sh", "scripts/test-bazel-cache-backed.sh", "scripts/run-vitest-bazel.mjs", diff --git a/docs/blog-shadow-preview.md b/docs/blog-shadow-preview.md index 87c1153..96d8de8 100644 --- a/docs/blog-shadow-preview.md +++ b/docs/blog-shadow-preview.md @@ -21,9 +21,10 @@ https://jesssullivan-blog-shadow.taila4c78d.ts.net - GloriousFlywheel supplies reusable runner/build, Nix/toolchain, Bazel/cache/RBE, enrollment, and validation substrate. It does not own this application's deployment. -- Blahaj and Lab do not own this application. Cluster receiver/admission - substrate, credential escrow, or host bootstrap support does not transfer - source, workload, route, or deployment authority. +- Blahaj and Lab do not own this application. Blahaj's cluster admission and + enforcement, including only its canonical adopted-live receiver/reaper + exceptions, does not transfer source, workload, route, or deployment + authority. Neither does Lab credential escrow or host bootstrap support. ## Automatic PR Flow diff --git a/scripts/test-shadow-image-digest-contract.sh b/scripts/test-shadow-image-digest-contract.sh new file mode 100755 index 0000000..5d20ddd --- /dev/null +++ b/scripts/test-shadow-image-digest-contract.sh @@ -0,0 +1,44 @@ +#!/usr/bin/env bash +# shellcheck disable=SC2016 # GitHub expressions below are literal source contracts. +set -euo pipefail + +if [[ -n "${TEST_SRCDIR:-}" && -n "${TEST_WORKSPACE:-}" ]]; then + workspace="${TEST_SRCDIR}/${TEST_WORKSPACE}" +else + workspace="${BUILD_WORKSPACE_DIRECTORY:-$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)}" +fi +workflow="${workspace}/.github/workflows/shadow-image.yml" + +require_fixed() { + local needle="$1" + local label="$2" + + if ! grep -Fq -- "${needle}" "${workflow}"; then + echo "ERROR: shadow image workflow missing ${label}: ${needle}" >&2 + exit 1 + fi +} + +require_fixed "id: source" "selected-source identity step" +require_fixed 'source_sha="$(git rev-parse HEAD)"' "checked-out commit binding" +require_fixed 'SHA_SHORT="${{ steps.source.outputs.short_sha }}"' "tag-to-selected-source binding" +require_fixed "id: source_image" "build digest output" +require_fixed 'SOURCE_DIGEST: ${{ steps.source_image.outputs.digest }}' "source digest propagation" +require_fixed '[[ ! "${SOURCE_DIGEST}" =~ ^sha256:[0-9a-f]{64}$ ]]' "source digest validation" +require_fixed 'echo " ${IMAGE_NAME}@${SUMMARY_SOURCE_DIGEST}"' "digest-pinned private mirror" +require_fixed 'echo "operator_digest=\"\$(docker buildx imagetools inspect \\"' "operator digest resolution" +require_fixed 'echo "export TF_VAR_image_digest=\"\${operator_digest}\""' "operator digest apply evidence" +require_fixed 'echo "export TF_VAR_source_sha=${SUMMARY_SOURCE_SHA}"' "source SHA apply evidence" +require_fixed 'echo "export TF_VAR_source_workflow_run_id=${SUMMARY_SOURCE_RUN_ID}"' "source run apply evidence" + +if grep -Fq -- 'SHA_SHORT="${GITHUB_SHA:0:12}"' "${workflow}"; then + echo "ERROR: shadow image tag still binds to the dispatch ref instead of the selected checkout" >&2 + exit 1 +fi + +if grep -Fq -- '${IMAGE_NAME}:${SUMMARY_IMAGE_TAG}"' "${workflow}"; then + echo "ERROR: private mirror source must be digest-pinned, not tag-only" >&2 + exit 1 +fi + +echo "shadow image digest contract passed" From 03ba8f239c6b56f1b2014c081ed784d3e768f490 Mon Sep 17 00:00:00 2001 From: Jess Sullivan Date: Fri, 17 Jul 2026 23:24:07 -0400 Subject: [PATCH 3/5] docs: correct cross-repo delivery ownership (TIN-3026) --- .github/workflows/shadow-image.yml | 53 +++----------------- AGENTS.md | 19 ++++--- BUILD.bazel | 8 --- docs/blog-shadow-preview.md | 24 +++++++-- scripts/test-shadow-image-digest-contract.sh | 44 ---------------- 5 files changed, 37 insertions(+), 111 deletions(-) delete mode 100755 scripts/test-shadow-image-digest-contract.sh diff --git a/.github/workflows/shadow-image.yml b/.github/workflows/shadow-image.yml index 1e78598..c9eb86e 100644 --- a/.github/workflows/shadow-image.yml +++ b/.github/workflows/shadow-image.yml @@ -80,21 +80,6 @@ jobs: with: ref: ${{ inputs.ref || github.ref }} - - name: Resolve selected source identity - id: source - run: | - set -euo pipefail - source_sha="$(git rev-parse HEAD)" - if [[ ! "${source_sha}" =~ ^[0-9a-f]{40}$ ]]; then - echo "::error::Selected source did not resolve to a full commit SHA." - exit 1 - fi - { - echo "sha=${source_sha}" - echo "short_sha=${source_sha:0:12}" - echo "workflow_run_id=${GITHUB_RUN_ID}" - } >> "$GITHUB_OUTPUT" - - name: Compute image tag id: tag run: | @@ -104,7 +89,7 @@ jobs: if [ -z "${REF_SLUG}" ]; then REF_SLUG="manual" fi - SHA_SHORT="${{ steps.source.outputs.short_sha }}" + SHA_SHORT="${GITHUB_SHA:0:12}" TAG="shadow-${REF_SLUG}-${SHA_SHORT}-amd64" echo "tag=${TAG}" >> "$GITHUB_OUTPUT" echo "Computed tag: ${TAG}" @@ -120,7 +105,6 @@ jobs: password: ${{ secrets.GITHUB_TOKEN }} - name: Build and push - id: source_image uses: docker/build-push-action@v7 with: context: . @@ -132,18 +116,6 @@ jobs: cache-to: type=gha,mode=max provenance: false - - name: Validate immutable source evidence - id: evidence - env: - SOURCE_DIGEST: ${{ steps.source_image.outputs.digest }} - run: | - set -euo pipefail - if [[ ! "${SOURCE_DIGEST}" =~ ^sha256:[0-9a-f]{64}$ ]]; then - echo "::error::Source image did not publish a valid immutable OCI digest." - exit 1 - fi - echo "source_digest=${SOURCE_DIGEST}" >> "$GITHUB_OUTPUT" - - name: Trivy scan (report-only for M1.5) # v0.36.0; keep this action pinned because the shadow lane has deploy credentials nearby. uses: aquasecurity/trivy-action@ed142fd0673e97e23eac54620cfb913e5ce36c25 @@ -157,20 +129,13 @@ jobs: ignore-unfixed: true - name: Summary - env: - SUMMARY_IMAGE_TAG: ${{ steps.tag.outputs.tag }} - SUMMARY_SOURCE_DIGEST: ${{ steps.evidence.outputs.source_digest }} - SUMMARY_SOURCE_SHA: ${{ steps.source.outputs.sha }} - SUMMARY_SOURCE_RUN_ID: ${{ steps.source.outputs.workflow_run_id }} run: | { echo "## Shadow image build" echo "" - echo "- Image tag: \`${IMAGE_NAME}:${SUMMARY_IMAGE_TAG}\`" - echo "- Source digest: \`${SUMMARY_SOURCE_DIGEST}\`" + echo "- Image: \`${{ env.IMAGE_NAME }}:${{ steps.tag.outputs.tag }}\`" echo "- Branch: \`${{ github.ref_name }}\`" - echo "- Selected commit: \`${SUMMARY_SOURCE_SHA}\`" - echo "- Source workflow run: \`${SUMMARY_SOURCE_RUN_ID}\`" + echo "- Commit: \`${{ github.sha }}\`" echo "- Source runner: \`${{ needs.resolve.outputs.source_runner }}\`" echo "" echo "### Private package mirror" @@ -180,11 +145,8 @@ jobs: echo "\`\`\`bash" echo "gh auth token | docker login ghcr.io -u Jesssullivan --password-stdin" echo "docker buildx imagetools create \\" - echo " -t ghcr.io/jesssullivan/jesssullivan-github-io-shadow-operator:${SUMMARY_IMAGE_TAG} \\" - echo " ${IMAGE_NAME}@${SUMMARY_SOURCE_DIGEST}" - echo "operator_digest=\"\$(docker buildx imagetools inspect \\" - echo " ghcr.io/jesssullivan/jesssullivan-github-io-shadow-operator:${SUMMARY_IMAGE_TAG} \\" - echo " --format '{{json .Manifest.Digest}}' | tr -d '\"')\"" + echo " -t ghcr.io/jesssullivan/jesssullivan-github-io-shadow-operator:${{ steps.tag.outputs.tag }} \\" + echo " ${{ env.IMAGE_NAME }}:${{ steps.tag.outputs.tag }}" echo "docker logout ghcr.io" echo "\`\`\`" echo "" @@ -192,10 +154,7 @@ jobs: echo "From \`~/git/jesssullivan-infra/\`:" echo "" echo "\`\`\`bash" - echo "export TF_VAR_image_tag=${SUMMARY_IMAGE_TAG}" - echo "export TF_VAR_image_digest=\"\${operator_digest}\"" - echo "export TF_VAR_source_sha=${SUMMARY_SOURCE_SHA}" - echo "export TF_VAR_source_workflow_run_id=${SUMMARY_SOURCE_RUN_ID}" + echo "export TF_VAR_image_tag=${{ steps.tag.outputs.tag }}" echo "export TF_VAR_ghcr_token=\"\$(operator-token-source)\"" echo "export TF_VAR_ghcr_username=\"jesssullivan\"" echo "just blog-shadow-plan" diff --git a/AGENTS.md b/AGENTS.md index 9cbf988..ecd94ed 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -26,9 +26,9 @@ These rules apply before the writing-style guidance below. - This repo owns blog source, the static build, source-image/digest publication and protected dispatch, and the production Cloudflare Pages contract. - `Jesssullivan/jesssullivan-infra` owns the private tailnet acceptance - environment: dispatch and digest validation, private image mirroring, - RustFS-backed OpenTofu state, the `jesssullivan-blog-shadow` workload, apply - workflow, and Tailscale route. + environment: dispatch validation and digest validation, private image + mirroring, RustFS-backed OpenTofu state, the `jesssullivan-blog-shadow` + workload, protected apply workflow, and Tailscale route. - `https://jesssullivan-blog-shadow.taila4c78d.ts.net` is the approved active exact-head acceptance and interactive QA route. The Cloudflare Pages shadow workflow and `https://tss.tinyland.dev` remain active compatibility/build @@ -40,11 +40,14 @@ These rules apply before the writing-style guidance below. to GF. - `tinyland-inc/tinyland.dev` owns mothership content, broker, producer, and federation contracts consumed by this spoke. -- `tinyland-inc/blahaj` owns cluster-side admission and enforcement. Its - app-specific receivers and reapers are limited to the canonical adopted-live - exception register; none owns this blog's shadow workload or application - lifecycle. `tinyland-inc/lab` may escrow narrowly scoped credentials and - bootstrap hosts, but it is not an application deployment owner. +- `tinyland-inc/blahaj` is the bounded infrastructure receiver and owns + cluster-side admission, RBAC, placement, storage, + DNS/certificate/tunnel enforcement, and state contracts. Its app-specific + receivers and reapers are limited to the canonical adopted-live exception + register; none owns this blog's shadow workload, apply decision, or + application lifecycle. `tinyland-inc/lab` may bootstrap hosts, enforce + runtime policy and operator preflights, and project narrowly scoped + credentials, but it is not an application deployment owner. - Never infer application ownership from the cluster hosting a pod, the repo escrowing a credential, or the runner executing a build. Follow the source-to-digest-to-overlay chain above. diff --git a/BUILD.bazel b/BUILD.bazel index 3341d36..f7bce87 100644 --- a/BUILD.bazel +++ b/BUILD.bazel @@ -312,13 +312,6 @@ sh_test( visibility = ["//visibility:public"], ) -sh_test( - name = "shadow_image_digest_contract", - srcs = ["scripts/test-shadow-image-digest-contract.sh"], - data = [".github/workflows/shadow-image.yml"], - visibility = ["//visibility:public"], -) - sh_test( name = "cloudflare_cache_purge_contract", srcs = ["scripts/test-cloudflare-cache-purge-contract.sh"], @@ -378,7 +371,6 @@ exports_files( "scripts/serve-static-build.mjs", "scripts/test-cloudflare-cache-purge-contract.sh", "scripts/test-held-post-edge-404.mjs", - "scripts/test-shadow-image-digest-contract.sh", "scripts/test-shadow-preview-digest-contract.sh", "scripts/test-bazel-cache-backed.sh", "scripts/run-vitest-bazel.mjs", diff --git a/docs/blog-shadow-preview.md b/docs/blog-shadow-preview.md index 96d8de8..06273f7 100644 --- a/docs/blog-shadow-preview.md +++ b/docs/blog-shadow-preview.md @@ -21,10 +21,12 @@ https://jesssullivan-blog-shadow.taila4c78d.ts.net - GloriousFlywheel supplies reusable runner/build, Nix/toolchain, Bazel/cache/RBE, enrollment, and validation substrate. It does not own this application's deployment. -- Blahaj and Lab do not own this application. Blahaj's cluster admission and - enforcement, including only its canonical adopted-live receiver/reaper - exceptions, does not transfer source, workload, route, or deployment - authority. Neither does Lab credential escrow or host bootstrap support. +- Blahaj and Lab do not own this application. Blahaj is the bounded + infrastructure receiver for cluster admission, RBAC, placement, storage, + DNS/certificate/tunnel enforcement, and state contracts; its canonical + adopted-live receiver/reaper exceptions do not transfer source, workload, + route, apply-decision, or lifecycle authority. Neither do Lab host bootstrap, + runtime policy, operator preflight, or scoped credential projection. ## Automatic PR Flow @@ -120,6 +122,20 @@ still referenced by machine production-consumer checks. They do not prove the private mirror, infra-owned state/apply, digest-pinned workload, or current MagicDNS route, so do not cite them as exact-head acceptance evidence. +The workflow uses these repository credentials and configuration: + +| Kind | Name | Purpose | +|---|---|---| +| Secret | `CLOUDFLARE_ACCOUNT_ID` | Cloudflare account that owns the Pages project | +| Secret | `CLOUDFLARE_API_TOKEN` | Token with Cloudflare Pages edit/deploy access | +| Variable | `CLOUDFLARE_PAGES_PROJECT_NAME` | Pages project name; defaults to `transscendsurvival-org` | + +PRs and missing-credential runs build and validate the static artifact, then +skip the Cloudflare deploy with an explicit notice. Manual dispatch accepts +`require_deploy=true` when an operator deliberately wants missing credentials +to fail instead of skip. This remains compatibility and production-serving +machinery, not the interactive PR QA route. + Cloudflare Pages remains the production serving authority for `https://transscendsurvival.org` and `https://www.transscendsurvival.org`. GitHub Pages remains the rollback publisher and parity path. The approved diff --git a/scripts/test-shadow-image-digest-contract.sh b/scripts/test-shadow-image-digest-contract.sh deleted file mode 100755 index 5d20ddd..0000000 --- a/scripts/test-shadow-image-digest-contract.sh +++ /dev/null @@ -1,44 +0,0 @@ -#!/usr/bin/env bash -# shellcheck disable=SC2016 # GitHub expressions below are literal source contracts. -set -euo pipefail - -if [[ -n "${TEST_SRCDIR:-}" && -n "${TEST_WORKSPACE:-}" ]]; then - workspace="${TEST_SRCDIR}/${TEST_WORKSPACE}" -else - workspace="${BUILD_WORKSPACE_DIRECTORY:-$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)}" -fi -workflow="${workspace}/.github/workflows/shadow-image.yml" - -require_fixed() { - local needle="$1" - local label="$2" - - if ! grep -Fq -- "${needle}" "${workflow}"; then - echo "ERROR: shadow image workflow missing ${label}: ${needle}" >&2 - exit 1 - fi -} - -require_fixed "id: source" "selected-source identity step" -require_fixed 'source_sha="$(git rev-parse HEAD)"' "checked-out commit binding" -require_fixed 'SHA_SHORT="${{ steps.source.outputs.short_sha }}"' "tag-to-selected-source binding" -require_fixed "id: source_image" "build digest output" -require_fixed 'SOURCE_DIGEST: ${{ steps.source_image.outputs.digest }}' "source digest propagation" -require_fixed '[[ ! "${SOURCE_DIGEST}" =~ ^sha256:[0-9a-f]{64}$ ]]' "source digest validation" -require_fixed 'echo " ${IMAGE_NAME}@${SUMMARY_SOURCE_DIGEST}"' "digest-pinned private mirror" -require_fixed 'echo "operator_digest=\"\$(docker buildx imagetools inspect \\"' "operator digest resolution" -require_fixed 'echo "export TF_VAR_image_digest=\"\${operator_digest}\""' "operator digest apply evidence" -require_fixed 'echo "export TF_VAR_source_sha=${SUMMARY_SOURCE_SHA}"' "source SHA apply evidence" -require_fixed 'echo "export TF_VAR_source_workflow_run_id=${SUMMARY_SOURCE_RUN_ID}"' "source run apply evidence" - -if grep -Fq -- 'SHA_SHORT="${GITHUB_SHA:0:12}"' "${workflow}"; then - echo "ERROR: shadow image tag still binds to the dispatch ref instead of the selected checkout" >&2 - exit 1 -fi - -if grep -Fq -- '${IMAGE_NAME}:${SUMMARY_IMAGE_TAG}"' "${workflow}"; then - echo "ERROR: private mirror source must be digest-pinned, not tag-only" >&2 - exit 1 -fi - -echo "shadow image digest contract passed" From 75454ae12807e98258636ab3451292848e557271 Mon Sep 17 00:00:00 2001 From: Jess Sullivan Date: Sat, 18 Jul 2026 00:31:25 -0400 Subject: [PATCH 4/5] docs: distinguish blog compatibility surfaces (TIN-3026) --- AGENTS.md | 6 ++++-- docs/blog-shadow-preview.md | 20 ++++++++++++-------- 2 files changed, 16 insertions(+), 10 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index ecd94ed..f1fb6b0 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -31,8 +31,10 @@ These rules apply before the writing-style guidance below. workload, protected apply workflow, and Tailscale route. - `https://jesssullivan-blog-shadow.taila4c78d.ts.net` is the approved active exact-head acceptance and interactive QA route. The Cloudflare Pages shadow - workflow and `https://tss.tinyland.dev` remain active compatibility/build - surfaces, but they do not prove the digest-pinned private exact-head route. + workflow targets the `transscendsurvival-org` project and its + `https://tss.ephemera.tinyland.dev` compatibility route. + `https://tss.tinyland.dev` belongs to the separately deployed `tss-shadow` + project. Neither proves the digest-pinned private exact-head route. Cloudflare Pages remains production serving. - `tinyland-inc/GloriousFlywheel` supplies reusable runner/build, Nix/toolchain, Bazel/cache/RBE, enrollment, and validation substrate. Passing GF checks or diff --git a/docs/blog-shadow-preview.md b/docs/blog-shadow-preview.md index 06273f7..c3854b4 100644 --- a/docs/blog-shadow-preview.md +++ b/docs/blog-shadow-preview.md @@ -113,14 +113,17 @@ uses the `tinyland-dind` ARC runner by default and accepts `BLOG_SHADOW_SOURCE_RUNNER=ubuntu-latest`, or manual dispatch with `source_runner=ubuntu-latest`, when the ARC source-build lane is unavailable. -## Cloudflare Pages Compatibility Shadow +## Cloudflare Pages Compatibility Surfaces `.github/workflows/cloudflare-pages-shadow.yml` and -`https://tss.tinyland.dev` remain active Cloudflare Pages build/deploy and -compatibility-shadow surfaces. They exercise the static Pages artifact and are -still referenced by machine production-consumer checks. They do not prove the -private mirror, infra-owned state/apply, digest-pinned workload, or current -MagicDNS route, so do not cite them as exact-head acceptance evidence. +`https://tss.ephemera.tinyland.dev` are the `transscendsurvival-org` Pages +build/deploy and compatibility-shadow surfaces. The workflow exercises the +static Pages artifact and is still referenced by machine production-consumer +checks. `https://tss.tinyland.dev` belongs to the separate `tss-shadow` Pages +project and can go stale until that project is deliberately redeployed. None +of these surfaces proves the private mirror, infra-owned state/apply, +digest-pinned workload, or current MagicDNS route, so do not cite them as +exact-head acceptance evidence. The workflow uses these repository credentials and configuration: @@ -161,5 +164,6 @@ curl -fsSIL "$SHADOW/pulse/client" curl -fsSL "$SHADOW/data/pulse/public-snapshot.v1.json" ``` -Browser validation for the client route stays in hosted GitHub Actions. Do not -run Playwright locally. +Browser validation for the client route stays in the GF-backed +`bazel-remote-gates` GitHub Actions job on `tinyland-dind`. Do not run +Playwright locally. From 76e64704c31906d0c12045b17afb6601997e129a Mon Sep 17 00:00:00 2001 From: Jess Sullivan Date: Sat, 18 Jul 2026 01:15:30 -0400 Subject: [PATCH 5/5] docs: distinguish production Pages from legacy QA routes --- AGENTS.md | 24 ++++++++++++++---------- docs/blog-shadow-preview.md | 26 ++++++++++++++------------ 2 files changed, 28 insertions(+), 22 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index f1fb6b0..4e511c5 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -30,12 +30,14 @@ These rules apply before the writing-style guidance below. mirroring, RustFS-backed OpenTofu state, the `jesssullivan-blog-shadow` workload, protected apply workflow, and Tailscale route. - `https://jesssullivan-blog-shadow.taila4c78d.ts.net` is the approved active - exact-head acceptance and interactive QA route. The Cloudflare Pages shadow - workflow targets the `transscendsurvival-org` project and its - `https://tss.ephemera.tinyland.dev` compatibility route. - `https://tss.tinyland.dev` belongs to the separately deployed `tss-shadow` - project. Neither proves the digest-pinned private exact-head route. - Cloudflare Pages remains production serving. + exact-head acceptance and interactive QA route. Despite its stale name, + `.github/workflows/cloudflare-pages-shadow.yml` publishes the production + `transscendsurvival-org` Pages project on eligible `main` or manual runs; + pull-request runs are build-only. `https://tss.ephemera.tinyland.dev` and + the separately deployed `https://tss.tinyland.dev` still serve legacy + compatibility content, but both are retired as QA routes. Neither proves the + digest-pinned private exact-head route. Cloudflare Pages remains production + serving. - `tinyland-inc/GloriousFlywheel` supplies reusable runner/build, Nix/toolchain, Bazel/cache/RBE, enrollment, and validation substrate. Passing GF checks or running on GF substrate does not transfer application deployment ownership @@ -59,10 +61,12 @@ These rules apply before the writing-style guidance below. - Normal local development is npm/SvelteKit: `npm ci`, `npm run build`, `npm run lint`, and focused scripts from `package.json`. - Production behavior changes require `npm run test:production-health`. That check covers public DNS, apex/`www` HTTPS, canonical redirects, slash variants, Tinyland broker contract, and browser hydration. - CI has two lanes. `build-and-test` runs hosted checks such as gitleaks, production dependency audit, lint, npm build, bundle reporting, and Lighthouse. `bazel-remote-gates` is the check/test/e2e authority. -- `.github/workflows/cloudflare-pages-shadow.yml` remains the Cloudflare Pages - build/deploy lane for production and its compatibility shadow. It is not the - private exact-head acceptance authority. GitHub Pages deploys come from - `.github/workflows/deploy-pages.yml` and are still needed for rollback parity. +- `.github/workflows/cloudflare-pages-shadow.yml` is the stale-named + Cloudflare Pages production build/deploy lane; pull requests build without + deploying. It does not make either legacy `tss` route an active QA surface + and is not the private exact-head acceptance authority. GitHub Pages deploys + come from `.github/workflows/deploy-pages.yml` and are still needed for + rollback parity. - `.github/workflows/production-health.yml` runs every 30 minutes and sends ntfy alerts on failure. Treat a red scheduled monitor as production evidence, not noise. ## Bazel And GloriousFlywheel diff --git a/docs/blog-shadow-preview.md b/docs/blog-shadow-preview.md index c3854b4..aed4386 100644 --- a/docs/blog-shadow-preview.md +++ b/docs/blog-shadow-preview.md @@ -113,15 +113,16 @@ uses the `tinyland-dind` ARC runner by default and accepts `BLOG_SHADOW_SOURCE_RUNNER=ubuntu-latest`, or manual dispatch with `source_runner=ubuntu-latest`, when the ARC source-build lane is unavailable. -## Cloudflare Pages Compatibility Surfaces - -`.github/workflows/cloudflare-pages-shadow.yml` and -`https://tss.ephemera.tinyland.dev` are the `transscendsurvival-org` Pages -build/deploy and compatibility-shadow surfaces. The workflow exercises the -static Pages artifact and is still referenced by machine production-consumer -checks. `https://tss.tinyland.dev` belongs to the separate `tss-shadow` Pages -project and can go stale until that project is deliberately redeployed. None -of these surfaces proves the private mirror, infra-owned state/apply, +## Cloudflare Pages Production And Legacy Routes + +Despite its stale name, `.github/workflows/cloudflare-pages-shadow.yml` builds +and, on eligible `main` or manual runs, deploys the production +`transscendsurvival-org` Pages project. Pull-request runs are build-only. +`https://tss.ephemera.tinyland.dev` still serves legacy compatibility content +from that project, while `https://tss.tinyland.dev` still serves the separate, +stale `tss-shadow` project. Both URLs are retired as QA routes. Do not +deliberately redeploy the separate `tss-shadow` project as part of blog QA. +None of these surfaces proves the private mirror, infra-owned state/apply, digest-pinned workload, or current MagicDNS route, so do not cite them as exact-head acceptance evidence. @@ -134,10 +135,11 @@ The workflow uses these repository credentials and configuration: | Variable | `CLOUDFLARE_PAGES_PROJECT_NAME` | Pages project name; defaults to `transscendsurvival-org` | PRs and missing-credential runs build and validate the static artifact, then -skip the Cloudflare deploy with an explicit notice. Manual dispatch accepts +skip the Cloudflare deploy with an explicit notice. Eligible `main` pushes and +manual runs can deploy production. Manual dispatch accepts `require_deploy=true` when an operator deliberately wants missing credentials -to fail instead of skip. This remains compatibility and production-serving -machinery, not the interactive PR QA route. +to fail instead of skip. This is production-serving machinery, not the +interactive PR QA route. Cloudflare Pages remains the production serving authority for `https://transscendsurvival.org` and `https://www.transscendsurvival.org`.