Skip to content

Commit db5f1cc

Browse files
ahcarpenterclaude
andcommitted
merge: integrate phase 05.1 github-actions-ci-cd into main
Phase 05.1 delivers the GitHub Actions CD pipeline rewrite: - IMAGE_TAG Pulumi config key with ENV fallback - Two-job cd-base.yml (build + deploy) with WIF auth, Docker GHA layer cache, SHA-only image tags, imageTag config-map passthrough, and dev health check - cd-dev.yml triggers on push to main - cd-staging.yml is workflow_dispatch only (no PR trigger) - cd-prod.yml uses environment: prod approval gate - Static test scaffold (23 tests) validates all CD contracts via YAML and AST parsing Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2 parents a9f329d + d8455be commit db5f1cc

15 files changed

Lines changed: 1286 additions & 48 deletions

File tree

.github/workflows/cd-base.yml

Lines changed: 55 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
name: CD Base
22

3-
on:
3+
"on":
44
workflow_call:
55
inputs:
66
stack:
@@ -12,42 +12,68 @@ on:
1212
environment:
1313
required: false
1414
type: string
15+
gcp_project:
16+
required: true
17+
type: string
1518
secrets:
1619
WIF_PROVIDER:
1720
required: true
1821
WIF_SERVICE_ACCOUNT:
1922
required: true
2023
PULUMI_CONFIG_PASSPHRASE:
2124
required: true
25+
outputs:
26+
sha:
27+
description: "7-char git SHA used as image tag"
28+
value: ${{ jobs.build.outputs.sha }}
2229

2330
jobs:
24-
deploy:
31+
build:
2532
runs-on: ubuntu-latest
26-
environment: ${{ inputs.environment }}
2733
permissions:
2834
contents: read
2935
id-token: write
30-
36+
outputs:
37+
sha: ${{ steps.sha.outputs.sha }}
3138
steps:
3239
- uses: actions/checkout@v4
3340

41+
- id: sha
42+
run: echo "sha=$(git rev-parse --short=7 HEAD)" >> "$GITHUB_OUTPUT"
43+
3444
- uses: google-github-actions/auth@v3
3545
with:
3646
workload_identity_provider: ${{ secrets.WIF_PROVIDER }}
3747
service_account: ${{ secrets.WIF_SERVICE_ACCOUNT }}
3848

49+
- uses: docker/setup-buildx-action@v3
50+
3951
- name: Configure Docker for Artifact Registry
4052
run: gcloud auth configure-docker us-central1-docker.pkg.dev --quiet
4153

42-
- name: Build and push image
54+
- uses: docker/build-push-action@v6
4355
if: inputs.command == 'up'
44-
run: |
45-
PROJECT_ID=$(gcloud config get-value project)
46-
IMAGE="us-central1-docker.pkg.dev/${PROJECT_ID}/vici-images/vici"
47-
SHA=$(git rev-parse --short HEAD)
48-
docker build -t "${IMAGE}:${SHA}" -t "${IMAGE}:${{ inputs.stack }}" .
49-
docker push "${IMAGE}:${SHA}"
50-
docker push "${IMAGE}:${{ inputs.stack }}"
56+
with:
57+
context: .
58+
push: true
59+
tags: us-central1-docker.pkg.dev/${{ inputs.gcp_project }}/vici-images/vici:${{ steps.sha.outputs.sha }}
60+
cache-from: type=gha
61+
cache-to: type=gha,mode=max
62+
63+
deploy:
64+
runs-on: ubuntu-latest
65+
needs: [build]
66+
environment: ${{ inputs.environment }}
67+
permissions:
68+
contents: read
69+
id-token: write
70+
steps:
71+
- uses: actions/checkout@v4
72+
73+
- uses: google-github-actions/auth@v3
74+
with:
75+
workload_identity_provider: ${{ secrets.WIF_PROVIDER }}
76+
service_account: ${{ secrets.WIF_SERVICE_ACCOUNT }}
5177

