From 822a79d323e161315eee04e139f251d05f8f1dc7 Mon Sep 17 00:00:00 2001 From: scottschreckengaust <345885+scottschreckengaust@users.noreply.github.com> Date: Fri, 15 May 2026 00:23:43 +0000 Subject: [PATCH 1/6] feat(ci): rename computeVariant to compute_type and apply as resource tag Aligns CI and CDK terminology with the existing ComputeType union in repo-config.ts. build.yml matrix key, env var, and cdk.context.json key are all renamed from computeVariant to compute_type. The CDK app now reads compute_type from context (default: agentcore) and applies it as a resource tag for per-type baseline diffs and cost attribution. Closes phase 2 items in #73. Co-Authored-By: Claude Opus 4.6 (1M context) --- .github/workflows/build.yml | 12 ++++++------ cdk/src/main.ts | 3 +++ cdk/test/stacks/github-tags.test.ts | 24 ++++++++++++++++++++++++ 3 files changed, 33 insertions(+), 6 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 3fd3cc4b..7d04a1c8 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -26,7 +26,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - variant: [agentcore] + compute_type: [agentcore] outputs: self_mutation_happened: ${{ steps.self_mutation.outputs.self_mutation_happened }} env: @@ -96,7 +96,7 @@ jobs: esac - name: Generate CDK context env: - VARIANT: ${{ matrix.variant }} + COMPUTE_TYPE: ${{ matrix.compute_type }} TAG_SHA: ${{ steps.tags.outputs.sha }} TAG_REF: ${{ steps.tags.outputs.ref }} TAG_REF_TYPE: ${{ steps.tags.outputs.ref-type }} @@ -111,7 +111,7 @@ jobs: TAG_REPOSITORY: ${{ github.repository }} run: | jq -n \ - --arg computeVariant "$VARIANT" \ + --arg compute_type "$COMPUTE_TYPE" \ --arg stackName "backgroundagent-dev" \ --arg sha "$TAG_SHA" \ --arg ref "$TAG_REF" \ @@ -126,7 +126,7 @@ jobs: --arg workflow "$TAG_WORKFLOW" \ --arg repository "$TAG_REPOSITORY" \ '{ - "computeVariant": $computeVariant, + "compute_type": $compute_type, "stackName": $stackName, "github:sha": $sha, "github:ref": $ref, @@ -155,10 +155,10 @@ jobs: run: mise run install - name: build run: mise run build - - name: Upload CDK artifact (${{ matrix.variant }}) + - name: Upload CDK artifact (${{ matrix.compute_type }}) uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: - name: cdk-${{ matrix.variant }}-out + name: cdk-${{ matrix.compute_type }}-out path: | cdk/cdk.out/ cdk/cdk.context.json diff --git a/cdk/src/main.ts b/cdk/src/main.ts index 496cfbd4..bf1c1a45 100644 --- a/cdk/src/main.ts +++ b/cdk/src/main.ts @@ -42,6 +42,9 @@ const stack = new AgentStack( }, ); +const computeType = app.node.tryGetContext('compute_type') ?? 'agentcore'; +Tags.of(stack).add('compute_type', computeType); + const githubTagKeys = [ 'sha', 'ref', diff --git a/cdk/test/stacks/github-tags.test.ts b/cdk/test/stacks/github-tags.test.ts index a273fa7c..96bd1d44 100644 --- a/cdk/test/stacks/github-tags.test.ts +++ b/cdk/test/stacks/github-tags.test.ts @@ -44,6 +44,9 @@ function synthWithTags(context: Record = {}): Template { env: { account: '123456789012', region: 'us-east-1' }, }); + const computeType = app.node.tryGetContext('compute_type') ?? 'agentcore'; + Tags.of(stack).add('compute_type', computeType); + const githubTagKeys = [ 'sha', 'ref', 'ref-type', 'actor', 'head-ref', 'base-ref', 'pr-number', 'run-id', 'run-attempt', @@ -125,4 +128,25 @@ describe('github:* resource tags', () => { expect(tags.find(t => t.Key === 'github:sha')!.Value).toBe('none'); expect(tags.find(t => t.Key === 'github:head-ref')!.Value).toBe('none'); }); + + test('compute_type tag defaults to "agentcore" when no context is provided', () => { + const resources = templateWithDefaults.findResources('AWS::DynamoDB::Table'); + const firstResource = Object.values(resources)[0]; + const tags: Array<{ Key: string; Value: string }> = firstResource?.Properties?.Tags ?? []; + + const tag = tags.find(t => t.Key === 'compute_type'); + expect(tag).toBeDefined(); + expect(tag!.Value).toBe('agentcore'); + }); + + test('compute_type tag reflects context value when provided', () => { + const template = synthWithTags({ compute_type: 'ecs' }); + const resources = template.findResources('AWS::DynamoDB::Table'); + const firstResource = Object.values(resources)[0]; + const tags: Array<{ Key: string; Value: string }> = firstResource?.Properties?.Tags ?? []; + + const tag = tags.find(t => t.Key === 'compute_type'); + expect(tag).toBeDefined(); + expect(tag!.Value).toBe('ecs'); + }); }); From feccf16091bd9195ce570b13e775c89ad848a626 Mon Sep 17 00:00:00 2001 From: bgagent <345885+scottschreckengaust@users.noreply.github.com> Date: Fri, 15 May 2026 01:19:57 +0000 Subject: [PATCH 2/6] 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:=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--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) --- .github/workflows/deploy.yml | 116 +++++++++++++++++++++++++++++++++++ 1 file changed, 116 insertions(+) create mode 100644 .github/workflows/deploy.yml diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml new file mode 100644 index 00000000..a857fdcd --- /dev/null +++ b/.github/workflows/deploy.yml @@ -0,0 +1,116 @@ +name: deploy +on: + # zizmor: ignore[dangerous-triggers] — intentional; workflow_run is required + # for OIDC id-token on PR builds. Mitigations: env-var-only untrusted input, + # least-privilege permissions per job, deploy environment approval gate. + workflow_run: + workflows: [build] + types: [completed] +permissions: {} +jobs: + resolve-targets: + if: github.event.workflow_run.conclusion == 'success' + runs-on: ubuntu-latest + permissions: + actions: read + pull-requests: read + outputs: + matrix: ${{ steps.targets.outputs.matrix }} + has_targets: ${{ steps.targets.outputs.has_targets }} + run_id: ${{ github.event.workflow_run.id }} + steps: + - name: Resolve deploy targets from labels + id: targets + env: + GH_TOKEN: ${{ github.token }} + HEAD_BRANCH: ${{ github.event.workflow_run.head_branch }} + EVENT_TYPE: ${{ github.event.workflow_run.event }} + REPO: ${{ github.repository }} + PR_NUMBER_FROM_EVENT: ${{ github.event.workflow_run.pull_requests[0].number }} + run: | + ALL_TYPES='["agentcore"]' + + # Push to main always deploys all registered types + if [[ "$HEAD_BRANCH" == "main" && "$EVENT_TYPE" == "push" ]]; then + echo "matrix=$ALL_TYPES" >> "$GITHUB_OUTPUT" + echo "has_targets=true" >> "$GITHUB_OUTPUT" + exit 0 + fi + + # For PRs, look up labels via API + if [[ -z "$PR_NUMBER_FROM_EVENT" ]]; then + echo "matrix=[]" >> "$GITHUB_OUTPUT" + echo "has_targets=false" >> "$GITHUB_OUTPUT" + exit 0 + fi + + LABELS=$(gh api "repos/$REPO/pulls/$PR_NUMBER_FROM_EVENT" --jq '[.labels[].name]') + + if echo "$LABELS" | jq -e 'index("deploy:*")' > /dev/null 2>&1; then + echo "matrix=$ALL_TYPES" >> "$GITHUB_OUTPUT" + echo "has_targets=true" >> "$GITHUB_OUTPUT" + elif echo "$LABELS" | jq -e '[.[] | select(startswith("deploy:"))] | length > 0' > /dev/null 2>&1; then + TYPES=$(echo "$LABELS" | jq -c '[.[] | select(startswith("deploy:")) | ltrimstr("deploy:")]') + echo "matrix=$TYPES" >> "$GITHUB_OUTPUT" + echo "has_targets=true" >> "$GITHUB_OUTPUT" + elif echo "$LABELS" | jq -e 'index("deploy")' > /dev/null 2>&1; then + echo "matrix=$ALL_TYPES" >> "$GITHUB_OUTPUT" + echo "has_targets=true" >> "$GITHUB_OUTPUT" + else + echo "matrix=[]" >> "$GITHUB_OUTPUT" + echo "has_targets=false" >> "$GITHUB_OUTPUT" + fi + + deploy: + needs: resolve-targets + if: needs.resolve-targets.outputs.has_targets == 'true' + runs-on: ubuntu-latest + environment: deploy + concurrency: + group: deploy-${{ matrix.compute_type }} + cancel-in-progress: false + strategy: + matrix: + compute_type: ${{ fromJson(needs.resolve-targets.outputs.matrix) }} + max-parallel: 3 + permissions: + id-token: write + contents: read + actions: read + steps: + - name: Checkout + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + persist-credentials: false + + - name: Download CDK artifact (${{ matrix.compute_type }}) + uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0 + with: + name: cdk-${{ matrix.compute_type }}-out + path: cdk/ + run-id: ${{ needs.resolve-targets.outputs.run_id }} + github-token: ${{ github.token }} + + - name: Configure AWS credentials + uses: aws-actions/configure-aws-credentials@7474bc4690e29a8392af63c5b98e7449536d5c3a # v4.3.1 + with: + role-to-assume: ${{ secrets.AWS_ROLE_TO_ASSUME }} + aws-region: ${{ vars.AWS_REGION }} + + - name: Install mise + uses: jdx/mise-action@1648a7812b9aeae629881980618f079932869151 # v4.0.1 + with: + cache: true + + - name: Setup Node.js + uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 + with: + node-version: 22.x + + - name: Install dependencies + run: yarn install --immutable + + - name: Deploy + env: + COMPUTE_TYPE: ${{ matrix.compute_type }} + run: npx cdk deploy --app cdk/cdk.out --all --require-approval never From e8324013874a7e7469bb79893e7c9665bc7e6226 Mon Sep 17 00:00:00 2001 From: bgagent <345885+scottschreckengaust@users.noreply.github.com> Date: Fri, 15 May 2026 01:43:06 +0000 Subject: [PATCH 3/6] feat(ci): dynamic stack naming and workflow_dispatch deploy trigger build.yml: Replace hardcoded stackName with trigger-aware naming: - push to main: main- - pull_request: pr- - merge_group: mg- - workflow_dispatch: - - fallback: - 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) --- .github/workflows/build.yml | 42 ++++++++++++++++++++++++++++++++++- .github/workflows/deploy.yml | 43 +++++++++++++++++++++++++++++++----- 2 files changed, 78 insertions(+), 7 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 7d04a1c8..4904f67c 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -94,9 +94,49 @@ jobs: echo "pr-number=" >> "$GITHUB_OUTPUT" ;; esac + - name: Resolve stack name + id: naming + env: + EVENT_NAME: ${{ github.event_name }} + COMPUTE_TYPE: ${{ matrix.compute_type }} + GH_SHA: ${{ github.sha }} + GH_REF_NAME: ${{ github.ref_name }} + PR_NUMBER: ${{ steps.tags.outputs.pr-number }} + run: | + sanitize() { + echo "$1" | tr '/_.' '-' | sed 's/[^a-z0-9-]//g; s/--*/-/g; s/^-//; s/-$//' | cut -c1-60 + } + + case "$EVENT_NAME" in + push) + REF=$(sanitize "$GH_REF_NAME") + STACK_NAME="${REF}-${COMPUTE_TYPE}" + ;; + pull_request|pull_request_target) + STACK_NAME="pr${PR_NUMBER}-${COMPUTE_TYPE}" + ;; + merge_group) + if [[ -n "$PR_NUMBER" ]]; then + STACK_NAME="mg${PR_NUMBER}-${COMPUTE_TYPE}" + else + STACK_NAME="${COMPUTE_TYPE}-${GH_SHA:0:7}" + fi + ;; + workflow_dispatch) + REF=$(sanitize "$GH_REF_NAME") + STACK_NAME="${REF}-${COMPUTE_TYPE}" + ;; + *) + STACK_NAME="${COMPUTE_TYPE}-${GH_SHA:0:7}" + ;; + esac + + echo "stack_name=$STACK_NAME" >> "$GITHUB_OUTPUT" + echo "Stack name: $STACK_NAME" - name: Generate CDK context env: COMPUTE_TYPE: ${{ matrix.compute_type }} + STACK_NAME: ${{ steps.naming.outputs.stack_name }} TAG_SHA: ${{ steps.tags.outputs.sha }} TAG_REF: ${{ steps.tags.outputs.ref }} TAG_REF_TYPE: ${{ steps.tags.outputs.ref-type }} @@ -112,7 +152,7 @@ jobs: run: | jq -n \ --arg compute_type "$COMPUTE_TYPE" \ - --arg stackName "backgroundagent-dev" \ + --arg stackName "$STACK_NAME" \ --arg sha "$TAG_SHA" \ --arg ref "$TAG_REF" \ --arg ref_type "$TAG_REF_TYPE" \ diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index a857fdcd..f585572c 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -6,10 +6,21 @@ on: workflow_run: workflows: [build] types: [completed] + workflow_dispatch: + inputs: + compute_type: + description: "Compute type to deploy" + required: true + type: choice + options: + - all + - agentcore permissions: {} jobs: resolve-targets: - if: github.event.workflow_run.conclusion == 'success' + if: >- + github.event_name == 'workflow_dispatch' || + github.event.workflow_run.conclusion == 'success' runs-on: ubuntu-latest permissions: actions: read @@ -17,21 +28,41 @@ jobs: outputs: matrix: ${{ steps.targets.outputs.matrix }} has_targets: ${{ steps.targets.outputs.has_targets }} - run_id: ${{ github.event.workflow_run.id }} + run_id: ${{ steps.targets.outputs.run_id }} steps: - - name: Resolve deploy targets from labels + - name: Resolve deploy targets id: targets env: GH_TOKEN: ${{ github.token }} + EVENT_NAME: ${{ github.event_name }} HEAD_BRANCH: ${{ github.event.workflow_run.head_branch }} - EVENT_TYPE: ${{ github.event.workflow_run.event }} + BUILD_EVENT_TYPE: ${{ github.event.workflow_run.event }} REPO: ${{ github.repository }} PR_NUMBER_FROM_EVENT: ${{ github.event.workflow_run.pull_requests[0].number }} + WORKFLOW_RUN_ID: ${{ github.event.workflow_run.id }} + DISPATCH_COMPUTE_TYPE: ${{ inputs.compute_type }} run: | ALL_TYPES='["agentcore"]' - # Push to main always deploys all registered types - if [[ "$HEAD_BRANCH" == "main" && "$EVENT_TYPE" == "push" ]]; then + # workflow_dispatch: use input choice + if [[ "$EVENT_NAME" == "workflow_dispatch" ]]; then + if [[ "$DISPATCH_COMPUTE_TYPE" == "all" ]]; then + echo "matrix=$ALL_TYPES" >> "$GITHUB_OUTPUT" + else + echo "matrix=[\"$DISPATCH_COMPUTE_TYPE\"]" >> "$GITHUB_OUTPUT" + fi + echo "has_targets=true" >> "$GITHUB_OUTPUT" + # Find the latest successful build run for this branch + RUN_ID=$(gh api "repos/$REPO/actions/workflows/build.yml/runs?branch=$(git branch --show-current 2>/dev/null || echo main)&status=success&per_page=1" --jq '.workflow_runs[0].id // empty') + echo "run_id=${RUN_ID}" >> "$GITHUB_OUTPUT" + exit 0 + fi + + # workflow_run context from here on + echo "run_id=${WORKFLOW_RUN_ID}" >> "$GITHUB_OUTPUT" + + # Push to main or workflow_dispatch on build always deploys all + if [[ "$HEAD_BRANCH" == "main" && ("$BUILD_EVENT_TYPE" == "push" || "$BUILD_EVENT_TYPE" == "workflow_dispatch") ]]; then echo "matrix=$ALL_TYPES" >> "$GITHUB_OUTPUT" echo "has_targets=true" >> "$GITHUB_OUTPUT" exit 0 From 382c55178382f473c40f1e2c573b98b1a4968a3c Mon Sep 17 00:00:00 2001 From: bgagent <345885+scottschreckengaust@users.noreply.github.com> Date: Fri, 15 May 2026 02:04:21 +0000 Subject: [PATCH 4/6] 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) --- .github/workflows/build.yml | 43 +++++++++++++- .github/workflows/deploy.yml | 108 +++++++++++++++-------------------- 2 files changed, 87 insertions(+), 64 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 4904f67c..aafa50a2 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -1,7 +1,15 @@ name: build on: pull_request: {} - workflow_dispatch: {} + workflow_dispatch: + inputs: + deploy: + description: "Deploy after build (- = no deploy)" + type: choice + default: "-" + options: + - "-" + - agentcore permissions: actions: none attestations: none @@ -202,6 +210,39 @@ jobs: path: | cdk/cdk.out/ cdk/cdk.context.json + - name: Write deploy intent + env: + EVENT_NAME: ${{ github.event_name }} + GH_REF_NAME: ${{ github.ref_name }} + DISPATCH_DEPLOY: ${{ inputs.deploy }} + COMPUTE_TYPE: ${{ matrix.compute_type }} + run: | + # Determine deploy intent based on trigger type + case "$EVENT_NAME" in + push) + if [[ "$GH_REF_NAME" == "main" ]]; then + INTENT="$COMPUTE_TYPE" + else + INTENT="-" + fi + ;; + workflow_dispatch) + INTENT="$DISPATCH_DEPLOY" + ;; + pull_request|pull_request_target) + INTENT="labels" + ;; + *) + INTENT="-" + ;; + esac + echo "{\"deploy\":\"$INTENT\"}" > deploy-intent.json + echo "Deploy intent: $INTENT" + - name: Upload deploy intent + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: deploy-intent + path: deploy-intent.json - name: Find mutations id: self_mutation run: |- diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index f585572c..5919230d 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -6,21 +6,10 @@ on: workflow_run: workflows: [build] types: [completed] - workflow_dispatch: - inputs: - compute_type: - description: "Compute type to deploy" - required: true - type: choice - options: - - all - - agentcore permissions: {} jobs: resolve-targets: - if: >- - github.event_name == 'workflow_dispatch' || - github.event.workflow_run.conclusion == 'success' + if: github.event.workflow_run.conclusion == 'success' runs-on: ubuntu-latest permissions: actions: read @@ -28,69 +17,62 @@ jobs: outputs: matrix: ${{ steps.targets.outputs.matrix }} has_targets: ${{ steps.targets.outputs.has_targets }} - run_id: ${{ steps.targets.outputs.run_id }} + run_id: ${{ github.event.workflow_run.id }} steps: + - name: Download deploy intent + uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0 + with: + name: deploy-intent + run-id: ${{ github.event.workflow_run.id }} + github-token: ${{ github.token }} + - name: Resolve deploy targets id: targets env: GH_TOKEN: ${{ github.token }} - EVENT_NAME: ${{ github.event_name }} - HEAD_BRANCH: ${{ github.event.workflow_run.head_branch }} - BUILD_EVENT_TYPE: ${{ github.event.workflow_run.event }} REPO: ${{ github.repository }} PR_NUMBER_FROM_EVENT: ${{ github.event.workflow_run.pull_requests[0].number }} - WORKFLOW_RUN_ID: ${{ github.event.workflow_run.id }} - DISPATCH_COMPUTE_TYPE: ${{ inputs.compute_type }} run: | ALL_TYPES='["agentcore"]' + INTENT=$(jq -r '.deploy' deploy-intent.json) + echo "Deploy intent from build: $INTENT" - # workflow_dispatch: use input choice - if [[ "$EVENT_NAME" == "workflow_dispatch" ]]; then - if [[ "$DISPATCH_COMPUTE_TYPE" == "all" ]]; then - echo "matrix=$ALL_TYPES" >> "$GITHUB_OUTPUT" - else - echo "matrix=[\"$DISPATCH_COMPUTE_TYPE\"]" >> "$GITHUB_OUTPUT" - fi - echo "has_targets=true" >> "$GITHUB_OUTPUT" - # Find the latest successful build run for this branch - RUN_ID=$(gh api "repos/$REPO/actions/workflows/build.yml/runs?branch=$(git branch --show-current 2>/dev/null || echo main)&status=success&per_page=1" --jq '.workflow_runs[0].id // empty') - echo "run_id=${RUN_ID}" >> "$GITHUB_OUTPUT" - exit 0 - fi - - # workflow_run context from here on - echo "run_id=${WORKFLOW_RUN_ID}" >> "$GITHUB_OUTPUT" - - # Push to main or workflow_dispatch on build always deploys all - if [[ "$HEAD_BRANCH" == "main" && ("$BUILD_EVENT_TYPE" == "push" || "$BUILD_EVENT_TYPE" == "workflow_dispatch") ]]; then - echo "matrix=$ALL_TYPES" >> "$GITHUB_OUTPUT" - echo "has_targets=true" >> "$GITHUB_OUTPUT" - exit 0 - fi - - # For PRs, look up labels via API - if [[ -z "$PR_NUMBER_FROM_EVENT" ]]; then - echo "matrix=[]" >> "$GITHUB_OUTPUT" - echo "has_targets=false" >> "$GITHUB_OUTPUT" - exit 0 - fi + case "$INTENT" in + -) + echo "matrix=[]" >> "$GITHUB_OUTPUT" + echo "has_targets=false" >> "$GITHUB_OUTPUT" + ;; + labels) + # PR-triggered build — check labels + if [[ -z "$PR_NUMBER_FROM_EVENT" ]]; then + echo "matrix=[]" >> "$GITHUB_OUTPUT" + echo "has_targets=false" >> "$GITHUB_OUTPUT" + exit 0 + fi - LABELS=$(gh api "repos/$REPO/pulls/$PR_NUMBER_FROM_EVENT" --jq '[.labels[].name]') + LABELS=$(gh api "repos/$REPO/pulls/$PR_NUMBER_FROM_EVENT" --jq '[.labels[].name]') - if echo "$LABELS" | jq -e 'index("deploy:*")' > /dev/null 2>&1; then - echo "matrix=$ALL_TYPES" >> "$GITHUB_OUTPUT" - echo "has_targets=true" >> "$GITHUB_OUTPUT" - elif echo "$LABELS" | jq -e '[.[] | select(startswith("deploy:"))] | length > 0' > /dev/null 2>&1; then - TYPES=$(echo "$LABELS" | jq -c '[.[] | select(startswith("deploy:")) | ltrimstr("deploy:")]') - echo "matrix=$TYPES" >> "$GITHUB_OUTPUT" - echo "has_targets=true" >> "$GITHUB_OUTPUT" - elif echo "$LABELS" | jq -e 'index("deploy")' > /dev/null 2>&1; then - echo "matrix=$ALL_TYPES" >> "$GITHUB_OUTPUT" - echo "has_targets=true" >> "$GITHUB_OUTPUT" - else - echo "matrix=[]" >> "$GITHUB_OUTPUT" - echo "has_targets=false" >> "$GITHUB_OUTPUT" - fi + if echo "$LABELS" | jq -e 'index("deploy:*")' > /dev/null 2>&1; then + echo "matrix=$ALL_TYPES" >> "$GITHUB_OUTPUT" + echo "has_targets=true" >> "$GITHUB_OUTPUT" + elif echo "$LABELS" | jq -e '[.[] | select(startswith("deploy:"))] | length > 0' > /dev/null 2>&1; then + TYPES=$(echo "$LABELS" | jq -c '[.[] | select(startswith("deploy:")) | ltrimstr("deploy:")]') + echo "matrix=$TYPES" >> "$GITHUB_OUTPUT" + echo "has_targets=true" >> "$GITHUB_OUTPUT" + elif echo "$LABELS" | jq -e 'index("deploy")' > /dev/null 2>&1; then + echo "matrix=$ALL_TYPES" >> "$GITHUB_OUTPUT" + echo "has_targets=true" >> "$GITHUB_OUTPUT" + else + echo "matrix=[]" >> "$GITHUB_OUTPUT" + echo "has_targets=false" >> "$GITHUB_OUTPUT" + fi + ;; + *) + # Specific compute_type from push-to-main or workflow_dispatch + echo "matrix=[\"$INTENT\"]" >> "$GITHUB_OUTPUT" + echo "has_targets=true" >> "$GITHUB_OUTPUT" + ;; + esac deploy: needs: resolve-targets From 266a504b7c91ab264253f311f04892906ef288d8 Mon Sep 17 00:00:00 2001 From: bgagent <345885+scottschreckengaust@users.noreply.github.com> Date: Fri, 15 May 2026 02:41:22 +0000 Subject: [PATCH 5/6] 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: 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) --- .github/workflows/build.yml | 30 ++++++++++++++++++++---- .github/workflows/deploy.yml | 45 ++++++++++++++++++++++++++++++++---- 2 files changed, 66 insertions(+), 9 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index aafa50a2..7fb319af 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -112,7 +112,13 @@ jobs: PR_NUMBER: ${{ steps.tags.outputs.pr-number }} run: | sanitize() { - echo "$1" | tr '/_.' '-' | sed 's/[^a-z0-9-]//g; s/--*/-/g; s/^-//; s/-$//' | cut -c1-60 + local result + result=$(echo "$1" | tr '[:upper:]' '[:lower:]' | tr '/_.' '-' | sed 's/[^a-z0-9-]//g; s/--*/-/g; s/^-//; s/-$//' | cut -c1-60) + # CloudFormation requires stack names to start with a letter + if [[ "$result" =~ ^[0-9] ]]; then + result="s-${result}" + fi + echo "$result" } case "$EVENT_NAME" in @@ -121,10 +127,14 @@ jobs: STACK_NAME="${REF}-${COMPUTE_TYPE}" ;; pull_request|pull_request_target) + if [[ ! "$PR_NUMBER" =~ ^[0-9]+$ ]]; then + echo "::error::Invalid PR number: '$PR_NUMBER'" + exit 1 + fi STACK_NAME="pr${PR_NUMBER}-${COMPUTE_TYPE}" ;; merge_group) - if [[ -n "$PR_NUMBER" ]]; then + if [[ -n "$PR_NUMBER" && "$PR_NUMBER" =~ ^[0-9]+$ ]]; then STACK_NAME="mg${PR_NUMBER}-${COMPUTE_TYPE}" else STACK_NAME="${COMPUTE_TYPE}-${GH_SHA:0:7}" @@ -216,8 +226,17 @@ jobs: GH_REF_NAME: ${{ github.ref_name }} DISPATCH_DEPLOY: ${{ inputs.deploy }} COMPUTE_TYPE: ${{ matrix.compute_type }} + ALLOWED_COMPUTE_TYPES: "agentcore" run: | - # Determine deploy intent based on trigger type + validate_compute_type() { + local type="$1" + for allowed in $ALLOWED_COMPUTE_TYPES; do + [[ "$type" == "$allowed" ]] && return 0 + done + echo "::error::Invalid compute_type: '$type'. Allowed: $ALLOWED_COMPUTE_TYPES" + exit 1 + } + case "$EVENT_NAME" in push) if [[ "$GH_REF_NAME" == "main" ]]; then @@ -227,6 +246,9 @@ jobs: fi ;; workflow_dispatch) + if [[ "$DISPATCH_DEPLOY" != "-" ]]; then + validate_compute_type "$DISPATCH_DEPLOY" + fi INTENT="$DISPATCH_DEPLOY" ;; pull_request|pull_request_target) @@ -236,7 +258,7 @@ jobs: INTENT="-" ;; esac - echo "{\"deploy\":\"$INTENT\"}" > deploy-intent.json + jq -n --arg deploy "$INTENT" '{"deploy":$deploy}' > deploy-intent.json echo "Deploy intent: $INTENT" - name: Upload deploy intent uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 5919230d..073d5982 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -32,8 +32,32 @@ jobs: GH_TOKEN: ${{ github.token }} REPO: ${{ github.repository }} PR_NUMBER_FROM_EVENT: ${{ github.event.workflow_run.pull_requests[0].number }} + ALLOWED_COMPUTE_TYPES: "agentcore" run: | ALL_TYPES='["agentcore"]' + + validate_compute_type() { + local type="$1" + for allowed in $ALLOWED_COMPUTE_TYPES; do + [[ "$type" == "$allowed" ]] && return 0 + done + echo "::error::Invalid compute_type: '$type'. Allowed: $ALLOWED_COMPUTE_TYPES" + return 1 + } + + filter_valid_types() { + local input_json="$1" + local valid_json="[]" + for type in $(echo "$input_json" | jq -r '.[]'); do + if validate_compute_type "$type" 2>/dev/null; then + valid_json=$(echo "$valid_json" | jq --arg t "$type" '. + [$t]') + else + echo "::warning::Ignoring invalid compute_type from label: '$type'" + fi + done + echo "$valid_json" + } + INTENT=$(jq -r '.deploy' deploy-intent.json) echo "Deploy intent from build: $INTENT" @@ -43,7 +67,6 @@ jobs: echo "has_targets=false" >> "$GITHUB_OUTPUT" ;; labels) - # PR-triggered build — check labels if [[ -z "$PR_NUMBER_FROM_EVENT" ]]; then echo "matrix=[]" >> "$GITHUB_OUTPUT" echo "has_targets=false" >> "$GITHUB_OUTPUT" @@ -56,9 +79,17 @@ jobs: echo "matrix=$ALL_TYPES" >> "$GITHUB_OUTPUT" echo "has_targets=true" >> "$GITHUB_OUTPUT" elif echo "$LABELS" | jq -e '[.[] | select(startswith("deploy:"))] | length > 0' > /dev/null 2>&1; then - TYPES=$(echo "$LABELS" | jq -c '[.[] | select(startswith("deploy:")) | ltrimstr("deploy:")]') - echo "matrix=$TYPES" >> "$GITHUB_OUTPUT" - echo "has_targets=true" >> "$GITHUB_OUTPUT" + RAW_TYPES=$(echo "$LABELS" | jq -c '[.[] | select(startswith("deploy:")) | ltrimstr("deploy:")]') + VALIDATED=$(filter_valid_types "$RAW_TYPES") + COUNT=$(echo "$VALIDATED" | jq 'length') + if [[ "$COUNT" -gt 0 ]]; then + echo "matrix=$VALIDATED" >> "$GITHUB_OUTPUT" + echo "has_targets=true" >> "$GITHUB_OUTPUT" + else + echo "::warning::All deploy: labels were invalid" + echo "matrix=[]" >> "$GITHUB_OUTPUT" + echo "has_targets=false" >> "$GITHUB_OUTPUT" + fi elif echo "$LABELS" | jq -e 'index("deploy")' > /dev/null 2>&1; then echo "matrix=$ALL_TYPES" >> "$GITHUB_OUTPUT" echo "has_targets=true" >> "$GITHUB_OUTPUT" @@ -68,7 +99,11 @@ jobs: fi ;; *) - # Specific compute_type from push-to-main or workflow_dispatch + if ! validate_compute_type "$INTENT"; then + echo "matrix=[]" >> "$GITHUB_OUTPUT" + echo "has_targets=false" >> "$GITHUB_OUTPUT" + exit 1 + fi echo "matrix=[\"$INTENT\"]" >> "$GITHUB_OUTPUT" echo "has_targets=true" >> "$GITHUB_OUTPUT" ;; From 026562feb4e1f6acf91920b3a750bc9aa6210ac7 Mon Sep 17 00:00:00 2001 From: bgagent <345885+scottschreckengaust@users.noreply.github.com> Date: Fri, 15 May 2026 03:14:31 +0000 Subject: [PATCH 6/6] 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) --- .gitignore | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/.gitignore b/.gitignore index 60b31701..e32519e6 100644 --- a/.gitignore +++ b/.gitignore @@ -57,6 +57,11 @@ cdk.out/ cdk.context.json /assets/ +# ────────────────────────────────────────────── +# CI artifacts (generated during build) +# ────────────────────────────────────────────── +deploy-intent.json + # ────────────────────────────────────────────── # Build outputs (compiled TS → JS) # ──────────────────────────────────────────────