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).
3143 - ' .github/workflows/e2e*'
3244
3345concurrency :
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
3749permissions :
@@ -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()
0 commit comments