Cut the deploy over to the artifact store: image fetch, deployed markers, prewarm retired#653
Merged
Merged
Conversation
fetch_artifacts downloads every manifest-listed artifact from the GCS store into POLICYENGINE_DATA_FOLDER, with a freshness gate comparing the manifest's version receipt against the image env (and the installed bundle receipt's data version when present), a loud one-error existence pass before any download, unconditional overwrites, and an inline GCP credentials shim whose temp file is deleted before the layer commits. Inert in this commit: nothing calls it yet. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The deploy resolves the precompute job's manifest digest on the runner (modal.is_local time) and passes bucket + manifest content as the fetch layer's args, so the layer cache follows the artifact set exactly. A digest-less import stays safe (precompute and smoke apps import app.py where no digest can exist) via a None-manifest sentinel that fails the build, not the import. The prebuild layer, its years constant, and the prewarm app are deleted; retired coupling tests are replaced per the migration mapping (fetch layer ordering, sentinel, years-cover-default). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
A new "Record deployment marker" step runs on both legs after the health check, writing deployed/<environment>.json (last-writer-wins) with the manifest digest, versions, and run identity — the first caller of the Phase-1 marker helpers, and the liveness signal the artifact GC will rely on. A failed marker write fails the deploy job by design: a deploy that cannot record itself must not pass silently. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The executor README loses the retired prebuild/prewarm section and gains the consumer half of the pipeline: the fetch layer and what busts its cache, the freshness gate, local-deploy inputs, the deployed markers, and the inherited bundle-install staleness residual. Remaining prose mentions of the prebuild (root README, smoke app and artifact_keys docstrings, one test name) are updated to match. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Review feedback on the credentials shim: instead of writing the secret blob to a temp file and pointing GOOGLE_APPLICATION_CREDENTIALS at it, parse it and hand google.oauth2.service_account credentials straight to storage.Client — the standard google-auth pattern for credentials that arrive as data. No file ever exists, so the risk of committing a key into the image layer is gone structurally, and the cleanup block goes with it. A source-level regression test keeps tempfile out of the module; the ADC fallback for local drill runs is unchanged. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…st inertness The custom-data folder redirect is permanent under the artifact pipeline, not a bridge pending issue #596 — its comment (and the matching test's) now states the constraint on its own terms. The marker step's env wiring is asserted line-by-line, since the payload builder maps missing env vars to empty strings and a silent rename would otherwise empty the marker's fields with green tests. The container-entrypoint test now sets a manifest digest and asserts the fetch layer args stay inert, locking the is_local-first ordering. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…dary DeployedMarker joins the strict precompute schemas: the marker is a persisted wire shape GC will read back, so it must not live as a bare dict. record_deployment builds the model and dumps it at the store call site (the publish_manifest_impl discipline), and app.py now validates what it reads from the store as ArtifactManifest before handing the canonical payload to the fetch layer — a corrupt or foreign manifest fails the deploy on the runner, not mid-image-build. Test manifests are derived from the model so the fakes cannot drift off the wire shape; the dict survives only at the import-restricted layer boundary, where it belongs. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
anth-volk
marked this pull request as ready for review
July 24, 2026 21:53
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #652
Phase 3 of the baseline artifact pipeline (phase 1: #644, phase 2: #649). The store that phases 1–2 fill and verify finally gets its consumer: the image fetches precomputed artifacts instead of building datasets, and the deploy records what it shipped.
What changed
Commit 1 — self-contained
fetch_artifacts(inert). New image-layer function in_image_setup.py: downloads every manifest-listed artifact intoPOLICYENGINE_DATA_FOLDER, with a freshness gate (manifest receipt versions vs the image's env versions, plus the installed bundle receipt's data version when present), one loud error listing all missing store objects before any download, unconditional overwrites, and GCS credentials built in memory from the secret blob (service_account.Credentials.from_service_account_infopassed straight tostorage.Client— nothing is ever written to disk, since arun_functionlayer commits its filesystem into the image; reworked in the follow-up commit after review). Zero callers in this commit.Commit 2 — the atomic swap.
app.pyresolves the deploy manifest on the runner atmodal.is_local()time from the newPOLICYENGINE_MANIFEST_DIGESTenv var (threaded fromneeds.precompute.outputs.manifest_digest, alongside the bucket var andGCP_CREDENTIALS_JSON) and passes bucket + manifest content as the fetch layer's args — the layer cache follows the artifact set exactly, retiring theforce_buildritual. A digest-less import stays safe (the precompute and smoke apps importapp.pywhere no digest can exist) via a None-manifest sentinel that fails at build time, never import time. The prebuild layer,PREBUILD_DATASET_YEARS, andprewarm_app.pyare deleted; retired tests are replaced per a migration mapping (fetch layer ordering + sentinel + years-cover-default; the writer==reader filename coupling was already locked by the phase-1 precompute tests).Commit 3 — deployed markers. A "Record deployment marker" step runs on both legs after the health check, writing
deployed/<environment>.json(manifest digest, versions, run identity) via the first caller of the phase-1 marker helpers — the liveness signal for phase-4 GC. A failed marker write fails the deploy job by design.Commit 4 — docs. Executor README describes the pipeline as it now is (fetch layer, freshness gate, local-deploy inputs, markers, the inherited bundle-install staleness residual); all remaining prebuild/prewarm prose is gone.
Why the merge is atomic
Once
app.pyexpects the manifest digest, a deploy without the precompute output fails. The producer already flows from phase 2, so this PR adds only the consumer — there is no intermediate merge state where a deploy half-consumes the store.Verification
baseline_artifact=hiton a beta economy request with p50 down,deployed/beta.json+deployed/prod.jsonin the bucket, then a fault drill (delete one baseline object → local fetch fails loudly → plain precompute re-run heals with an unchanged manifest digest).Rollback
One revert restores the prebuild layer, prewarm app, workflow, and README verbatim; phases 1–2 keep running harmlessly underneath. Markers go stale after a revert (last-writer-wins, no more writers) — the phase-4 GC design must treat marker staleness conservatively.
🤖 Generated with Claude Code