-
Notifications
You must be signed in to change notification settings - Fork 1
288 lines (246 loc) · 12 KB
/
Copy pathci.yml
File metadata and controls
288 lines (246 loc) · 12 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
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
name: CI
on:
push:
branches: [main]
pull_request:
branches: [main]
permissions:
contents: read
# Cancel superseded in-progress runs on the same ref — rapid pushes to a PR
# otherwise launch redundant overlapping quality/test/cdk-check runs.
concurrency:
group: ci-${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
quality:
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
- uses: astral-sh/setup-uv@fac544c07dec837d0ccb6301d7b5580bf5edae39 # v8.2.0
with:
enable-cache: true
# Fails when the committed lambda/requirements.txt drifts from uv.lock.
# Dependabot's uv ecosystem regenerates pyproject.toml + uv.lock but does
# not know about the exported requirements file that PythonFunction bundles
# into the deployed Lambda — so without this check, a merged Dependabot PR
# could ship a Lambda built from stale pins. Fix by running `make lock`.
- name: Check lambda/requirements.txt is in sync with uv.lock
run: |
uv export --only-group lambda --no-emit-project --no-header --format requirements.txt -o /tmp/expected-requirements.txt
if ! diff -q /tmp/expected-requirements.txt lambda/requirements.txt > /dev/null; then
echo "::error::lambda/requirements.txt is out of sync with uv.lock. Run 'make lock' locally and commit the result."
diff /tmp/expected-requirements.txt lambda/requirements.txt || true
exit 1
fi
- name: Install CDK + lint + docs (and test for pytest plugins referenced by configs)
run: uv sync --locked --group cdk --group test --group lint --group docs
- name: Run pre-commit hooks
run: uv run pre-commit run --all-files
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
with:
node-version: "22"
cache: "npm"
# markdownlint comes from package.json (pinned + Dependabot-tracked, same
# supply-chain posture as the CDK CLI). Rules live in .markdownlint.yaml;
# CHANGELOG.md is excluded because it is generated by git-cliff.
- name: Install node tooling
run: npm ci
- name: Lint Markdown
run: npx markdownlint --config .markdownlint.yaml "*.md" "docs/**/*.md" --ignore CHANGELOG.md
- name: Prune uv cache
run: uv cache prune --ci
test:
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
- uses: astral-sh/setup-uv@fac544c07dec837d0ccb6301d7b5580bf5edae39 # v8.2.0
with:
enable-cache: true
- name: Install Lambda runtime + test runner
run: uv sync --locked --only-group lambda --only-group test
- name: Run unit tests
env:
AWS_DEFAULT_REGION: us-east-1
run: uv run pytest tests/unit -v
# ── OpenAPI contract gates ─────────────────────────────────────────────
# docs/openapi.json is committed so PR diffs show API-contract changes.
# Gate 1 (drift): regenerate and compare byte-for-byte — generation is
# hermetic (the generator pins its own env vars), so any diff means a
# route/model changed without `make openapi` being run.
- name: Check committed OpenAPI spec is current
run: |
uv run python scripts/generate_openapi.py --out-path /tmp/openapi-latest.json
if ! cmp --silent /tmp/openapi-latest.json docs/openapi.json; then
echo "::error::docs/openapi.json is out of date. Run 'make openapi' locally and commit the result."
diff /tmp/openapi-latest.json docs/openapi.json || true
exit 1
fi
# Gate 2 (breaking changes, PRs only): diff this PR's spec against the
# base branch's committed spec and fail on breaking API changes. The base
# spec is read out of git (not a raw URL) so the gate works on forks and
# is race-free against pushes to the base branch. Skipped gracefully on
# the bootstrap case where the base branch has no spec yet. Written into
# the workspace because the oasdiff action mounts only the workspace.
- name: Fetch base-branch OpenAPI spec
if: github.event_name == 'pull_request'
id: base-spec
run: |
git fetch origin "${{ github.base_ref }}" --depth=1
if git show "origin/${{ github.base_ref }}:docs/openapi.json" > openapi-base.json 2>/dev/null; then
echo "exists=true" >> "$GITHUB_OUTPUT"
else
echo "exists=false" >> "$GITHUB_OUTPUT"
echo "No committed spec on base branch yet — skipping breaking-change gate."
fi
- name: API breaking-change gate
if: github.event_name == 'pull_request' && steps.base-spec.outputs.exists == 'true'
uses: oasdiff/oasdiff-action/breaking@ae5ef6402ebe218ac6e872c59d0fa4557694e75c # v0.1.4
with:
base: openapi-base.json
revision: docs/openapi.json
fail-on: ERR
- name: Upload coverage report
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
if: always()
with:
name: coverage-report
path: report.html
- name: Prune uv cache
run: uv cache prune --ci
cdk-check:
# Native ARM64 runner: the Lambda targets arm64 (Graviton), so CDK's
# PythonFunction bundling container (public.ecr.aws/sam/build-python3.14)
# runs natively here. On amd64 runners the same step needed QEMU binfmt
# emulation — slower, and one more action in the supply chain.
runs-on: ubuntu-24.04-arm
timeout-minutes: 25
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
- uses: astral-sh/setup-uv@fac544c07dec837d0ccb6301d7b5580bf5edae39 # v8.2.0
with:
enable-cache: true
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
with:
node-version: "22"
cache: "npm"
- name: Install CDK + test runner
run: uv sync --locked --group cdk --group test
# The CDK CLI is pinned in package.json and tracked by Dependabot's npm
# ecosystem — `npm ci` installs the locked version and `npx cdk` runs it.
# This replaces the previous floating `npm install -g`, which was the one
# un-tracked supply-chain input in an otherwise fully-pinned repo.
- name: Install pinned CDK CLI
run: npm ci
# The '**' glob is required to descend into Stage-nested stacks. Without
# it, `cdk synth` only synthesizes the top-level App (which contains the
# Stage but no stacks directly), so asset bundling never runs against the
# actual WAF/backend/frontend stacks. `make cdk-synth` uses the same glob
# — this aligns the CI gate with what runs locally.
- name: Run cdk synth
env:
CDK_DEFAULT_ACCOUNT: "123456789012"
CDK_DEFAULT_REGION: us-east-1
AWS_DEFAULT_REGION: us-east-1
run: npx cdk synth '**' --quiet
# cdk-nag v3 hard gate: CDK signals a failed policy validation by setting
# process.exitCode in the NODE process — for a Python app that's jsii's
# throwaway kernel, so the synth above exits 0 even with findings
# (verified live). The checker fails on any violation AND on a missing
# report (packs not attached = broken gate, not a pass); see
# scripts/check_validation_report.py.
- name: Check cdk-nag validation report
run: uv run python scripts/check_validation_report.py cdk.out
- name: Run CDK stack assertion tests
env:
AWS_DEFAULT_REGION: us-east-1
run: uv run pytest tests/cdk -v --override-ini="addopts=" --timeout=120
- name: Prune uv cache
run: uv cache prune --ci
cdk-diff:
# PR-only infra-change visibility: synthesize the base branch and the PR,
# then post a CloudFormation diff (resources added / removed / modified, plus
# IAM statement changes) as a sticky PR comment so a reviewer can catch a
# destructive change to a stateful resource before merge. Hermetic — it diffs
# two locally synthesized templates, so it needs NO AWS account or credentials
# (unlike `cdk diff` against deployed stacks, which would need OIDC and break
# this repo's credential-free CI). Same native-ARM runner as cdk-check so the
# PythonFunction bundling container runs without QEMU emulation.
if: github.event_name == 'pull_request'
runs-on: ubuntu-24.04-arm
timeout-minutes: 25
permissions:
contents: read
pull-requests: write # post/update the diff comment (read-only on fork PRs — summary still carries it)
steps:
- name: Check out the PR
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
path: pr
- name: Check out the base branch
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
ref: ${{ github.base_ref }}
path: base
- uses: astral-sh/setup-uv@fac544c07dec837d0ccb6301d7b5580bf5edae39 # v8.2.0
with:
enable-cache: true
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
with:
node-version: "22"
cache: "npm"
cache-dependency-path: pr/package-lock.json
# Synthesize both branches into self-contained cloud assemblies. Each
# checkout installs its own pinned deps (uv + the npm CDK CLI), so the diff
# is correct even when the PR bumps a CDK version.
- name: Synthesize base and PR templates
env:
CDK_DEFAULT_ACCOUNT: "123456789012"
CDK_DEFAULT_REGION: us-east-1
AWS_DEFAULT_REGION: us-east-1
run: |
for dir in base pr; do
echo "::group::synth $dir"
( cd "$dir" \
&& uv sync --locked --group cdk --group test \
&& npm ci \
&& npx cdk synth '**' --quiet -o cdk.out )
echo "::endgroup::"
done
# Stdlib-only driver (no extra dependency): for each stack it runs
# `cdk diff --template <base> --app <pr-assembly>` — no re-synth, no AWS.
# Runs from the PR checkout so the script's `npx cdk` resolves the PINNED
# CLI in pr/node_modules (from the repo root npx would fetch an unpinned
# one). Paths are relative to pr/; the report lands at the workspace root.
- name: Render CDK diff
working-directory: pr
run: |
python3 scripts/cdk_pr_diff.py \
--pr-out cdk.out --base-out ../base/cdk.out \
--base-ref "${{ github.base_ref }}" --output ../cdk-diff.md
cat ../cdk-diff.md >> "$GITHUB_STEP_SUMMARY"
# Sticky comment: update the existing bot comment in place (matched by a
# hidden marker) instead of stacking a new one per push. Best-effort —
# fork PRs hand the workflow a read-only token, so this 403s and the job
# summary remains the source of truth.
- name: Post or update the diff comment
if: always()
continue-on-error: true
env:
GH_TOKEN: ${{ github.token }}
REPO: ${{ github.repository }}
PR_NUMBER: ${{ github.event.pull_request.number }}
run: |
marker='<!-- cdk-pr-diff -->'
printf '%s\n%s' "$marker" "$(cat cdk-diff.md)" > body.md
existing=$(gh api "repos/$REPO/issues/$PR_NUMBER/comments" --paginate \
--jq "map(select(.body | startswith(\"$marker\"))) | .[0].id // empty" | head -n1)
if [ -n "$existing" ]; then
gh api --method PATCH "repos/$REPO/issues/comments/$existing" -F body=@body.md >/dev/null
else
gh api --method POST "repos/$REPO/issues/$PR_NUMBER/comments" -F body=@body.md >/dev/null
fi
- name: Prune uv cache
if: always()
run: uv cache prune --ci