Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
6f8f472
ci: group Dependabot security updates for the examples
bnusunny Sep 13, 2026
52896af
ci: verify only the examples a pull request touches
bnusunny Sep 13, 2026
ccefd41
ci: auto-merge verified example-only Dependabot updates
bnusunny Sep 13, 2026
fa41e92
ci: document the verified Dependabot grouping semantics
bnusunny Sep 13, 2026
f314573
ci: harden the Dependabot auto-merge guards
bnusunny Sep 13, 2026
33c1cb3
ci: grant actions:read, count jobs safely, cover bundler
bnusunny Sep 13, 2026
38e46fd
ci: make Verify Examples self-verifying and gate on every check
bnusunny Sep 14, 2026
9be72a5
ci: keep both fail-safe paths from aborting instead of refusing
bnusunny Sep 14, 2026
1d2aca2
ci: make one-PR-per-example structural, and classify merge failures
bnusunny Sep 14, 2026
3c1ba63
ci: re-evaluate auto-merge on a schedule, and stop swallowing jq
bnusunny Sep 14, 2026
3a3940f
ci: spell out Dependabot's directories, and three review fixes
bnusunny Sep 14, 2026
1d2c360
ci: four review fixes, and a guard against example config drift
bnusunny Sep 14, 2026
b21954d
ci: complete the manifest table, require an open PR, fix a stale comment
bnusunny Sep 14, 2026
bb4e344
ci: grant checks/statuses, fail closed on an absent rollup, widen pus…
bnusunny Sep 14, 2026
399f8a5
Merge branch 'main' into ci/dependabot-grouping-and-targeted-example-…
bnusunny Sep 14, 2026
8ee5b00
ci: shorten the group names, and narrow what counts as shared
bnusunny Sep 14, 2026
8a35cb2
ci: fix two ways the drift guard could lie, and refresh a stale header
bnusunny Sep 14, 2026
b1670c3
ci: merge after approval, not auto-merge on green
bnusunny Sep 14, 2026
f049b3c
ci: let the ruleset decide whether the merge is unattended
bnusunny Sep 14, 2026
b39624c
ci: settle on merge-after-approval, and stop floating a ruleset bypass
bnusunny Sep 14, 2026
fb69e5d
ci: drop the merge automation, leave Dependabot PRs to their owners
bnusunny Sep 15, 2026
0316c21
ci: fail on a missing matrix key, assert the prefix, say what was ver…
bnusunny Sep 15, 2026
e26d40f
ci: assert the matrix entries' shape, and scope the policy keys to ex…
bnusunny Sep 15, 2026
3a96664
ci: no-renames diff, anchor example paths, assert the last two group …
bnusunny Sep 15, 2026
5b7e8b6
ci: catch a duplicated directory, and re-run the guard when its limit…
bnusunny Sep 16, 2026
b13d801
ci: derive the kinds from the matrix, normalize paths, lint both temp…
bnusunny Sep 16, 2026
89a1c58
ci: derive the covered set from the matrix, fix the header sim, asser…
bnusunny Sep 17, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
107 changes: 107 additions & 0 deletions .github/dependabot.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
# Dependabot configuration for the example applications.
#
# The adapter itself (Cargo.toml at the repo root) is deliberately NOT managed here:
# it ships as the lambda-adapter binary, and its dependencies are reviewed by hand.
#
# Why this file exists: without it, Dependabot opens one pull request per advisory
# per manifest. That produced 69 open PRs, eight of them against the single lockfile
# in examples/remix/remix-app, which then conflict with each other as soon as one
# lands. The `groups` blocks below batch each ecosystem's security fixes so an
# example app is updated by one PR instead of eight.
#
# Grouping is per directory: Dependabot treats each (ecosystem, directory) pair as its
# own update and does not batch across directories unless `group-by: dependency-name`
# is set, which applies to version updates only. So this yields one pull request per
# example app per ecosystem, which is the intent — a broken bump then fails one
# example's verification instead of blocking every example's fixes at once.
#
# `open-pull-requests-limit: 0` disables *version* updates and leaves *security*
# updates on, which keeps the current behavior: Dependabot only opens PRs for
# advisories, not for every dependency that drifts behind. Security updates are
# explicitly exempt from this limit and do not count toward it. Raise it per ecosystem
# if you later want routine version bumps too.
version: 2
updates:
- package-ecosystem: npm
directories:
- "/examples/**"
Comment thread
bnusunny marked this conversation as resolved.
Outdated
schedule:
interval: weekly
open-pull-requests-limit: 0
commit-message:
prefix: chore
groups:
examples-npm:
applies-to: security-updates
patterns:
- "*"

- package-ecosystem: pip
directories:
- "/examples/**"
schedule:
interval: weekly
open-pull-requests-limit: 0
commit-message:
prefix: chore
groups:
examples-pip:
applies-to: security-updates
patterns:
- "*"

