CI/CD Workflows Reference
GitHub Actions workflow architecture, mechanics, and customization.
Workflow Architecture
Orchestrator:
- ci-cd.yml - Main workflow coordinating all jobs based on trigger event
Reusable Workflows:
- config-summary.yml - Configuration and production mode detection
- metadata-extract.yml - Build metadata extraction
- docker-build.yml - Docker image build and push
- pull-and-promote.yml - Image promotion between registries (production mode)
- resolve-image-digest.yml - Digest lookup by tag (production mode)
- terraform-plan-apply.yml - Terraform deployment
- code-quality.yml - Code quality checks (ruff, mypy, pytest)
- required-checks.yml - Conditional status check wrapper
Key principle: Infrastructure as code + GitOps = reproducible deployments.
GitHub Variables (Auto-Created by Bootstrap)
Dev-only mode:
- Variables scoped to repository (no environments)
Production mode:
- Variables scoped to environments (dev/stage/prod)
| Variable Name | Description |
|---|---|
GOOGLE_CLOUD_PROJECT |
GCP project ID |
REGION |
GCP Compute region |
GOOGLE_CLOUD_LOCATION |
Vertex AI model endpoint routing |
IMAGE_NAME |
Docker image name (also agent_name) |
WORKLOAD_IDENTITY_PROVIDER |
WIF provider resource name |
ARTIFACT_REGISTRY_URI |
Registry URI |
ARTIFACT_REGISTRY_LOCATION |
Registry location |
TERRAFORM_STATE_BUCKET |
GCS bucket for main module state |
WORKLOAD_IDENTITY_POOL_PRINCIPAL_IDENTIFIER |
WIF principal identifier for main module IAM bindings |
OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT |
Capture LLM content in traces |
Note: These are Variables (not Secrets) because they're resource identifiers, not credentials. Security comes from WIF IAM policies.
ci-cd.yml (Orchestrator)
Triggers:
- Pull request to main (paths filtered)
- Push to main (paths filtered)
- Tag push matching v*
Key jobs:
- meta - Extract metadata (tags, SHA, context)
- config - Determine production mode
- build - Build Docker image (branch events only, not tags)
- resolve-digest - Look up image in stage by tag (tag events in production mode)
- dev-plan / dev-apply - Dev environment (branch events)
- stage-promote / stage-plan / stage-apply - Stage environment (merge in production mode)
- prod-promote / prod-plan / prod-apply - Prod environment (tags in production mode)
Concurrency:
- PR builds: Cancel in-progress on new push (cancel-in-progress: true)
- Main builds: Run sequentially (no cancellation, cancel-in-progress: false)
- Per-environment Terraform locking prevents state corruption
Path filtering:
paths:
- 'src/**'
- 'pyproject.toml'
- 'uv.lock'
- 'Dockerfile'
- '.dockerignore'
- 'terraform/main/**'
- '.github/workflows/ci-cd.yml'
- '.github/workflows/config-summary.yml'
- '.github/workflows/docker-build.yml'
- '.github/workflows/metadata-extract.yml'
- '.github/workflows/pull-and-promote.yml'
- '.github/workflows/resolve-image-digest.yml'
- '.github/workflows/terraform-plan-apply.yml'
Tag triggers (v*) always run regardless of paths.
Workflow Flows
Job-level dependency graphs showing how GitHub Actions jobs chain together. For the higher-level deployment strategy view, see Deployment Modes: Deployment Flow.
PR Flow
Trigger: Push to feature branch with open PR
What happens (both modes):
config → metadata-extract → docker-build → dev-plan
↓
Push to dev registry: pr-{number}-{sha}
↓
Terraform plan (no apply)
↓
Comment plan on PR
Result: Plan preview in PR comment, no actual deployment.
Merge Flow
Dev-only mode:
config → metadata-extract → docker-build → dev-plan → dev-apply
↓
Push to dev registry: {sha}, latest
↓
Deploy to dev Cloud Run
Production mode:
config → metadata-extract → docker-build
↓ ↓
└────────────────────┴─→ dev-plan → dev-apply
(parallel)
↓
stage-promote → stage-plan → stage-apply
↓
Pull from dev, push to stage
↓
Deploy to stage Cloud Run
Result: Dev deployed (always), stage deployed (production mode only).
Tag Flow
Dev-only mode:
config → metadata-extract → docker-build → dev-plan → dev-apply
↓
Push to dev registry: {sha}, latest, {version}
↓
Deploy to dev Cloud Run
Production mode:
config → metadata-extract → resolve-digest → prod-promote → prod-plan → prod-apply
↓ ↓ ↑
Look up image in Pull from stage (requires
stage by tag Push to prod approval)
↓
Deploy to prod Cloud Run (after approval)
Result: Version-tagged deployment. Prod requires manual approval in prod-apply environment.
Image Tagging Strategy
Pull Request builds:
- Format: pr-{number}-{sha} (e.g., pr-123-abc1234)
- Isolated from main builds
- Tagged for dev registry only
Main branch builds:
- Tags: {sha} (primary), latest
- Example: abc1234, latest
Version tag builds:
- Tags: {sha}, latest, {version}
- Example: abc1234, latest, v1.0.0
Deployment uses image digest (not tags) to ensure every rebuild triggers a new Cloud Run revision.
Reusable Workflows
config-summary.yml
Purpose: Determine deployment mode and create configuration summary.
Inputs:
- production_mode (boolean) - Enable multi-environment deployment
Outputs:
- production_mode - Pass-through for downstream jobs
- Job summary with deployment mode explanation
When it runs: First job in every ci-cd.yml run
metadata-extract.yml
Purpose: Extract build metadata (tags, SHA, context).
Outputs:
- Image tags (PR, SHA, latest, version)
- Build context (pull_request, push, tag)
- Metadata summary
When it runs: After config job in ci-cd.yml
docker-build.yml
Purpose: Build and push multi-platform Docker images.
Inputs:
- Image tags from metadata-extract.yml
- Registry URI and location
- Environment (dev/stage/prod)
Features:
- Multi-platform support (linux/amd64)
- Registry cache with protected buildcache tag
- Build provenance and SBOM generation
Outputs:
- Image digest (immutable identifier)
- Digest URI (registry/image@sha256:...)
When it runs: After metadata extraction (branch events only, not tags)
pull-and-promote.yml
Purpose: Promote images between registries (production mode only).
Inputs:
- Source environment (dev or stage)
- Target environment (stage or prod)
- Source digest
- Target tags
How it works:
1. Authenticate to source and target registries via WIF
2. Pull image from source registry by digest
3. Re-tag image with all target tags
4. Push to target registry
Outputs:
- Image digest (same as source)
- Digest URI in target registry
When it runs: Production mode deployments (dev → stage, stage → prod)
resolve-image-digest.yml
Purpose: Resolve image digest from tag (production mode only).
Inputs:
- Environment (stage)
- Tags to resolve
How it works:
1. Authenticate to registry via WIF
2. Query Artifact Registry for image by tag
3. Extract digest (sha256:...)
Outputs:
- Image digest
- All tags associated with the image
When it runs: Production mode tag deployments (lookup stage image for prod)
terraform-plan-apply.yml
Purpose: Plan and apply Terraform changes.
Inputs:
- Environment (dev/stage/prod)
- Action (plan/apply)
- Docker image digest
- WIF and state bucket details
- save_plan (boolean) - Save plan artifact
- use_saved_plan (boolean) - Use saved plan artifact
Features:
- Plan artifacts saved between jobs (ensures plan matches apply)
- PR comment with plan output (plan-only runs)
- Job summary with deployment details
- Terraform format, init, validate, plan, apply steps
When it runs: After build (or promote) for each environment
Key behavior:
- plan job on PR: Comment plan, don't save artifact
- plan job on merge: Save plan artifact (no comment)
- apply job: Use saved plan artifact
code-quality.yml
Purpose: Run code quality checks (ruff, mypy, pytest).
Steps:
1. Install uv and Python 3.13
2. Install dependencies with uv sync --locked
3. Run ruff format check
4. Run ruff linting
5. Run mypy type checking
6. Run pytest with coverage
Timeout: 10 minutes (typical: 2-3 minutes)
When it runs: Push to main (paths filtered) or called by required-checks.yml
required-checks.yml
Purpose: Conditional status check wrapper for branch protection.
How it works:
1. check-changes job: Use paths-filter to detect code changes
2. code-quality job: Run if code changed
3. required-status job: Always run (required in branch protection)
- Pass if no code changes
- Pass if code changed and quality checks passed
- Fail if code changed and quality checks failed
Why this exists: Branch protection requires a status check that always runs. This wrapper allows skipping quality checks when code hasn't changed while maintaining a consistent required status.
Workflow Behavior
Build cache:
- Registry cache with protected buildcache tag
- Significant speedup on cache hits
- Never expires (protected by cleanup policy in bootstrap)
Timeouts:
- Build: 30 minutes
- Deploy: 20 minutes per environment
- Code quality: 10 minutes
See workflow files for specific timeout values.
Job Summaries
Workflows generate formatted summaries in GitHub Actions UI:
Config summary:
- Deployment mode (dev-only vs production)
- Environment deployment plan
- Mode switching instructions
Metadata extraction:
- Build context (PR, main, tag, manual)
- Branch/tag name and commit SHA
- All image tags (bulleted list)
Terraform deployment:
- Environment and action (plan/apply)
- Docker image being deployed
- Step outcomes (format, init, validate, plan, apply)
- Deployed resources (Cloud Run URL, Cloud SQL, Agent Engine, GCS bucket, bastion instance/zone)
- Collapsible plan output
Job summaries provide quick insight without log analysis.
PR Comments
Terraform plan workflow posts formatted comments on PRs:
Comment includes:
- Plan summary (resources to add/change/destroy)
- Collapsible sections for detailed output
- Format, init, validation results
- Full plan output
Permissions: Requires pull-requests: write in ci-cd.yml (configured).
Authentication
Workload Identity Federation (WIF):
- Keyless authentication (no service account keys)
- GitHub Actions requests OIDC token
- GCP validates against WIF provider
- Grants temporary credentials scoped to repository
IAM roles: See terraform/bootstrap/module/gcp/main.tf for complete role list.
Security:
- Repository-scoped IAM bindings (attribute condition on repository name)
- Minimal permissions (only required roles)
- Environment isolation (production mode, separate projects)
- Cross-project IAM is registry-scoped (not project-level)
Customization
Change Deployment Mode
Edit production_mode in .github/workflows/ci-cd.yml:
jobs:
config:
uses: ./.github/workflows/config-summary.yml
with:
production_mode: true # or false for dev-only
See Deployment Modes for complete instructions.
Add Environment Variables
Runtime config (LOG_LEVEL, SERVE_WEB_INTERFACE, etc.):
1. Settings → Environments → {environment} → Environment variables
2. Add or edit variable
3. Re-run deployment or push new commit
Infrastructure config (CORS origins, etc.):
1. Edit terraform/main/main.tf
2. Create PR
3. Merge PR → deploys via CI/CD
See Deployment Modes for runtime vs infrastructure distinction.
Add Build Steps
Edit .github/workflows/ci-cd.yml or reusable workflows:
- Code quality checks → Edit code-quality.yml
- Integration tests → Add job after docker-build in ci-cd.yml
- Custom notifications → Add to orchestrator
Modify Triggers
Edit .github/workflows/ci-cd.yml triggers:
on:
pull_request:
paths:
- 'src/**'
# Add more paths
push:
branches:
- main
# Add more branches
push:
tags:
- 'v*'
# Add more tag patterns