Cloud Run · Publish Pipeline — Plumbing Complete, GitHub PAT Pending

DARE.CO.UK · INFRASTRUCTURE · 12 MAY 2026

Runbook + operational chronology of today’s containerisation of the dev-reports publish pipeline. Five phases shipped; first end-to-end execution gated on a 5-minute GitHub PAT mint. Generated immediately after the canary execution validated the rest of the chain.

TL;DR

What was plumbed in

Five phases, each independently shippable. Chronology of the session:

Phase 1 — Toolkit Linux portability

Two surgical ports in xlab-co/toolkit (commit f08566b):

python REPORTS_DIR = os.environ.get("REPORTS_DIR", ~/Downloads) STAGE_DIR = os.environ.get("STAGE_DIR", REPORTS_DIR/dev_reports_pages) THUMB_CACHE_DIR = os.environ.get("THUMB_CACHE_DIR", REPORTS_DIR/devreports_thumbs)

Defaults reproduce the historical layout exactly. Smoke-tested locally — 49 reports staged, 49 thumb cache hits, no breakage.

Phase 2 — Content repo

Created xlab-co/devreports-content (private). Migrated 56 markdown reports + .gitignore (excludes generated HTML / JPG / staging output) + README documenting filename conventions. This becomes the source-of-truth corpus the Cloud Run Job clones at runtime, decoupling content from the Mac filesystem.

Initial commit captures today’s catalog state. The local generator scripts (dare_audit.py, dare_session_report.py, dare_404_audit.py, narrator, etc.) still write to ~/Downloads/ today; a later session adds git add + commit + push steps to those scripts so generated reports flow through this repo automatically.

Phase 3 — GCP project + Secret Manager + Artifact Registry

Mirrors today’s audrey-experiments setup pattern:

Resource Identifier
GCP project dare-devreports (number 574960446082) — under the dare.co.uk org
Billing account 003C9F-AC7F67-74ACF1 (My Billing Account)
APIs enabled Cloud Run, Cloud Build, Artifact Registry, Secret Manager, Cloud Scheduler, IAM Credentials
Artifact Registry us-central1-docker.pkg.dev/dare-devreports/devreports (Docker format)
Runtime service account devreports-publish-sa@dare-devreports.iam.gserviceaccount.com
Secret cf-pages-deploy (CDN, security layer, and DNS provider sitting in front of dare.co.uk.">CF Pages deploy token, version 1; same source as the launchd cron’s op-injection ref)
IAM bindings devreports-publish-sa + Cloud Run service agent both have secretmanager.secretAccessor on cf-pages-deploy

Two gotchas surfaced and were resolved during setup:

  1. Fresh-project IAM propagation delay — Artifact Registry create initially returned IAM_PERMISSION_DENIED even though dan@dare.co.uk has roles/owner. A 30s wait + retry succeeded. Standard GCP eventually-consistent behaviour; worth knowing.
  2. Empty secret payload from a piped op read that silently failed auth — first op read | gcloud secrets create cf-pages-deploy --data-file=- created the secret but populated zero versions, because op had auth-timed-out and returned empty. Downstream Secret was not found errors at job creation time. Fix: explicit length check on the op-read output before piping to gcloud secrets versions add, plus a separate version-add step rather than create-with-version. Worth memorialising — silent empty pipes are exactly the kind of issue that’s easy to miss.

Phase 4 — Dockerfile + entrypoint + cloudbuild config

Three files in xlab-co/toolkit/cloud-run/ (commit 08e6141):

STASH44

The Dockerfile copies only the three toolkit modules the publish needs (seo_render_html.py, thumbnailer.py, dare_dev_reports_publish.py) rather than the full toolkit checkout — image stays small + the runtime surface is auditable.

The cloudbuild config has a small staging step that lays out the build context so the Dockerfile’s COPY toolkit/seo_render_html.py /app/ resolves cleanly. The toolkit repo’s flat root layout doesn’t match the Dockerfile’s toolkit/... path expectations; the staging step bridges that without restructuring the repo.

Phase 5 — First image build + Cloud Run Job + canary execution

STASH50

Build time: 3 min 28s. Image landed at us-central1-docker.pkg.dev/dare-devreports/devreports/publish:08e6141 + :latest (490MB). Cloud Run Job created:

STASH53

Canary executed at 18:35Z. Logs from Cloud Logging:

STASH54

This failure mode is the canary signal — every other layer of the chain works. The container provisioned, the image was pulled from Artifact Registry, the runtime SA resolved, the CLOUDFLARE_API_TOKEN secret bound from Secret Manager (otherwise we’d have failed before reaching entrypoint), the entrypoint ran, the config echoed correctly, and execution stopped at exactly the explicit guard for the one piece that’s still missing.

Architecture — end state

STASH56

Both paths converge on the same Cloudflare Pages project — once Cloud Run is validated, the cutover is just “disable the launchd plist” with no DNS or config changes.

Action plan — next session (≤ 1 hour for first successful run)

1. Mint a fine-grained GitHub PAT (5 min, interactive)

At https://github.com/settings/personal-access-tokens/new:

Copy the PAT once issued. Per feedback_op_read_never_to_chat_stdout.md, never paste it into chat — straight from clipboard into the gcloud command below.

2. Store PAT in Secret Manager + grant access (3 min)

STASH60

3. Bind PAT to the Cloud Run Job (1 min)

STASH61

4. First end-to-end execution (2 min + ~3 min runtime)

STASH62

Expected output: git clone succeeds, dare_dev_reports_publish.py stages all reports, wrangler pages deploy ships to devreports.dare.co.uk. Verify via git log on the Cloudflare Pages project + a fresh load of the catalog page.

5. Compare output with the launchd cron (validation gate)

After the Cloud Run execution completes, compare the deployed catalog vs what launchd’s most-recent run produced. Equivalent content + identical thumbnail set = the migration is functionally complete.

Open items (Phase 6 and beyond)

Why this matters beyond today

Three categories of payoff:

Immediate operational wins:

Substrate compounding (per feedback_upstream_fix_downstream_compounding.md):

Learning lab payoff (per user_portfolio_as_learning_lab.md):

Portability

Same architecture applies anywhere the portfolio runs a content publish pipeline:

Property What lives in containerised pipeline Status
dare-co-uk site Static-site deploy (already Cloudflare Pages; wrangler push) Already cloud-native; no migration
devreports.dare.co.uk Today’s migration — content → render → CDN, security layer, and DNS provider sitting in front of dare.co.uk.">CF Pages Phase 1-5 done; Phase 6 (scheduler) next
dashboard.dare.co.uk Cloudflare analytics fetch → narrator generation → render → deploy Same shape; pending similar containerisation pass
beta.audreyinc.com Worker-with-Assets; deploys directly via wrangler Already cloud-native; no migration
studio.audreyinc.com (planned) Same Worker pattern + Shopify Admin API proxy + Vertex AI uplift Will be cloud-native from day one
dogwood.house (when activated) Same shape — content + render + deploy via Cloud Run Apply the template directly
client work Per-client GCP project per reference_gcp_cli_and_agency_iam.md Apply the template per engagement

Linked memories


Cloud Run Job devreports-publish is provisioned, image built, secrets wired, canary validated. The 5-minute GitHub PAT mint is the only thing between here and a fully cloud-native publish pipeline.

Source: dare_cloud_run_publish_2026-05-12.md · Rendered 2026-05-12 14:49