- package-ecosystem: gomod
directories:
- "/examples/**"
schedule:
interval: weekly
open-pull-requests-limit: 0
commit-message:
prefix: chore
groups:
examples-gomod:
applies-to: security-updates
patterns:
- "*"

- package-ecosystem: maven
directories:
- "/examples/**"
schedule:
interval: weekly
open-pull-requests-limit: 0
commit-message:
prefix: chore
groups:
examples-maven:
applies-to: security-updates
patterns:
- "*"

- package-ecosystem: nuget
directories:
- "/examples/**"
schedule:
interval: weekly
open-pull-requests-limit: 0
commit-message:
prefix: chore
groups:
examples-nuget:
applies-to: security-updates
patterns:
- "*"

- package-ecosystem: cargo
directories:
- "/examples/**"
schedule:
interval: weekly
open-pull-requests-limit: 0
commit-message:
prefix: chore
groups:
examples-cargo:
applies-to: security-updates
patterns:
- "*"
Comment thread
bnusunny marked this conversation as resolved.
Outdated
26 changes: 26 additions & 0 deletions .github/example-matrix.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
{
"image": [
{ "name": "expressjs", "path": "/", "expect_body": "Hi there!" },
{ "name": "fastapi", "path": "/", "expect_body": "message" },
{ "name": "fastapi-background-tasks", "path": "/", "expect_body": "message" },
{ "name": "fasthtml", "path": "/", "expect_body": "Hello World" },
{ "name": "gin", "path": "/", "expect_body": "message" },
{ "name": "nextjs", "path": "/", "expect_body": "Next.js Logo" },
{ "name": "remix", "path": "/", "expect_body": "Welcome to" },
{ "name": "springboot", "path": "/v1/", "expect_body": "Hello, world!" }
],
"zip": [
Comment thread
bnusunny marked this conversation as resolved.
{ "name": "deno-zip", "path": "/", "expect_body": "success", "port": "8000" },
{ "name": "expressjs-zip", "path": "/", "expect_body": "Hi there!", "port": "8000" },
{ "name": "fastapi-zip", "path": "/", "expect_body": "message", "port": "8000" },
{ "name": "fasthtml-zip", "path": "/", "expect_body": "Hello World", "port": "8000" },
{ "name": "flask-zip", "path": "/", "expect_body": "message", "port": "8000" },
{ "name": "gin-zip", "path": "/", "expect_body": "message", "port": "8000" },
{ "name": "remix-zip", "path": "/", "expect_body": "Welcome to", "port": "8000" },
{ "name": "springboot-zip", "path": "/v1/", "expect_body": "Hello, world!", "port": "8000" }
],
"stream": [
{ "name": "fasthtml-response-streaming", "kind": "image", "path": "/", "expect_body": "Serverless Bedtime" },
{ "name": "fasthtml-response-streaming-zip", "kind": "zip-fasthtml", "path": "/", "expect_body": "Click to stream" }
]
}
62 changes: 62 additions & 0 deletions .github/scripts/select-examples.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
#!/usr/bin/env bash
#
# Picks which examples the Verify Examples workflow needs to run, and writes one
# matrix per job kind (image, zip, stream) to $GITHUB_OUTPUT.
#
# A dependency bump under examples/remix/remix-app has no bearing on springboot or
# deno-zip, so verifying all 18 matrix entries for it burns runners for no signal.
# With ~70 open Dependabot PRs against the examples that cost dominates CI.
#
# Fails safe: anything this script cannot resolve confidently — no base commit, a
# base commit not present locally, a change to shared code — verifies everything.
#
# Inputs:
# BASE_SHA base commit to diff against; empty means "verify everything"
# GITHUB_OUTPUT set by Actions
set -euo pipefail

MATRIX="$(dirname "$0")/../example-matrix.json"

emit_all() {
local kind
for kind in image zip stream; do
Comment thread
bnusunny marked this conversation as resolved.
Outdated
echo "$kind=$(jq -c ".$kind" "$MATRIX")" >>"$GITHUB_OUTPUT"
Comment thread
bnusunny marked this conversation as resolved.
Outdated
done
}

if [[ -z "${BASE_SHA:-}" ]]; then
echo "No base commit (push or manual run): verifying every example."
emit_all
exit 0
fi

if ! git cat-file -e "$BASE_SHA^{commit}" 2>/dev/null; then
echo "Base commit $BASE_SHA is not available locally: verifying every example."
emit_all
exit 0
fi

# HEAD is the pull request's merge commit, so diffing from the merge base yields
# exactly the changes the PR contributes.
base="$(git merge-base "$BASE_SHA" HEAD)"
changed="$(git diff --name-only "$base" HEAD)"
Comment thread
bnusunny marked this conversation as resolved.
Outdated
echo "Changed files:"
echo "$changed" | sed 's/^/ /'

