Skip to content

Commit df1dccb

Browse files
committed
ci: allow selecting exact PR e2e tests
1 parent 67073aa commit df1dccb

3 files changed

Lines changed: 148 additions & 48 deletions

File tree

.github/workflows/e2e-tests.yml

Lines changed: 130 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,18 @@ on:
88
cdk_branch:
99
description: 'CDK repo branch to build from (default: main)'
1010
default: 'main'
11+
pr_number:
12+
description: 'PR number to test (optional; checks out the PR head)'
13+
required: false
14+
type: string
15+
test_selector:
16+
description: 'Test file or exact test at a line (for example: e2e-tests/import-gateway.test.ts:120)'
17+
required: false
18+
type: string
19+
test_name_pattern:
20+
description: 'Optional Vitest test name pattern (requires test_selector)'
21+
required: false
22+
type: string
1123
pull_request_target:
1224
branches: [main, feat/**]
1325
# Skip E2E on changes that can't affect agent behavior (e.g. docs-only PRs).
@@ -31,7 +43,7 @@ on:
3143
- '.github/workflows/e2e*'
3244

3345
concurrency:
34-
group: e2e-${{ github.event.pull_request.number || github.ref }}
46+
group: e2e-${{ github.event.pull_request.number || inputs.pr_number || github.ref }}
3547
cancel-in-progress: true
3648

3749
permissions:
@@ -44,25 +56,43 @@ jobs:
4456
if: github.event_name == 'workflow_dispatch' || github.event_name == 'pull_request_target'
4557
outputs:
4658
is_authorized: ${{ steps.check.outputs.is_authorized }}
59+
base_sha: ${{ steps.check.outputs.base_sha }}
60+
checkout_ref: ${{ steps.check.outputs.checkout_ref }}
4761
steps:
4862
- name: Check authorization
4963
id: check
5064
env:
5165
EVENT_NAME: ${{ github.event_name }}
5266
AUTHORIZED_USERS: ${{ secrets.AUTHORIZED_USERS }}
67+
PR_BASE_SHA: ${{ github.event.pull_request.base.sha }}
5368
# Gate on the PR AUTHOR (whose code this is), NOT github.actor (who triggered the run).
5469
# On pull_request_target a trusted actor (e.g. a maintainer pushing to a fork PR) triggering
5570
# a run must NOT cause an untrusted author's fork code to execute with our AWS role/secrets.
5671
PR_AUTHOR: ${{ github.event.pull_request.user.login }}
72+
PR_HEAD_SHA: ${{ github.event.pull_request.head.sha }}
73+
PR_NUMBER: ${{ inputs.pr_number }}
5774
run: |
75+
set -euo pipefail
5876
if [[ "$EVENT_NAME" == "workflow_dispatch" ]]; then
77+
CHECKOUT_REF="$GITHUB_SHA"
78+
if [[ -n "$PR_NUMBER" ]]; then
79+
if [[ ! "$PR_NUMBER" =~ ^[1-9][0-9]*$ ]]; then
80+
echo "::error::pr_number must be a positive integer"
81+
exit 1
82+
fi
83+
CHECKOUT_REF="refs/pull/${PR_NUMBER}/head"
84+
fi
5985
echo "✅ Manual workflow dispatch — authorized"
6086
echo "is_authorized=true" >> "$GITHUB_OUTPUT"
87+
echo "base_sha=HEAD~1" >> "$GITHUB_OUTPUT"
88+
echo "checkout_ref=$CHECKOUT_REF" >> "$GITHUB_OUTPUT"
6189
exit 0
6290
fi
6391
if [[ ",$AUTHORIZED_USERS," == *",${PR_AUTHOR},"* ]]; then
6492
echo "✅ PR author ${PR_AUTHOR} is authorized"
6593
echo "is_authorized=true" >> "$GITHUB_OUTPUT"
94+
echo "base_sha=$PR_BASE_SHA" >> "$GITHUB_OUTPUT"
95+
echo "checkout_ref=$PR_HEAD_SHA" >> "$GITHUB_OUTPUT"
6696
else
6797
echo "⏭️ PR author ${PR_AUTHOR} is not in AUTHORIZED_USERS — skipping E2E tests."
6898
echo "ℹ️ External contributors: a maintainer can run the E2E tests via workflow_dispatch after reviewing the code."
@@ -86,12 +116,10 @@ jobs:
86116
steps:
87117
- uses: actions/checkout@v7
88118
with:
89-
ref: ${{ github.event.pull_request.head.sha }}
119+
ref: ${{ needs.authorize.outputs.checkout_ref }}
90120
fetch-depth: 0
91-
# Safe because the `authorize` job above gates on the PR AUTHOR being in AUTHORIZED_USERS:
92-
# external authors skip at the gate, so only trusted authors' code is ever checked out here.
93-
# Needed because authorized maintainers routinely open PRs from their own forks, and
94-
# checkout@v7 otherwise refuses fork-PR checkout in pull_request_target.
121+
# Automatic runs are author-gated. Manual dispatch is restricted to repository
122+
# collaborators and is the reviewed path for testing external PR code.
95123
allow-unsafe-pr-checkout: true
96124
- uses: actions/setup-node@v7
97125
with:
@@ -148,39 +176,77 @@ jobs:
148176
- name: Install CLI globally
149177
run: npm install -g "$TARBALL"
150178

151-
- name: Detect changed e2e test files
152-
id: changed
179+
- name: Resolve E2E test selection
180+
id: selection
153181
env:
154-
BASE_SHA: ${{ github.event.pull_request.base.sha || 'HEAD~1' }}
182+
BASE_SHA: ${{ needs.authorize.outputs.base_sha }}
183+
TEST_NAME_PATTERN: ${{ inputs.test_name_pattern }}
184+
TEST_SELECTOR: ${{ inputs.test_selector }}
155185
run: |
156-
# If any helper file changed, run all e2e tests
157-
HELPERS_CHANGED=$(git diff --name-only "$BASE_SHA"..HEAD -- 'e2e-tests/*.ts' \
158-
| grep -v '\.test\.ts$' | head -1)
159-
if [ -n "$HELPERS_CHANGED" ]; then
160-
GA_EXTRA=$(find e2e-tests -name '*.test.ts' \
161-
| grep -v '^e2e-tests/strands-bedrock\.test\.ts$' \
162-
| grep -v '^e2e-tests/payment-strands-bedrock\.test\.ts$' \
163-
| grep -v '^e2e-tests/harness-' \
164-
| tr '\n' ' ')
165-
HARNESS_EXTRA=$(find e2e-tests -name 'harness-*.test.ts' \
166-
| grep -v '^e2e-tests/harness-bedrock\.test\.ts$' \
167-
| tr '\n' ' ')
186+
set -euo pipefail
187+
188+
if [[ -n "$TEST_NAME_PATTERN" && -z "$TEST_SELECTOR" ]]; then
189+
echo "::error::test_name_pattern requires test_selector"
190+
exit 1
191+
fi
192+
193+
if [[ -n "$TEST_SELECTOR" ]]; then
194+
if [[ ! "$TEST_SELECTOR" =~ ^e2e-tests/([A-Za-z0-9_-]+/)*[A-Za-z0-9._-]+\.test\.ts(:[1-9][0-9]*)?$ ]]; then
195+
echo "::error::test_selector must be an e2e-tests/**/*.test.ts path, optionally followed by :line"
196+
exit 1
197+
fi
198+
199+
TEST_FILE="${TEST_SELECTOR%%:*}"
200+
if ! git ls-files --error-unmatch "$TEST_FILE" > /dev/null || [[ ! -f "$TEST_FILE" ]]; then
201+
echo "::error::test_selector does not reference a tracked test file: $TEST_FILE"
202+
exit 1
203+
fi
204+
205+
if [[ "$(basename "$TEST_FILE")" == harness-* ]]; then
206+
GA_TESTS=""
207+
HARNESS_TESTS="$TEST_SELECTOR"
208+
else
209+
GA_TESTS="$TEST_SELECTOR"
210+
HARNESS_TESTS=""
211+
fi
168212
else
169-
GA_EXTRA=$(git diff --name-only "$BASE_SHA"..HEAD -- 'e2e-tests/*.test.ts' \
170-
| grep -v '^e2e-tests/strands-bedrock\.test\.ts$' \
171-
| grep -v '^e2e-tests/payment-strands-bedrock\.test\.ts$' \
172-
| grep -v '^e2e-tests/harness-' \
173-
| tr '\n' ' ')
174-
HARNESS_EXTRA=$(git diff --name-only "$BASE_SHA"..HEAD -- 'e2e-tests/harness-*.test.ts' \
175-
| grep -v '^e2e-tests/harness-bedrock\.test\.ts$' \
176-
| tr '\n' ' ')
213+
# If any helper file changed, run all e2e tests.
214+
HELPERS_CHANGED=$(git diff --name-only "$BASE_SHA"..HEAD -- 'e2e-tests/*.ts' \
215+
| grep -v '\.test\.ts$' | head -1 || true)
216+
if [[ -n "$HELPERS_CHANGED" ]]; then
217+
GA_EXTRA=$(find e2e-tests -name '*.test.ts' \
218+
| grep -v '^e2e-tests/strands-bedrock\.test\.ts$' \
219+
| grep -v '^e2e-tests/payment-strands-bedrock\.test\.ts$' \
220+
| grep -v '^e2e-tests/harness-' \
221+
| tr '\n' ' ' || true)
222+
HARNESS_EXTRA=$(find e2e-tests -name 'harness-*.test.ts' \
223+
| grep -v '^e2e-tests/harness-bedrock\.test\.ts$' \
224+
| tr '\n' ' ' || true)
225+
else
226+
GA_EXTRA=$(git diff --name-only "$BASE_SHA"..HEAD -- 'e2e-tests/*.test.ts' \
227+
| grep -v '^e2e-tests/strands-bedrock\.test\.ts$' \
228+
| grep -v '^e2e-tests/payment-strands-bedrock\.test\.ts$' \
229+
| grep -v '^e2e-tests/harness-' \
230+
| tr '\n' ' ' || true)
231+
HARNESS_EXTRA=$(git diff --name-only "$BASE_SHA"..HEAD -- 'e2e-tests/harness-*.test.ts' \
232+
| grep -v '^e2e-tests/harness-bedrock\.test\.ts$' \
233+
| tr '\n' ' ' || true)
234+
fi
235+
GA_TESTS="e2e-tests/strands-bedrock.test.ts e2e-tests/payment-strands-bedrock.test.ts $GA_EXTRA"
236+
HARNESS_TESTS="e2e-tests/harness-bedrock.test.ts $HARNESS_EXTRA"
177237
fi
178-
echo "ga_extra=$GA_EXTRA" >> "$GITHUB_OUTPUT"
179-
echo "harness_extra=$HARNESS_EXTRA" >> "$GITHUB_OUTPUT"
180-
echo "GA extra tests: ${GA_EXTRA:-none}"
181-
echo "Harness extra tests: ${HARNESS_EXTRA:-none}"
238+
239+
[[ -n "$GA_TESTS" ]] && RUN_GA=true || RUN_GA=false
240+
[[ -n "$HARNESS_TESTS" ]] && RUN_HARNESS=true || RUN_HARNESS=false
241+
echo "ga_tests=$GA_TESTS" >> "$GITHUB_OUTPUT"
242+
echo "harness_tests=$HARNESS_TESTS" >> "$GITHUB_OUTPUT"
243+
echo "run_ga=$RUN_GA" >> "$GITHUB_OUTPUT"
244+
echo "run_harness=$RUN_HARNESS" >> "$GITHUB_OUTPUT"
245+
echo "GA tests: ${GA_TESTS:-none}"
246+
echo "Harness tests: ${HARNESS_TESTS:-none}"
182247
183248
- name: Run E2E tests (GA)
249+
if: steps.selection.outputs.run_ga == 'true'
184250
env:
185251
AWS_ACCOUNT_ID: ${{ steps.aws.outputs.account_id }}
186252
AWS_REGION: ${{ inputs.aws_region || 'us-east-1' }}
@@ -198,16 +264,24 @@ jobs:
198264
CDP_API_KEY_ID: ${{ env.E2E_CDP_API_KEY_ID }}
199265
CDP_API_KEY_SECRET: ${{ env.E2E_CDP_API_KEY_SECRET }}
200266
CDP_WALLET_SECRET: ${{ env.E2E_CDP_WALLET_SECRET }}
201-
GA_EXTRA: ${{ steps.changed.outputs.ga_extra }}
202-
# GA_EXTRA is a space-separated test-file list; left unquoted intentionally so it word-splits into vitest args.
203-
# --reporter=verbose keeps the console log; --reporter=junit adds a machine-readable
204-
# report surfaced in the GH Actions test-summary tab and uploaded as an artifact below.
205-
run:
206-
npx vitest run --project e2e --reporter=verbose --reporter=junit
207-
--outputFile.junit=test-results/e2e-ga.junit.xml e2e-tests/strands-bedrock.test.ts
208-
e2e-tests/payment-strands-bedrock.test.ts $GA_EXTRA
267+
GA_TESTS: ${{ steps.selection.outputs.ga_tests }}
268+
TEST_NAME_PATTERN: ${{ inputs.test_name_pattern }}
269+
run: |
270+
set -euo pipefail
271+
read -r -a TEST_FILES <<< "$GA_TESTS"
272+
ARGS=(npx vitest run --project e2e --reporter=verbose --reporter=junit \
273+
--outputFile.junit=test-results/e2e-ga.junit.xml)
274+
if [[ -n "$TEST_NAME_PATTERN" ]]; then
275+
ARGS+=(--testNamePattern "$TEST_NAME_PATTERN")
276+
fi
277+
ARGS+=("${TEST_FILES[@]}")
278+
printf 'Running:'
279+
printf ' %q' "${ARGS[@]}"
280+
printf '\n'
281+
"${ARGS[@]}"
209282
210283
- name: Run E2E tests (harness)
284+
if: steps.selection.outputs.run_harness == 'true'
211285
env:
212286
AWS_ACCOUNT_ID: ${{ steps.aws.outputs.account_id }}
213287
AWS_REGION: ${{ inputs.aws_region || 'us-east-1' }}
@@ -218,12 +292,21 @@ jobs:
218292
E2E_S3_ACCESS_POINT_ARN: ${{ env.E2E_S3_ACCESS_POINT_ARN }}
219293
E2E_FILESYSTEM_SUBNET_ID: ${{ env.E2E_FILESYSTEM_SUBNET_ID }}
220294
E2E_FILESYSTEM_SECURITY_GROUP_ID: ${{ env.E2E_FILESYSTEM_SECURITY_GROUP_ID }}
221-
HARNESS_EXTRA: ${{ steps.changed.outputs.harness_extra }}
222-
# HARNESS_EXTRA is a space-separated test-file list; left unquoted intentionally so it word-splits into vitest args.
223-
# Separate --outputFile from the GA step so the two vitest runs don't clobber each other's report.
224-
run:
225-
npx vitest run --project e2e --reporter=verbose --reporter=junit
226-
--outputFile.junit=test-results/e2e-harness.junit.xml e2e-tests/harness-bedrock.test.ts $HARNESS_EXTRA
295+
HARNESS_TESTS: ${{ steps.selection.outputs.harness_tests }}
296+
TEST_NAME_PATTERN: ${{ inputs.test_name_pattern }}
297+
run: |
298+
set -euo pipefail
299+
read -r -a TEST_FILES <<< "$HARNESS_TESTS"
300+
ARGS=(npx vitest run --project e2e --reporter=verbose --reporter=junit \
301+
--outputFile.junit=test-results/e2e-harness.junit.xml)
302+
if [[ -n "$TEST_NAME_PATTERN" ]]; then
303+
ARGS+=(--testNamePattern "$TEST_NAME_PATTERN")
304+
fi
305+
ARGS+=("${TEST_FILES[@]}")
306+
printf 'Running:'
307+
printf ' %q' "${ARGS[@]}"
308+
printf '\n'
309+
"${ARGS[@]}"
227310
228311
- name: Upload E2E test report
229312
if: always()

docs/testing/e2e-tests.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,18 @@ See [e2e-tests/README.md](../../e2e-tests/README.md) for full prerequisite detai
1616
npm run test:e2e # Run e2e tests
1717
```
1818

19+
## Running a Specific Test on a PR
20+
21+
Use the **E2E Tests** workflow's **Run workflow** form:
22+
23+
1. Set `pr_number` to the PR whose head commit should be tested.
24+
2. Set `test_selector` to a test file, such as `e2e-tests/import-gateway.test.ts`.
25+
3. To run one exact test, append its source line, such as `e2e-tests/import-gateway.test.ts:120`.
26+
4. Optionally set `test_name_pattern` to a Vitest test-name regular expression.
27+
28+
When `test_selector` is set, the workflow runs only that selection instead of the baseline and changed E2E files. A
29+
maintainer can use this flow for an external PR after reviewing its code.
30+
1931
## Test Organization
2032

2133
```

e2e-tests/README.md

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,9 +34,14 @@ npm run test:e2e
3434

3535
# Run a specific file
3636
npx vitest run e2e-tests/strands-bedrock.test.ts
37+
38+
# Run the test declared at a specific line
39+
npx vitest run e2e-tests/import-gateway.test.ts:120
3740
```
3841

39-
E2E tests are not run automatically on every PR. They run on a schedule and before releases.
42+
The PR E2E workflow runs baseline tests plus E2E files changed by the PR. Maintainers can also dispatch the workflow for
43+
a PR with a specific file, line, or test-name pattern; see
44+
[`docs/testing/e2e-tests.md`](../docs/testing/e2e-tests.md#running-a-specific-test-on-a-pr).
4045

4146
## Writing E2E Tests
4247

0 commit comments

Comments
 (0)