Skip to content

Commit 125e205

Browse files
scottschreckengaustclaudekrokoko
authored
feat(ci): add deploy pipeline with OIDC, dynamic stack naming, and deploy-intent artifact (#98)
* feat(ci): add deploy.yml workflow triggered by build completion Adds a deploy workflow that: - Fires on workflow_run after build.yml succeeds - Resolves deploy targets from PR labels (deploy=all, deploy:<type>=one) or defaults to all registered types on push to main - Skips entirely (no approval prompt) when no deploy labels are present - Downloads the exact cdk-<compute_type>-out artifact from the build run - Uses OIDC to assume the CDK bootstrap deploy role - Deploys via `cdk deploy --app cdk/cdk.out --all --require-approval never` - Protected by the `deploy` GitHub environment (manual approval required) - Concurrency: non-cancellable once started, max-parallel 3 Part of #73 Phase 3. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * feat(ci): dynamic stack naming and workflow_dispatch deploy trigger build.yml: Replace hardcoded stackName with trigger-aware naming: - push to main: main-<compute_type> - pull_request: pr<number>-<compute_type> - merge_group: mg<pr_number>-<compute_type> - workflow_dispatch: <branch>-<compute_type> - fallback: <compute_type>-<sha7> All inputs sanitized (alphanumeric + hyphens, 60-char branch cap). deploy.yml: Add workflow_dispatch trigger with compute_type choice input (all, agentcore). Handle non-PR triggers (push to main, workflow_dispatch on build) by deploying all registered types. Label-based resolution only applies to PR triggers. Part of #73 Phase 3. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * refactor(ci): move deploy input to build.yml, use deploy-intent artifact build.yml now owns the deploy decision via workflow_dispatch choice input: - "-" (default): no deploy - "agentcore": deploy agentcore after build Build always writes a deploy-intent.json artifact encoding the decision: - push to main: intent = compute_type (deploy) - workflow_dispatch with choice: intent = selected value - pull_request: intent = "labels" (defer to deploy.yml label check) - anything else: intent = "-" (no deploy) deploy.yml simplified to a pure consumer: - Removes workflow_dispatch trigger (single entry point is build.yml) - Downloads deploy-intent.json from triggering build run - Reads intent: "-" = skip, "labels" = check PR labels, else = deploy Part of #73 Phase 3. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix(ci): add input validation and allowlist enforcement for compute types Addresses 5 security findings: 1. CRITICAL: deploy.yml wildcard case now validates intent against ALLOWED_COMPUTE_TYPES before passing to matrix. Invalid values cause the workflow to fail with an error annotation. 2. MEDIUM: PR label deploy:<type> values are filtered through validate_compute_type(). Invalid labels emit a warning and are ignored rather than passed to the deploy matrix. 3. MEDIUM: sanitize() now lowercases input and prefixes "s-" if the result starts with a digit (CloudFormation requires letter start). 4. LOW: deploy-intent.json is now written with jq (safe JSON encoding) instead of shell string interpolation. 5. LOW: PR_NUMBER is validated as numeric before use in stack names. The ALLOWED_COMPUTE_TYPES allowlist is defined as an env var in each step that performs validation. When new compute types are added to the matrix, this allowlist must be updated in both build.yml and deploy.yml. Part of #73 Phase 3. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix(ci): add deploy-intent.json to .gitignore The file is generated during build and was being picked up by the mutation detection step, causing the build to fail. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix(security): skip deploy pipeline for fork PRs + document CI/CD flow Adds `head_repository.full_name == github.repository` guard to deploy.yml's resolve-targets job. Prevents fork PRs from triggering deploy approval prompts — a fork could modify build.yml to produce a deploy-intent artifact that would otherwise reach the approval gate. Also documents the full deploy.yml pipeline in DEPLOYMENT_GUIDE.md (build → deploy stages, security controls, fork exclusion rationale, administrator setup). This makes the deploy flow explicit for contributors, reviewers, agents, and administrators. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix(ci): use compact jq output in filter_valid_types to prevent multi-line matrix corruption Without -c, jq pretty-prints the JSON array across multiple lines. GitHub Actions parses GITHUB_OUTPUT as one key=value per line, so `matrix` captured only `[` and fromJson() in the deploy job failed — the deploy:<type> label path never deployed. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix(ci): handle sanitize() edge cases — trailing hyphen after cut and empty/leading-hyphen results Two bugs in stack-name sanitize(): 1. cut -c1-60 ran after trailing-hyphen trim, so a hyphen at position 60 survived → CloudFormation rejects trailing hyphens. Fix: reorder to cut first, then trim. 2. All-special-char refs sanitized to empty → STACK_NAME="-agentcore" (leading hyphen). The guard only checked ^[0-9], not ^[a-z]. Fix: guard checks for empty or any non-letter start. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix(ci): remove dead deploy:* literal match — index() is exact, not glob jq's index("deploy:*") matches only a label literally named "deploy:*", not a wildcard. The bare "deploy" label already triggers all-types deployment. This dead branch was misleading and could become a surprise full-deploy vector if such a label were ever created. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * chore(ci): derive ALL_TYPES from ALLOWED_COMPUTE_TYPES to reduce drift risk ALLOWED_COMPUTE_TYPES was duplicated across both workflows plus the matrix; adding a new compute type required updating 5 places. Now deploy.yml derives ALL_TYPES dynamically from ALLOWED_COMPUTE_TYPES, and cross-reference comments link the remaining sync points. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * docs(ci): clarify exit vs return in validate_compute_type across workflows build.yml uses exit 1 (no recovery — invalid dispatch input aborts step). deploy.yml uses return 1 (callers handle gracefully — skip type or set empty matrix). One-line comments prevent future confusion. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix(ci): resolve PR number via head_sha fallback when event field is empty workflow_run.pull_requests[0].number is empty for forks and often for indirect workflow_run triggers. Previously the labels deploy path would silently no-op. Now we fall back to gh api commits/{sha}/pulls to resolve the PR number, with a warning if neither source yields a result. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * feat(ci): add cdk diff job before deploy approval gate (B2 compensating control) Adds a `diff` job that runs immediately after resolve-targets (no environment gate). It downloads the same CDK artifact, assumes AWS credentials, and runs `cdk diff` — surfacing all CloudFormation changes in the GitHub step summary. The reviewer can inspect the diff before clicking "Approve" on the deploy environment gate. The deploy job now `needs: [resolve-targets, diff]` ensuring the diff completes before approval is requested. `--require-approval never` is retained on the deploy step because CDK hard-fails without TTY in CI; the GitHub environment approval gate is the human checkpoint. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> --------- Co-authored-by: bgagent <345885+scottschreckengaust@users.noreply.github.com> Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com> Co-authored-by: Alain Krok <alkrok@amazon.com>
1 parent aba8939 commit 125e205

5 files changed

Lines changed: 455 additions & 2 deletions

File tree

.github/workflows/build.yml

Lines changed: 108 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)