# Shared inputs every example is built against: the adapter itself, the layer
# wrapper, this workflow's own machinery.
if grep -qE '^(src/|layer/|Cargo\.toml$|Cargo\.lock$|\.github/workflows/examples\.yaml$|\.github/scripts/|\.github/example-matrix\.json$)' <<<"$changed"; then
Comment thread
bnusunny marked this conversation as resolved.
Outdated
Comment thread
bnusunny marked this conversation as resolved.
Outdated
echo "A shared path changed: verifying every example."
emit_all
exit 0
fi

# examples/<name>/... -> <name>
names="$(grep -oE '^examples/[^/]+' <<<"$changed" | cut -d/ -f2 | sort -u | jq -R . | jq -sc .)"
Comment thread
bnusunny marked this conversation as resolved.
Outdated
echo "Changed examples: $names"

for kind in image zip stream; do
matrix="$(jq -c --argjson names "$names" "[.$kind[] | select(.name as \$n | \$names | index(\$n))]" "$MATRIX")"
echo "$kind=$matrix"
echo "$kind=$matrix" >>"$GITHUB_OUTPUT"
done
78 changes: 78 additions & 0 deletions .github/workflows/dependabot-automerge.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
name: Dependabot Auto-merge

# Merges Dependabot pull requests that only touch example applications, once Verify
# Examples has actually verified them.
#
# Why workflow_run rather than `gh pr merge --auto`: auto-merge is gated on a
# repository's *required* status checks, and Verify Examples is path-filtered to
# examples/**. A required check from a path-filtered workflow never reports on pull
# requests outside those paths, which would block every source-only pull request
# forever. Keying off the completed run sidesteps that: the run itself is the evidence,
# and it is tied to the head commit that will be merged.
#
# Requires no branch protection and no repository settings.
on:
workflow_run:
workflows: ["Verify Examples"]
types:
- completed

permissions:
contents: write
pull-requests: write
Comment thread
bnusunny marked this conversation as resolved.
Outdated
Comment thread
bnusunny marked this conversation as resolved.
Outdated

jobs:
merge:
# Only a green pull request run can merge anything. A push run has no pull request
# to merge, and a failed run is the whole point of the gate.
if: >-
github.event.workflow_run.event == 'pull_request' &&
github.event.workflow_run.conclusion == 'success'
runs-on: ubuntu-24.04
steps:
- name: Merge if this is an example-only Dependabot update that was verified
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
REPO: ${{ github.repository }}
RUN_ID: ${{ github.event.workflow_run.id }}
PR_NUMBER: ${{ github.event.workflow_run.pull_requests[0].number }}
run: |
set -euo pipefail

if [[ -z "${PR_NUMBER:-}" ]]; then
echo "Run is not associated with a pull request; nothing to merge."
exit 0
fi

author=$(gh pr view "$PR_NUMBER" --repo "$REPO" --json author -q .author.login)
if [[ "$author" != "app/dependabot" && "$author" != "dependabot[bot]" ]]; then
echo "PR #$PR_NUMBER is authored by $author, not Dependabot. Skipping."
exit 0
fi

# Scope by what the PR actually changes rather than by its branch name:
# grouped updates do not reliably encode the directory in the ref. Examples
# are demo apps, so a bad bump costs a broken sample; the adapter's own
# dependencies, the workflows, and the templates stay manual.
outside=$(gh api --paginate "repos/$REPO/pulls/$PR_NUMBER/files" -q '.[].filename' \
Comment thread
bnusunny marked this conversation as resolved.
Outdated
| grep -v '^examples/' || true)
if [[ -n "$outside" ]]; then
echo "PR #$PR_NUMBER changes files outside examples/:"
echo "$outside" | sed 's/^/ /'
exit 0
fi

# Refuse to merge on template validation alone. If `select` filtered every
# matrix entry out, the changed example is not covered by a build-and-boot
# test, and a human should look at it. This is what keeps an uncovered
# example (nextjs-zip, say) from riding in on a green-but-empty run.
verified=$(gh api "repos/$REPO/actions/runs/$RUN_ID/jobs" --paginate \
-q '[.jobs[] | select(.name | startswith("test-")) | select(.conclusion == "success")] | length')
if [[ "$verified" -eq 0 ]]; then
Comment thread
bnusunny marked this conversation as resolved.
Outdated
echo "No test-* job ran for PR #$PR_NUMBER: the changed example has no"
echo "build-and-boot coverage, so this needs a human. Skipping."
exit 0
fi

echo "PR #$PR_NUMBER is example-only and passed $verified verification job(s). Merging."
gh pr merge "$PR_NUMBER" --repo "$REPO" --squash --delete-branch
Comment thread
bnusunny marked this conversation as resolved.
Outdated
Loading
Loading