-
Notifications
You must be signed in to change notification settings - Fork 162
484 lines (442 loc) · 20.1 KB
/
Copy pathexamples.yaml
File metadata and controls
484 lines (442 loc) · 20.1 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
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
name: Verify Examples
on:
pull_request:
branches:
- main
paths:
- "examples/**"
# This workflow's own machinery. The matrix file and the selector decide what
# gets verified at all, so a pull request that changes only those would
# otherwise not run here — and a malformed matrix or a selector regression
# would land unverified and surface later on an unrelated examples pull
# request. Including them makes the workflow verify its own changes.
- ".github/workflows/examples.yaml"
- ".github/scripts/select-examples.sh"
- ".github/scripts/verify-http.sh"
- ".github/scripts/check-example-config.sh"
- ".github/example-matrix.json"
- ".github/dependabot.yml"
# check-example-config.sh parses type-enum and header-max-length out of this file
# and asserts every entry's generated commit header against them, so a change to it
# has to re-run that assertion. Lowering header-max-length otherwise passes on the
# pull request that makes the change — pr.yaml and commitlint.yaml run, neither
# invokes the guard — and the bill arrives weeks later as a red Commit Lint on a
# Dependabot pull request nobody wrote. Headroom is ~12 characters today. It costs
# nothing: the selector does not treat it as a shared input, so such a pull request
# selects no examples and only validate does work.
- "commitlint.config.js"
push:
branches:
- main
# Every input select-examples.sh treats as shared, not just src/. layer/bootstrap in
# particular is the code path all eight zip examples boot — build-layer copies it
# into the artifact each of them injects — so leaving it out meant a regression there
# was verified by nothing at all.
paths:
- "src/**"
- "layer/**"
- "Cargo.toml"
- "Cargo.lock"
# Those same paths are deliberately NOT pull request triggers: adding them would run
# all eighteen matrix entries on every source pull request. Adapter changes are
# verified against the examples on push to main (above), and the selector's shared-path
# rule still applies to a pull request that touches both source and examples.
#
# The push list is deliberately shorter than the selector's shared-input set, which also
# names this workflow, verify-http.sh and the matrix file. Those three are pull request
# triggers, so a change to any of them already fans out to all eighteen entries before it
# lands; repeating them here would only re-run on main what the pull request just ran.
# The two lists answer different questions — what starts a run, versus what forces a run
# to cover everything.
workflow_dispatch:
permissions:
contents: read
env:
CARGO_TERM_COLOR: always
jobs:
# Narrows the test matrices to the examples a pull request actually touches, so a
# dependency bump in one example does not rebuild and boot all eighteen. Any change
# to shared code (src/, layer/, this workflow) still verifies everything. See
# .github/scripts/select-examples.sh.
select:
runs-on: ubuntu-24.04
outputs:
image: ${{ steps.filter.outputs.image }}
zip: ${{ steps.filter.outputs.zip }}
stream: ${{ steps.filter.outputs.stream }}
# Every selected example name, kind-agnostic, so examples-verified can say what it
# verified without binding each kind again.
selected: ${{ steps.filter.outputs.selected }}
steps:
- uses: actions/checkout@v4
with:
# Needed to diff against the base commit rather than a shallow clone.
fetch-depth: 0
- name: Select the examples to verify
id: filter
env:
BASE_SHA: ${{ github.event.pull_request.base.sha }}
run: .github/scripts/select-examples.sh
validate:
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v4
with:
python-version: "3.13"
- uses: aws-actions/setup-sam@v2
with:
use-installer: true
token: ${{ secrets.GITHUB_TOKEN }}
# The two hand-maintained lists describing the examples drift silently otherwise:
# an unclaimed manifest reverts to one Dependabot pull request per advisory with a
# commit header Commit Lint rejects, and that is invisible until the pull requests
# appear weeks later.
- name: Check the example config lists against the tree
run: |
pip install pyyaml --quiet
.github/scripts/check-example-config.sh
- name: Validate all SAM templates
run: |
failed=0
# Both spellings: examples/sinatra and examples/go-http-zip use template.yml,
# so a .yaml-only find left them unlinted while examples-verified still claimed
# "template validation only" for them — and both have Dependabot entries and no
# matrix entry, so that claim is the only thing their pull requests get.
for template in $(find examples -maxdepth 2 \
\( -name "template.yaml" -o -name "template.yml" \) | sort); do
dir=$(dirname "$template")
echo "Validating $dir..."
if ! sam validate --template "$template" --lint 2>&1; then
echo "FAIL: $dir"
failed=1
fi
done
if [ "$failed" -eq 1 ]; then
echo "Some templates failed validation"
exit 1
fi
# Only when something will use it. The artifact exists for the test jobs, so building
# it when `select` chose nothing is several minutes of runner time for nobody.
build-layer:
needs: [select]
if: >-
needs.select.outputs.image != '[]' ||
needs.select.outputs.zip != '[]' ||
needs.select.outputs.stream != '[]'
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@v4
- name: Install stable toolchain
run: rustup target add x86_64-unknown-linux-musl
- name: Install cargo lambda
run: pip3 install cargo-lambda
- name: Configure Rust cache
uses: Swatinem/rust-cache@v2
- name: Build x86_64 layer
run: |
cargo lambda build --release --extension --target x86_64-unknown-linux-musl
mkdir -p layer-x86_64
cp layer/bootstrap layer-x86_64/
cp target/lambda/extensions/lambda-adapter layer-x86_64/
- uses: actions/upload-artifact@v4
with:
name: layer-x86_64
path: layer-x86_64/
# Docker-based examples verified with sam local start-api. Each matrix entry
# asserts an exact status (and optional body substring) via verify-http.sh,
# not just liveness.
# Excluded: nginx, flask, aspnet-mvc (web app hardcodes port 8080
# which conflicts with SAM's Lambda Runtime Interface Emulator).
test-image:
needs: [select, build-layer]
if: needs.select.outputs.image != '[]'
runs-on: ubuntu-24.04
strategy:
fail-fast: false
matrix:
example: ${{ fromJSON(needs.select.outputs.image) }}
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v4
with:
python-version: "3.13"
- uses: aws-actions/setup-sam@v2
with:
use-installer: true
token: ${{ secrets.GITHUB_TOKEN }}
- uses: actions/download-artifact@v4
with:
name: layer-x86_64
path: /tmp/layer-x86_64
- name: Build local adapter image
run: |
# Build the local adapter under every tag the example Dockerfiles
# reference, so each example's `docker build` (COPY --from=...:<tag>)
# uses the locally compiled binary instead of pulling the released
# image. Deriving the tags from the Dockerfiles keeps CI in lockstep
# with them (no hardcoded version to forget at release time).
chmod +x /tmp/layer-x86_64/lambda-adapter
tags=$(grep -rhoE 'aws-lambda-adapter:[0-9A-Za-z._-]+' examples | sed 's/.*://' | sort -u)
echo "Building local adapter for tags: $tags"
for tag in $tags; do
printf 'FROM scratch\nCOPY --chmod=755 lambda-adapter /lambda-adapter\n' | \
docker build -t public.ecr.aws/awsguru/aws-lambda-adapter:$tag -f- /tmp/layer-x86_64
done
- name: Build
working-directory: examples/${{ matrix.example.name }}
run: |
# Retry to tolerate transient registry rate limits (toomanyrequests: Rate
# exceeded). public.ecr.aws throttles unauthenticated pulls at ~1 req/s per
# source IP, and the matrix runs many jobs from shared runner IPs, so bursts
# collide. Exponential backoff with random jitter desynchronizes retries
# across jobs to avoid a thundering herd re-colliding on the same boundary.
max_attempts=5
attempt=1
while true; do
if sam build; then exit 0; fi
if [ "$attempt" -ge "$max_attempts" ]; then
echo "sam build failed after ${max_attempts} attempts"
exit 1
fi
delay=$(( 10 * 2 ** (attempt - 1) + RANDOM % 16 )) # ~10/20/40/80s + 0-15s jitter
echo "sam build failed (attempt ${attempt}/${max_attempts}), retrying in ${delay}s..."
sleep "$delay"
attempt=$(( attempt + 1 ))
done
- name: Start local API and verify
working-directory: examples/${{ matrix.example.name }}
run: |
# Set PORT=8000 to avoid conflict with SAM's RIE on port 8080.
# Start and verify in the same step so the backgrounded sam local
# stays attached to this shell while requests are served (splitting
# it across steps races the container warm-up and yields 500s).
echo '{"Parameters":{"PORT":"8000"}}' > /tmp/env.json
sam local start-api --port 3000 --warm-containers EAGER --container-env-vars /tmp/env.json &
echo "SAM_PID=$!" >> $GITHUB_ENV
"$GITHUB_WORKSPACE/.github/scripts/verify-http.sh" \
http://127.0.0.1:3000 \
"${{ matrix.example.path }}" \
200 \
"${{ matrix.example.expect_body }}"
- name: Stop local API
if: always()
run: kill $SAM_PID 2>/dev/null || true
# Zip-based examples verified with local layer. Each matrix entry asserts an
# exact status (and optional body substring) via verify-http.sh.
# `port` is the app's listening port, passed through to the adapter via the
# PORT parameter; it must differ from SAM's RIE port (8080).
# Excluded: aspnet-*-zip (hardcode port 8080),
# bun/nginx (need third-party layers), arm64 examples (javalin, rust-*),
# nextjs-zip (Makefile produces a zip artifact incompatible with sam local).
test-zip:
needs: [select, build-layer]
if: needs.select.outputs.zip != '[]'
runs-on: ubuntu-24.04
strategy:
fail-fast: false
matrix:
example: ${{ fromJSON(needs.select.outputs.zip) }}
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v4
with:
python-version: "3.13"
- uses: aws-actions/setup-sam@v2
with:
use-installer: true
token: ${{ secrets.GITHUB_TOKEN }}
- uses: denoland/setup-deno@v2
if: matrix.example.name == 'deno-zip'
with:
deno-version: v2.x
- uses: actions/download-artifact@v4
with:
name: layer-x86_64
path: /tmp/layer-x86_64
- name: Build
working-directory: examples/${{ matrix.example.name }}
run: |
# Retry to tolerate transient registry rate limits (toomanyrequests: Rate
# exceeded). public.ecr.aws throttles unauthenticated pulls at ~1 req/s per
# source IP, and the matrix runs many jobs from shared runner IPs, so bursts
# collide. Exponential backoff with random jitter desynchronizes retries
# across jobs to avoid a thundering herd re-colliding on the same boundary.
max_attempts=5
attempt=1
while true; do
if sam build; then exit 0; fi
if [ "$attempt" -ge "$max_attempts" ]; then
echo "sam build failed after ${max_attempts} attempts"
exit 1
fi
delay=$(( 10 * 2 ** (attempt - 1) + RANDOM % 16 )) # ~10/20/40/80s + 0-15s jitter
echo "sam build failed (attempt ${attempt}/${max_attempts}), retrying in ${delay}s..."
sleep "$delay"
attempt=$(( attempt + 1 ))
done
- name: Inject local layer into build output
working-directory: examples/${{ matrix.example.name }}
run: |
# Prepare local layer directory with correct structure
LAYER_DIR=".aws-sam/build/LocalAdapterLayer"
mkdir -p "$LAYER_DIR/extensions"
cp /tmp/layer-x86_64/bootstrap "$LAYER_DIR/"
cp /tmp/layer-x86_64/lambda-adapter "$LAYER_DIR/extensions/"
chmod +x "$LAYER_DIR/bootstrap" "$LAYER_DIR/extensions/lambda-adapter"
# Add local layer resource and replace remote layer ARN refs
pip install pyyaml -q
python3 << 'PYEOF'
import yaml
with open(".aws-sam/build/template.yaml") as f:
t = yaml.safe_load(f)
t["Resources"]["LocalAdapterLayer"] = {
"Type": "AWS::Lambda::LayerVersion",
"Properties": {"Content": "LocalAdapterLayer", "LayerName": "local-adapter"},
}
for r in t.get("Resources", {}).values():
layers = r.get("Properties", {}).get("Layers", [])
for i, l in enumerate(layers):
if "LambdaAdapterLayerX86" in str(l):
layers[i] = {"Ref": "LocalAdapterLayer"}
with open(".aws-sam/build/template.yaml", "w") as f:
yaml.dump(t, f)
PYEOF
- name: Start local API and verify
working-directory: examples/${{ matrix.example.name }}
run: |
# Pass the app's listening port via PORT (must differ from RIE's 8080).
# Start and verify in the same step so the backgrounded sam local
# stays attached to this shell while requests are served (splitting
# it across steps races the container warm-up and yields 500s).
echo '{"Parameters":{"PORT":"${{ matrix.example.port }}"}}' > /tmp/env.json
sam local start-api --port 3000 --warm-containers EAGER --container-env-vars /tmp/env.json &
echo "SAM_PID=$!" >> $GITHUB_ENV
"$GITHUB_WORKSPACE/.github/scripts/verify-http.sh" \
http://127.0.0.1:3000 \
"${{ matrix.example.path }}" \
200 \
"${{ matrix.example.expect_body }}"
- name: Stop local API
if: always()
run: kill $SAM_PID 2>/dev/null || true
# Response-streaming examples verified by building and running the real app,
# then asserting an exact status/body with verify-http.sh. These use a Lambda
# Function URL with RESPONSE_STREAM and have no API Gateway events, so
# sam local start-api (used by test-image/test-zip) cannot drive them.
#
# `kind` selects how the app is launched:
# image - build the app image (its Dockerfile COPYs the local adapter
# image built below) and run it with PORT=8000.
# zip-fasthtml - run `python app/main.py` (FastHTML's serve() binds 8000).
# AWS_REGION is set so boto3 / AnthropicBedrock clients construct without real
# creds; the verified routes only render UI and never call Bedrock.
test-stream:
needs: [select, build-layer]
if: needs.select.outputs.stream != '[]'
runs-on: ubuntu-24.04
strategy:
fail-fast: false
matrix:
example: ${{ fromJSON(needs.select.outputs.stream) }}
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v4
with:
python-version: "3.12"
- uses: actions/download-artifact@v4
with:
name: layer-x86_64
path: /tmp/layer-x86_64
- name: Build local adapter image
run: |
# Build the local adapter under every tag the example Dockerfiles
# reference, so each example's `docker build` (COPY --from=...:<tag>)
# uses the locally compiled binary instead of pulling the released
# image. Deriving the tags from the Dockerfiles keeps CI in lockstep
# with them (no hardcoded version to forget at release time).
chmod +x /tmp/layer-x86_64/lambda-adapter
tags=$(grep -rhoE 'aws-lambda-adapter:[0-9A-Za-z._-]+' examples | sed 's/.*://' | sort -u)
echo "Building local adapter for tags: $tags"
for tag in $tags; do
printf 'FROM scratch\nCOPY --chmod=755 lambda-adapter /lambda-adapter\n' | \
docker build -t public.ecr.aws/awsguru/aws-lambda-adapter:$tag -f- /tmp/layer-x86_64
done
- name: Build and start app
working-directory: examples/${{ matrix.example.name }}
env:
KIND: ${{ matrix.example.kind }}
run: |
case "$KIND" in
image)
docker build -t stream-app app
docker run -d --name stream-app \
-e PORT=8000 -e AWS_REGION=us-east-1 -e AWS_DEFAULT_REGION=us-east-1 \
-p "8000:8000" stream-app
;;
zip-fasthtml)
pip install -r app/requirements.txt
PORT=8000 nohup python app/main.py > /tmp/app.log 2>&1 &
echo "APP_PID=$!" >> $GITHUB_ENV
;;
*)
echo "Unknown kind: $KIND"; exit 1 ;;
esac
- name: Verify
run: |
"$GITHUB_WORKSPACE/.github/scripts/verify-http.sh" \
http://127.0.0.1:8000 \
"${{ matrix.example.path }}" \
200 \
"${{ matrix.example.expect_body }}" \
|| { echo "--- app logs ---"; docker logs stream-app 2>/dev/null || cat /tmp/app.log 2>/dev/null || true; exit 1; }
- name: Stop app
if: always()
run: |
docker rm -f stream-app 2>/dev/null || true
kill $APP_PID 2>/dev/null || true
# One aggregate result for the whole workflow, so a reviewer has a single thing to look
# at before merging: the matrix jobs' names embed their parameters
# ("test-zip (deno-zip, /, success, 8000)") and vanish entirely when `select` filters an
# example out, so there is no stable per-example name to read.
#
# `skipped` counts as a pass — that is what a filtered-out matrix means. `failure`
# and `cancelled` do not.
#
# Note if you ever make this a required check in main's ruleset: this workflow is
# path-filtered, so it never runs on a pull request that touches none of those paths and
# the check would never report there, blocking that pull request indefinitely. Making it
# required needs a companion job reporting the same name on the complementary paths.
examples-verified:
if: always()
needs: [select, validate, build-layer, test-image, test-zip, test-stream]
runs-on: ubuntu-24.04
steps:
- name: Check the verification results
env:
RESULTS: ${{ join(needs.*.result, ' ') }}
SELECTED: ${{ needs.select.outputs.selected }}
run: |
echo "job results: $RESULTS"
for result in $RESULTS; do
case "$result" in
success | skipped) ;;
*)
echo "A verification job reported '$result'."
exit 1
;;
esac
done
# Green is not the same claim in both cases. Most examples have no matrix entry,
# so their pull requests filter every matrix to empty, skip the test jobs, and
# arrive here green having built and booted nothing. Say which it was, because a
# reviewer reading this one check has nothing else to go on — the per-example job
# names vanish when the matrix is filtered, which is why this job exists.
if [[ -n "${SELECTED:-}" ]]; then
echo "Built and booted: $SELECTED"
echo "**Verified:** $SELECTED" >>"$GITHUB_STEP_SUMMARY"
else
echo "No example was built or booted; this run verified templates only."
echo "**No example was built or booted** — template validation only." \
>>"$GITHUB_STEP_SUMMARY"
fi