Pipeline v2: build once, promote the artifact (development branch)#427
Draft
Omicron7 wants to merge 24 commits into
Draft
Pipeline v2: build once, promote the artifact (development branch)#427Omicron7 wants to merge 24 commits into
Omicron7 wants to merge 24 commits into
Conversation
Introduce the build-once/promote-the-artifact pipeline v2 actions: - actions/resolve-image: resolve a tag or a running environment to a digest-pinned image reference in the shared registry (cru-shared-artifacts). CloudRun implemented; ecs/lambda stubbed. - actions/deploy: deploy a pre-built digest reference to a target environment, enforcing the digest invariant (tag refs are rejected). CloudRun orchestration ported from v1's deploy-cloudrun (db-migrate job first, refresh other jobs, rewrite only the app container to preserve sidecars, re-attach RUNTIME secrets). ecs/lambda stubbed. New v2 helpers live in src/v2/ (env mapping, shared-registry paths, image-ref parsing, Artifact Registry tag->digest resolution via the REST API + google-auth-library, app-container heuristics). Existing src/gcp.js helpers are imported read-only; no v1 source is modified. esbuild emits dist/resolve-image.js and dist/deploy.js; build-dist.yml now also triggers on the pipeline-v2 branch. Adds vitest coverage for ref parsing/validation, registry paths, container heuristics, tag->digest resolution, and deploy orchestration. Docs in docs/pipeline-v2.md. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ispatch action Add the four reusable workflow_call workflows that wire the Pass 1 resolve-image/deploy actions into the build-once / promote-the-artifact flow, plus a generic cross-repo dispatch action: - build-candidate.yml: router (setup + cloudrun/ecs/lambda); cloudrun builds candidate-<n> + sha-<gitsha> to the shared registry, with a no-change guard that reuses an already-built git sha. - deploy-candidate.yml: deploy a candidate to release-candidate (no authz). - promote.yml / rollback.yml: production-locked, authz-gated; promote reuses the candidate build number as release-<n>. - actions/dispatch: generic workflow_dispatch trigger (replaces the hardcoded v1 trigger-deploy map); src + esbuild entry + vitest. - docs/pipeline-v2.md: workflow interfaces, app-info + env translation, release numbering, authz contract, locks, grants matrix, Pass 3 sketch. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The shared BUILD-secrets store (D2) is not provisioned yet: the cru-shared-artifacts project lacks the Secret Manager API, gcp-secrets has no per-app filtering for a shared project, and build SAs have no scoped IAM there. The first hoax candidate build failed on this step. Gate it behind a new build-secrets input (default false) until the D2 store lands; apps without BUILD secrets are unaffected. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…der router Fill in the ECS stubs for pipeline v2 (build-once/promote-the-artifact) and restructure the deploy workflows into provider routers. - src/v2/aws.js: ECR tag<->digest resolution, tags-for-digest, manifest re-tag (BatchGetImage -> PutImage), the app-container predicate + compose helper, and the service-match regex. All clients use v1's retry config (now policy). - src/v2/resolve-ecs.js / deploy-ecs.js: ECS keyed on the PROJECT NAME (fixing v1's repo-name mismatch). Deploy composes from the FAMILY'S LATEST task-def revision (Terraform's template), swaps only the app container's image, refreshes RUNTIME secrets, registers a revision, updates every service, and re-points EventBridge scheduled tasks; sidecars pass through untouched. assertDigestRef enforces the digest invariant. No runtime-project (GCP-only). - src/v2/image-ref.js: extract provider-neutral ref helpers (parseImageRef etc.) so aws.js and gcp.js share them without cross-provider imports; gcp.js re-exports. - actions/tag-image: provider-agnostic release tagging (cloudrun -> Artifact Registry REST addTag; ecs/lambda -> ECR manifest re-tag), replacing promote's gcloud CLI step. - workflows: deploy-candidate/promote/rollback become D9 routers — a lookup job (app-info + authz) outputs provider/type/project-ids; gcp/aws jobs gated on provider. GCP keeps today's flow (tag-image swaps the gcloud step); AWS assumes GitHubDeployECS. build-candidate's build-ecs job is implemented (prod-bound, role-suffix GitHubRole). Concurrency locks + authz + datadog conventions kept. Lambda stays stubbed. - docs + tests for every new module. Terraform follow-ups (aws/ecs/app, separate PR): add <project>-<env>-GitHubRole for builds (retire TaskRole's GitHub trust); add ecr:PutImage to GitHubDeployECS. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Rebuild bundles for the v2 ECS pass: new dist/tag-image.js, and dist/deploy.js / dist/resolve-image.js now include the ECS implementations. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Datadog DORA Metrics is a separate per-committer SKU with unclear per-app billing exposure; Cru's telemetry cost model is one CI Visibility committer on cru-deploy. Deploy/promote/rollback now post structured events to the standard Events API (included with the platform): service/environment/action/revision tags, promote carries the candidate, rollback carries rollback:true. A deployments ledger via an app-info extension is the deferred phase 2 for DORA-style math. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…tes, multi-provider aws jobs Implements the Lambda runtime for pipeline v2, matching the ECS/Cloud Run contracts: - resolve-lambda: tag->digest via ECR; environment->digest by reading the deployed image off the app's <project>-<nick>* image functions (scratch placeholder skipped, mirroring ECS). - deploy-lambda: v1's ratified selection (Image functions on the app OR scratch ECR repo), digest-pinned UpdateFunctionCode, plus v2 hardening — wait for each function to finish updating (waitUntilFunctionUpdatedV2) so promote/rollback never read back a stale digest. New lambdaWaitForFunctionUpdated helper in src/aws.js. - build-candidate: real build-lambda job mirroring build-ecs, with two deliberate differences (--provenance=false so Lambda accepts the manifest; no PROJECT_NAME/ENVIRONMENT/BUILD_NUMBER build args — v2 candidates are env-neutral). - deploy-candidate/promote/rollback aws jobs: guard extended to ecs|lambda, type-keyed deploy role (GitHubDeployLambda vs GitHubDeployECS), (AWS) job names. tag-image already routes lambda down the shared ECR path. - docs + tests (resolve/deploy lambda, incl. scratch flip, wait, no-match). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Two ratified changes ahead of v2.0.0: D10 — candidate/release tags carry the BUILD date: candidate-<yyyy-mm-dd>-<n>, promoted as release-<yyyy-mm-dd>-<n> (full-suffix reuse keeps release == candidate identity). Humans get age-at-a-glance in tag listings, dispatch inputs, and Slack; the build number remains the unique key. Legacy pre-D10 tags stay resolvable everywhere: the reuse guard and promote accept both forms, and rollback normalizes dated or legacy input (bare build numbers resolve only for legacy releases). deploy-candidate is idempotent by default: after resolving the candidate it reads what release-candidate currently runs and no-ops on a digest match, making scheduled app workflows (the RFC's nightly-if-changed cadence) safe to dispatch unconditionally — a quiet night is a true no-op. New force input covers deliberate same-digest redeploys (e.g. picking up an applied Terraform task-definition template). Cadence itself stays the app workflow's choice: per-merge (pilots) or nightly (RFC default) with no reusable-workflow changes. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The first human rollback after D10 typed the natural thing — the bare build number — and failed at resolve: bare numbers normalized to release-<n>, which only exists for pre-D10 releases. Fall back in both providers' tag resolution: when release-<n> misses, search for exactly one matching release-<yyyy-mm-dd>-<n> and resolve that (0 matches rethrows the original error; ambiguity never guesses). Failure was pre-mutation by design — production untouched. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
BUILD-time secrets live in the app repo as Actions secrets named BUILD_<NAME>. The repo is the isolation boundary -- matching the build's repo-scoped OIDC identity -- and one mechanism serves all three runtimes, replacing the planned shared Secret Manager store (API enablement, per-app label filtering, IAM name-prefix conditions, and a GCP/AWS asymmetry, all now unnecessary). Each build job exports ONLY BUILD_*-prefixed keys from the inherited secrets context (prefix stripped: BUILD_NPM_TOKEN -> NPM_TOKEN), no-oping when none exist; the build-secrets gate input is removed (no current caller passes it). The prefix filter is load-bearing: secrets:inherit exposes org secrets to the workflow, and nothing outside BUILD_* may reach build.sh. Writes need repo admin: DevOps via gh secret set until cru secret gains a brokered write path through the planned GitHub App. Out of scope for cru app secrets (runtime-only) -- build secrets are part of the build, which is GitHub. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…s, event enrichment, GCP retries Implements the memo's deferred telemetry hardening plus two flightdeck-adopted enrichments, all preserving the hard policy that telemetry never fails a deploy. - DD_VERSION injection: new optional `version` input on the deploy action, threaded through the router to all three impls. Cloud Run upserts DD_VERSION on the app container's env (services + jobs); ECS upserts into the composed task def's app-container `environment` array (composeTaskDefinition stays pure); Lambda deliberately ignores it (function env is Terraform-owned — never call UpdateFunctionConfiguration). Existing DD_VERSION entries are replaced, never duplicated; sidecars untouched. Workflows pass it: deploy-candidate -> the candidate tag, promote -> the derived release tag, rollback -> the actual release-* tag on the resolved digest (not the bare-number input). - Visible swallowed-telemetry failures: every Datadog step keeps continue-on-error but now appends a `|| echo "::warning ..."` so a failed tag/event post annotates the run instead of vanishing. - Event enrichment (from flightdeck's event shape): the Events API posts gain `actor:<github.actor>` and, when the resolved digest carries a sha-<gitsha> tag, `sha:<gitsha>` (prefix stripped, omitted cleanly when absent) — the deployments-ledger precursor. The datadog-ci pipeline tag steps also gain `version:<version>` where known. - GCP transient-5xx retries: all three Artifact Registry REST client.request sites in src/v2/gcp.js get shared gaxios retry options (5 attempts, GET/POST, 429 + 5xx) so a transient AR 503 no longer fails a resolve, matching the AWS SDK clients' maxAttempts:5. The retried tag-create POST is safe (alreadyExists is treated as success by the caller). Tests extended (composeTaskDefinition upsert new/replace/no-op, cloud run env injection incl. replace, deploy.js router version passthrough, lambda ignores version, ECS threading, gcp retryConfig passthrough); docs updated. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…dger Append each pipeline-v2 deployment event to the CruDeploymentLedger DynamoDB table (app-info's deployments ledger -- the fleet dashboard / DORA data spine) directly via `aws dynamodb put-item`, in both provider jobs of deploy-candidate, promote, and rollback, immediately after the Datadog event step. - Same telemetry policy as Datadog: continue-on-error + a trailing ::warning so a failed write is visible but NEVER fails the deploy. - GCP (Cloud Run) jobs assume a deploy role (ECS, arbitrarily -- trust is cru-deploy-repo-scoped) via configure-aws-credentials placed AFTER all GCP steps; AWS jobs already carry the grant on their deploy role. - The dynamodb:PutItem grant is added inline to the existing deploy roles in cru-terraform (no dedicated ledger role); until that applies the steps emit benign ::warning annotations and deploys are unaffected. - docs/pipeline-v2.md: new "Deployments ledger" subsection (schema, writers, never-fails policy, GET /deployments read path, v1 deferred to wave-2). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
All three pilots run it (staggered noon-UTC crons); per-merge remains supported per D11. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…m-owned Caught by review question before any deploy ran with it: the Cloud Run service's ignore_changes covers the app container's IMAGE only; its env is Terraform-managed (secret refs, environment map, cloud_sql vars). A pipeline-injected DD_VERSION would show as drift on every plan and be removed by every apply -- the docker_config disease. Unlike ECS there is no separate Terraform-owned template for the deploy to compose from, and ignore_changes cannot exempt a single env entry, so Cloud Run joins Lambda: version telemetry rides the deployment events. deployCloudRun now accepts and deliberately ignores version; tests assert the non-injection and that a pre-existing (Terraform-owned) DD_VERSION passes through untouched. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Brian's design, replacing deploy-time injection entirely: every candidate build passes --build-arg VERSION=<yyyy-mm-dd>-<n> (the tag family's bare suffix -- identical for candidate and future release under D10), and apps opt in with ARG VERSION + ENV DD_VERSION at the END of the Dockerfile (zero cache cost, graceful degradation without it). Strictly better than injection: one true version per build in every environment (injection made promote look like a version change on identical bytes); zero Terraform drift on ALL runtimes -- image ENV is invisible to TF env management, and function-config/service env never sets DD_VERSION, so the baked value shines through everywhere INCLUDING Lambda (which injection could never reach); the reused-candidate path keeps its original version, which is the artifact's identity. Removes the deploy action's version input, the ECS compose-time upsert, the router threading, and their tests; events and ledger rows still carry the full revision tag per deploy (what-was-deployed-where vs what-is-running). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…promote/rollback results Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…erdicts on promote Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…pdates Add a pre-deploy migration phase to deployEcs, mirroring the Cloud Run db-migrate job. Before any service is touched, DescribeTaskDefinition on the convention family <project>-<nick>-db-migrate decides whether the app has opted in (family present) or not (ClientException -> skip). When present, the deploy composes a revision from the family's latest (release digest + refreshed RUNTIME secrets, identical to the service path), RunTasks it once (startedBy cru-pipeline-v2), waits via waitUntilTasksStopped (900s), and requires the db-migrate container to exit 0. The run configuration (network + launch type/capacity-provider strategy) is borrowed from the first matching service, or from the EventBridge scheduled-task target for a jobs-only app; if neither exists the deploy throws. Any nonzero exit, failure stopCode/stoppedReason, or wait timeout throws and fails the deploy with the running services untouched. The matching service list is now fetched once in deployEcs and shared by the migration phase and updateServices. This replaces the retired ECS sidecar model, which was verified broken: the app container's dependsOn on the db-migrate container used condition=START with essential=false, so the app raced the migration (serving against the un-migrated schema) and a failed migration never blocked the app or the deploy. Migrations also re-ran on every task launch/scale-up; the pre-deploy task applies them once per deploy. The phase runs for every ECS deploy (rc, promote, rollback) since deployEcs is shared, matching Cloud Run (a rollback's older image no-ops against already-applied migrations). New src/aws.js helpers beside the existing ECS ones: ecsDescribeServices, ecsRunTask, ecsDescribeTasks, ecsWaitUntilTasksStopped (all using RETRY_CONFIG). BREAKING CHANGE: ECS deploys now run database migrations to completion as a discrete pre-deploy step keyed on the <project>-<nick>-db-migrate task-definition family, replacing the sidecar model. Requires the v2 aws/ecs/app module's top-level database_migrations object; module and workflows adopt as one unit. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…un image
The no-op guard's 'Check image currently on release-candidate' step resolves
the running image to a shared-registry digest. On an app's FIRST v2 deploy
after v1, the service still runs a tag-pinned image from its old per-project
registry (e.g. bills-stage/container/bills:staging-10108); resolving that tag
against the shared registry threw 'Tag not found', surfacing a red failure
annotation on an otherwise-green run (continue-on-error let the deploy
proceed correctly).
A tag ref whose repo is not the shared registry now returns an empty digest
('no comparable deployment') with an info log instead of throwing. Promote's
environment resolve also improves: a pre-v2 release-candidate now fails at
'Determine release number' with its clearer deploy-a-candidate-first error.
ECS is unaffected (v1/v2 share the app ECR repo, old tags resolve); Lambda
always reports digest refs.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
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.
Summary
The v2 pipeline — build-once/promote-the-artifact, per the Agentic Development Lifecycle RFC's Flight phase and the design memo's nine decisions. Long-lived development branch; not for merge until the hoax pilot promotes end-to-end. At release this merges as v2.0.0 with a rolling
v2tag; the v1 surface is never modified (verified: the only v1 file touched isbuild-dist.yml, whose push trigger gains this branch sodist/stays built here).What's here (Passes 1–2)
Actions (provider-agnostic interface, per-runtime modules; CloudRun implemented, ECS/Lambda stubbed):
actions/resolve-image— resolve what's running in an environment, or acandidate-<n>/release-<r>tag, to a digestactions/deploy— deploy an explicit digest (hard-fails on tag refs; the invariant is code, not convention), porting v1's CloudRun orchestration (db-migrate first, sidecars preserved, RUNTIME secrets re-attached)actions/dispatch— generic cross-repo workflow_dispatch (replaces trigger-deploy's hardcoded map)Reusable workflows:
build-candidate.yml— env-neutral image from main → shared registry (cru-shared-artifacts/<app>/<app>:candidate-<n>+sha-<gitsha>), router by type, no-change guard reuses existing images for unchanged commitsdeploy-candidate.yml— candidate tag → release-candidate environment (rc lock)promote.yml— authz (actor must have push on the app repo) → resolve the digest running on rc → deploy to production → tagrelease-<n>(n = the candidate's build number) → Datadog deployment mark (production lock)rollback.yml— redeploy anyrelease-<r>digest (shared production lock)Conventions: digests-only deploys; env long names
release-candidate/previewmap to existingstage/labnicknames; releases are permanent (never pruned — the registry module's KEEP policy enforces it); telemetry pinned@datadog/datadog-ci@5withcontinue-on-erroreverywhere. Full detail indocs/pipeline-v2.md, including the grants matrix and cru-deploy wrapper sketches.Validation
76 tests green (vitest), lint clean, 12 action bundles build. Workflow YAML parsed/validated; action input references cross-checked between passes.
Remaining before merge (Pass 3+)
cru-deploy wrappers (+
authz-tokensecret, WIF vars), hoax Terraform (registry module instance via CruGlobal/cru-terraform-modules#688 + prod env + grants), hoax app workflow pinned@pipeline-v2, end-to-end pilot promote/rollback, then ECS/Lambda passes.Supersedes #423.
🤖 Generated with Claude Code