-
Notifications
You must be signed in to change notification settings - Fork 24
262 lines (239 loc) · 10.3 KB
/
deploy.yml
File metadata and controls
262 lines (239 loc) · 10.3 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
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' &&
github.event.workflow_run.head_repository.full_name == github.repository
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: Download deploy intent
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
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 }}
REPO: ${{ github.repository }}
PR_NUMBER_FROM_EVENT: ${{ github.event.workflow_run.pull_requests[0].number }}
HEAD_SHA: ${{ github.event.workflow_run.head_sha }}
# Single source of truth for this workflow; keep in sync with
# build.yml matrix.compute_type and workflow_dispatch.inputs.deploy.options
ALLOWED_COMPUTE_TYPES: "agentcore"
run: |
ALL_TYPES=$(printf '%s\n' $ALLOWED_COMPUTE_TYPES | jq -Rc . | jq -sc .)
resolve_pr_number() {
if [[ -n "$PR_NUMBER_FROM_EVENT" ]]; then
echo "$PR_NUMBER_FROM_EVENT"
return
fi
gh api "repos/$REPO/commits/$HEAD_SHA/pulls" --jq '.[0].number // empty' 2>/dev/null || true
}
PR_NUMBER=$(resolve_pr_number)
# return (not exit) — callers handle failure gracefully (skip type or set empty matrix)
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 -c --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"
case "$INTENT" in
-)
echo "matrix=[]" >> "$GITHUB_OUTPUT"
echo "has_targets=false" >> "$GITHUB_OUTPUT"
;;
labels)
if [[ -z "$PR_NUMBER" ]]; then
echo "::warning::Could not resolve PR number from event or head_sha"
echo "matrix=[]" >> "$GITHUB_OUTPUT"
echo "has_targets=false" >> "$GITHUB_OUTPUT"
exit 0
fi
LABELS=$(gh api "repos/$REPO/pulls/$PR_NUMBER" --jq '[.labels[].name]')
if echo "$LABELS" | jq -e '[.[] | select(startswith("deploy:"))] | length > 0' > /dev/null 2>&1; then
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:<type> 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"
else
echo "matrix=[]" >> "$GITHUB_OUTPUT"
echo "has_targets=false" >> "$GITHUB_OUTPUT"
fi
;;
*)
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"
;;
esac
# Surfaces CloudFormation changes in the step summary BEFORE the deploy
# approval gate. Uses a read-only IAM role (no deploy/mutate permissions).
# Configure the 'diff' environment with no required reviewers so it auto-runs;
# gate it later if read-access to stack templates needs approval.
diff:
needs: resolve-targets
if: needs.resolve-targets.outputs.has_targets == 'true'
runs-on: ubuntu-latest
environment: diff
strategy:
matrix:
compute_type: ${{ fromJson(needs.resolve-targets.outputs.matrix) }}
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@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
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: Setup Node.js
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
with:
node-version: 22.x
- name: Install dependencies
run: yarn install --immutable
- name: CDK Diff (full)
env:
COMPUTE_TYPE: ${{ matrix.compute_type }}
run: |
# --method=template: read-only comparison against deployed template;
# no change-set creation, no S3 asset publishing, no deploy role needed.
npx cdk diff --app cdk/cdk.out --all --method=template --no-color 2>&1 | tee cdk-diff-full.txt || true
- name: CDK Diff (security only)
env:
COMPUTE_TYPE: ${{ matrix.compute_type }}
run: |
echo "## Security Changes (\`$COMPUTE_TYPE\`)" >> "$GITHUB_STEP_SUMMARY"
echo "" >> "$GITHUB_STEP_SUMMARY"
npx cdk diff --app cdk/cdk.out --all --method=template --security-only --no-color 2>&1 | tee cdk-diff-security.txt
if [ -s cdk-diff-security.txt ]; then
echo '```' >> "$GITHUB_STEP_SUMMARY"
cat cdk-diff-security.txt >> "$GITHUB_STEP_SUMMARY"
echo '```' >> "$GITHUB_STEP_SUMMARY"
else
echo "No security-relevant changes detected." >> "$GITHUB_STEP_SUMMARY"
fi
- name: Upload diff artifacts
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: cdk-diff-${{ matrix.compute_type }}
path: |
cdk-diff-full.txt
cdk-diff-security.txt
deploy:
needs: [resolve-targets, diff]
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@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
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 }}
# --require-approval never: CDK hard-fails in non-TTY CI without this
# (throws "terminal (TTY) is not attached"). The approval mechanism is the
# GitHub 'deploy' environment gate above — a human must approve before this
# job starts. The 'diff' job surfaces all CloudFormation and security changes
# in the step summary (visible before approval), and the full diff is
# downloadable as an artifact. Defense in depth:
# 1. diff environment (read-only role) → surfaces changes pre-approval
# 2. deploy environment (required reviewers) → human gate
# 3. fork guard (line 14) → blocks untrusted repositories
# 4. OIDC federation → no stored credentials, request-scoped tokens
run: npx cdk deploy --app cdk/cdk.out --all --require-approval never