5278
- uses: astral-sh/setup-uv@v5
5379
with:
@@ -61,6 +87,22 @@ jobs:
6187
command: ${{ inputs.command }}
6288
stack-name: ${{ inputs.stack }}
6389
work-dir: infra
90+
cloud-url: gs://vici-app-pulumi-state-${{ inputs.stack }}
91+
config-map: '{"vici-infra:imageTag": {"value": "${{ needs.build.outputs.sha }}", "secret": false}}'
6492
env:
6593
PULUMI_CONFIG_PASSPHRASE: ${{ secrets.PULUMI_CONFIG_PASSPHRASE }}
66-
PULUMI_BACKEND_URL: gs://vici-app-pulumi-state-${{ inputs.stack }}
94+
95+
- name: Health check
96+
if: inputs.stack == 'dev' && inputs.command == 'up'
97+
run: |
98+
for i in $(seq 1 12); do
99+
STATUS=$(curl -s -o /dev/null -w "%{http_code}" https://dev.usevici.com/health)
100+
if [ "$STATUS" = "200" ]; then
101+
echo "Health check passed (attempt $i)"
102+
exit 0
103+
fi
104+
echo "Attempt $i: got $STATUS, retrying in 5s..."
105+
sleep 5
106+
done
107+
echo "Health check failed after 60s"
108+
exit 1

.github/workflows/cd-dev.yml

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
name: CD Dev
22

3-
on:
3+
"on":
44
push:
55
branches: [main]
66

@@ -14,7 +14,8 @@ jobs:
1414
with:
1515
stack: dev
1616
command: up
17+
gcp_project: vici-app-dev
1718
secrets:
18-
WIF_PROVIDER: ${{ secrets.GCP_WIF_PROVIDER_DEV }}
19-
WIF_SERVICE_ACCOUNT: ${{ secrets.GCP_CI_SA_DEV }}
19+
WIF_PROVIDER: ${{ secrets.GCP_WIF_PROVIDER }}
20+
WIF_SERVICE_ACCOUNT: ${{ secrets.GCP_CI_SA_EMAIL }}
2021
PULUMI_CONFIG_PASSPHRASE: ${{ secrets.PULUMI_CONFIG_PASSPHRASE }}

.github/workflows/cd-prod.yml

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
name: CD Prod
22

3-
on:
3+
"on":
44
workflow_dispatch:
55

66
permissions:
@@ -14,7 +14,8 @@ jobs:
1414
stack: prod
1515
command: up
1616
environment: prod
17+
gcp_project: vici-app-prod
1718
secrets:
18-
WIF_PROVIDER: ${{ secrets.GCP_WIF_PROVIDER_PROD }}
19-
WIF_SERVICE_ACCOUNT: ${{ secrets.GCP_CI_SA_PROD }}
19+
WIF_PROVIDER: ${{ secrets.GCP_WIF_PROVIDER }}
20+
WIF_SERVICE_ACCOUNT: ${{ secrets.GCP_CI_SA_EMAIL }}
2021
PULUMI_CONFIG_PASSPHRASE: ${{ secrets.PULUMI_CONFIG_PASSPHRASE }}

.github/workflows/cd-staging.yml

Lines changed: 5 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,33 +1,20 @@
11
name: CD Staging
22

3-
on:
4-
pull_request:
5-
branches: [main]
3+
"on":
64
workflow_dispatch:
75

86
permissions:
97
contents: read
108
id-token: write
119

1210
jobs:
13-
preview:
14-
if: github.event_name == 'pull_request'
15-
uses: ./.github/workflows/cd-base.yml
16-
with:
17-
stack: staging
18-
command: preview
19-
secrets:
20-
WIF_PROVIDER: ${{ secrets.GCP_WIF_PROVIDER_STAGING }}
21-
WIF_SERVICE_ACCOUNT: ${{ secrets.GCP_CI_SA_STAGING }}
22-
PULUMI_CONFIG_PASSPHRASE: ${{ secrets.PULUMI_CONFIG_PASSPHRASE }}
23-
24-
deploy:
25-
if: github.event_name == 'workflow_dispatch'
11+
deploy-staging:
2612
uses: ./.github/workflows/cd-base.yml
2713
with:
2814
stack: staging
2915
command: up
16+
gcp_project: vici-app-staging
3017
secrets:
31-
WIF_PROVIDER: ${{ secrets.GCP_WIF_PROVIDER_STAGING }}
32-
WIF_SERVICE_ACCOUNT: ${{ secrets.GCP_CI_SA_STAGING }}
18+
WIF_PROVIDER: ${{ secrets.GCP_WIF_PROVIDER }}
19+
WIF_SERVICE_ACCOUNT: ${{ secrets.GCP_CI_SA_EMAIL }}
3320
PULUMI_CONFIG_PASSPHRASE: ${{ secrets.PULUMI_CONFIG_PASSPHRASE }}

.planning/workstreams/gks-refactor/ROADMAP.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -115,8 +115,8 @@ Plans:
115115
**Plans**: 2 plans
116116

117117
Plans:
118-
- [ ] 05.1-01-PLAN.md — Test scaffold and IMAGE_TAG config key for Pulumi
119-
- [ ] 05.1-02-PLAN.md — Rewrite all four CD workflow files to match locked decisions
118+
- [x] 05.1-01-PLAN.md — Test scaffold and IMAGE_TAG config key for Pulumi
119+
- [x] 05.1-02-PLAN.md — Rewrite all four CD workflow files to match locked decisions
120120

121121
### Phase 6: Infra Best-Practice Audit and Edge-Case Hardening
122122
**Goal**: All stateful infrastructure is protected from accidental deletion, namespaces enforce least-privilege network access, Temporal credentials follow the ESO pattern, and operators have a runbook for edge-case scenarios

.planning/workstreams/gks-refactor/STATE.md

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,8 @@ milestone: v1.0
44
milestone_name: milestone
55
status: executing
66
stopped_at: Phase 5.1 context gathered
7-
last_updated: "2026-04-10T03:27:00.619Z"
8-
last_activity: 2026-04-10 -- Phase 5.1 planning complete
7+
last_updated: "2026-04-11T19:21:43.667Z"
8+
last_activity: 2026-04-11 -- Phase 05.1 execution started
99
progress:
1010
total_phases: 7
1111
completed_phases: 4
@@ -21,15 +21,15 @@ progress:
2121
See: .planning/workstreams/gks-refactor/PROJECT.md (updated 2026-04-04)
2222

2323
**Core value:** All three environments run on 1:1 mirrored GKE infrastructure managed by a single Pulumi program
24-
**Current focus:** Phase 05 — application-deployment-and-ci-cd
24+
**Current focus:** Phase 05.1github-actions-ci-cd
2525

2626
## Current Position
2727

28-
Phase: 05
29-
Plan: Not started
28+
Phase: 05.1 (github-actions-ci-cd) — EXECUTING
29+
Plan: 1 of 2
3030
Plans: 3/3 complete, human UAT approved
31-
Status: Ready to execute
32-
Last activity: 2026-04-10 -- Phase 5.1 planning complete
31+
Status: Executing Phase 05.1
32+
Last activity: 2026-04-11 -- Phase 05.1 execution started
3333

3434
Progress: [████████░░] 83% (5 of 6 phases complete)
3535

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
---
2+
phase: 05.1-github-actions-ci-cd
3+
plan: "01"
4+
subsystem: infra
5+
tags: [cd, image-tag, pulumi-config, test-scaffold, static-analysis]
6+
dependency_graph:
7+
requires: []
8+
provides: [IMAGE_TAG config key, CD static test scaffold]
9+
affects: [infra/config.py, infra/components/app.py, tests/infra/]
10+
tech_stack:
11+
added: []
12+
patterns: [AST-based static test, YAML-based workflow test, Pulumi config fallback]
13+
key_files:
14+
created:
15+
- tests/infra/test_cd_workflows_static.py
16+
- tests/infra/test_cd_static.py
17+
modified:
18+
- infra/config.py
19+
- infra/components/app.py
20+
decisions:
21+
- IMAGE_TAG uses cfg.get() (optional) not cfg.require() so local dev works without the flag
22+
- ENV fallback preserves existing behavior for manual pulumi up without --config imageTag
23+
- Test scaffold created before workflow files (Wave 0 pattern) so Plan 02 has concrete assertions to satisfy
24+
metrics:
25+
duration: "~5 minutes"
26+
completed_date: "2026-04-11"
27+
tasks_completed: 2
28+
files_modified: 4
29+
---
30+
31+
# Phase 05.1 Plan 01: IMAGE_TAG Config and CD Test Scaffold Summary
32+
33+
**One-liner:** IMAGE_TAG config key with ENV fallback added to Pulumi; static test scaffold validates all CD-01 through CD-05 contracts via YAML and AST parsing.
34+
35+
## Tasks Completed
36+
37+
| Task | Name | Commit | Files |
38+
|------|------|--------|-------|
39+
| 1 | Create static test scaffold for CD workflows and imageTag config | 50914d5 | tests/infra/test_cd_workflows_static.py, tests/infra/test_cd_static.py |
40+
| 2 | Add IMAGE_TAG config key to config.py and update app.py | 450e6cf | infra/config.py, infra/components/app.py |
41+
42+
## What Was Built
43+
44+
### IMAGE_TAG Pulumi Config Key (Task 2)
45+
46+
Added `IMAGE_TAG: str = cfg.get("imageTag") or ENV` to `infra/config.py`. When CI runs `pulumi up --config vici-infra:imageTag=a1b2c3d`, the deployed container image tag is the 7-char git SHA (e.g., `us-central1-docker.pkg.dev/PROJECT/vici/app:a1b2c3d`). When running locally without the flag, `IMAGE_TAG` falls back to `ENV` (e.g., "dev"), preserving existing behavior.
47+
48+
Updated `infra/components/app.py` to import `IMAGE_TAG` alongside `ENV` and use it in the `pulumi.Output.concat(registry_url, "/vici:", IMAGE_TAG)` call.
49+
50+
### CD Static Test Scaffold (Task 1)
51+
52+
Two test files establish the RED → GREEN contract for Plan 02:
53+
54+
**`tests/infra/test_cd_workflows_static.py`** — Six test classes using `yaml.safe_load`:
55+
- `TestCD01DevAutoDeployOnMain` — cd-dev.yml push-to-main trigger and cd-base.yml call with `command: up, stack: dev`
56+
- `TestCD02StagingManualDispatchOnly` — cd-staging.yml has `workflow_dispatch` only (no `pull_request`)
57+
- `TestCD03ProdEnvironmentApproval` — cd-prod.yml passes `with.environment: prod`
58+
- `TestCD04WIFAuth` — cd-base.yml build+deploy jobs use `google-github-actions/auth@v3`, no static keys, `id-token: write`
59+
- `TestCD05CIUnchanged` — ci.yml has no `google-github-actions` steps and no `gcloud` run commands
60+
- `TestCDBaseStructure` — cd-base.yml has exactly `{build, deploy}` jobs, deploy needs build, build outputs sha, Docker GHA cache, conditional push on `command == up`, `gcp_project` input required, health check on dev up, pulumi config-map with imageTag
61+
62+
**`tests/infra/test_cd_static.py`** — Two test classes using `ast` module:
63+
- `TestImageTagConfig` — config.py has module-level `IMAGE_TAG` assignment and `cfg.get("imageTag") or ENV` pattern
64+
- `TestAppUsesImageTag` — app.py imports `IMAGE_TAG` on the `from config import` line and uses it (not `ENV`) in the registry_url concat
65+
66+
## Test State After This Plan
67+
68+
| Test Class | Status | Reason |
69+
|------------|--------|--------|
70+
| TestCD05CIUnchanged | PASS | ci.yml already has no GCP steps |
71+
| TestCD01DevAutoDeployOnMain::test_cd_dev_calls_cd_base_with_up | PASS | Existing cd-dev.yml happens to call cd-base.yml |
72+
| TestCD03ProdEnvironmentApproval::test_cd_prod_passes_environment_prod | PASS | Existing cd-prod.yml passes environment |
73+
| TestCD04WIFAuth::test_cd_base_deploy_job_uses_wif_auth | PASS | Existing cd-base.yml has WIF in single deploy job |
74+
| TestCD04WIFAuth::test_cd_base_no_static_key_steps | PASS | No static keys in existing workflow |
75+
| TestImageTagConfig (both) | PASS | Task 2 GREEN |
76+
| TestAppUsesImageTag (both) | PASS | Task 2 GREEN |
77+
| All remaining | FAIL | Expected RED — workflow files not yet rewritten (Plan 02) |
78+
79+
## Deviations from Plan
80+
81+
None — plan executed exactly as written.
82+
83+
## Known Stubs
84+
85+
None — IMAGE_TAG is fully wired from config.py to app.py. No placeholder values.
86+
87+
## Threat Flags
88+
89+
None — no new network endpoints, auth paths, or trust boundary crossings introduced. IMAGE_TAG is a non-secret Pulumi config value (git SHA string); T-5.1-01 disposition is `accept` per plan threat model.
90+
91+
## Self-Check: PASSED
92+
93+
- `tests/infra/test_cd_workflows_static.py` exists and contains all 6 required test classes
94+
- `tests/infra/test_cd_static.py` exists and contains both required test classes
95+
- Commit 50914d5 exists (test scaffold)
96+
- Commit 450e6cf exists (IMAGE_TAG config)
97+
- `grep "IMAGE_TAG" infra/config.py` returns the assignment line
98+
- `grep "IMAGE_TAG" infra/components/app.py` returns import and usage lines
99+
- `uv run pytest tests/infra/test_cd_static.py` — 4 passed
100+
- `uv run pytest tests/infra/test_cd_workflows_static.py::TestCD05CIUnchanged` — 1 passed

0 commit comments

Comments
 (0)