Skip to content

Commit ec00572

Browse files
committed
Run the iOS E2E suites on request from a maintainer's comment
A pull_request run from a fork gets no id token, so the E2E test jobs no longer run on that event. Instead, a maintainer comments `@react-native-bot e2e-tests` on the pull request and e2e-ios-on-demand.yml runs the suites from the default branch: the apps come from the artifacts a run of the pull request's head already built, the Maestro flows from the pull request, the workflows and scripts from the default branch. The two reusable E2E workflows take the run to download from and the ref to take the flows from as inputs, and the download-artifact action can read another run. The login label is derived from the calling workflow as `<owner>.<workflow file>`, which is how each repository's workflows are registered on the remote simulator service.
1 parent 75c54e7 commit ec00572

6 files changed

Lines changed: 228 additions & 5 deletions

File tree

‎.github/actions/download-artifact/action.yml‎

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,14 @@ inputs:
88
path:
99
description: Destination path
1010
required: true
11+
run-id:
12+
description: Workflow run to download from; defaults to the current run
13+
required: false
14+
default: ''
15+
github-token:
16+
description: Token with actions:read, needed when run-id is another run
17+
required: false
18+
default: ''
1119

1220
outputs:
1321
download-path:
@@ -25,6 +33,8 @@ runs:
2533
with:
2634
name: ${{ inputs.name }}
2735
path: ${{ inputs.path }}
36+
run-id: ${{ inputs.run-id || github.run_id }}
37+
github-token: ${{ inputs.github-token }}
2838

2939
- name: Wait before attempt 2
3040
if: ${{ always() && steps.download-1.outcome == 'failure' }}
@@ -41,6 +51,8 @@ runs:
4151
with:
4252
name: ${{ inputs.name }}
4353
path: ${{ inputs.path }}
54+
run-id: ${{ inputs.run-id || github.run_id }}
55+
github-token: ${{ inputs.github-token }}
4456

4557
- name: Wait before attempt 3
4658
if: ${{ always() && steps.download-2.outcome == 'failure' }}
@@ -57,3 +69,5 @@ runs:
5769
with:
5870
name: ${{ inputs.name }}
5971
path: ${{ inputs.path }}
72+
run-id: ${{ inputs.run-id || github.run_id }}
73+
github-token: ${{ inputs.github-token }}

‎.github/actions/maestro-ios/action.yml‎

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,9 @@ inputs:
2626
required: false
2727
default: ''
2828
description: Maestro device OS name, such as iOS-26-2
29+
argent-label:
30+
required: true
31+
description: Label the calling workflow is registered under on Argent Cloud; the job logs in as it
2932

3033
runs:
3134
using: composite
@@ -39,7 +42,7 @@ runs:
3942
# Logs in with the job's OIDC id token; released in the post step.
4043
uses: software-mansion-labs/argent-cloud-actions/acquire@v2
4144
with:
42-
label: react-native-e2e
45+
label: ${{ inputs.argent-label }}
4346
- name: Set up JDK 17
4447
uses: actions/setup-java@v5
4548
with:
Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
name: E2E iOS on demand
2+
3+
# Runs the iOS Maestro E2E suites against a pull request when a maintainer
4+
# comments `@react-native-bot e2e-tests` on it. The apps come from the
5+
# artifacts a run of the pull request's head already built, the Maestro flows
6+
# from the pull request, and the workflows and scripts from the default branch.
7+
8+
on:
9+
issue_comment:
10+
types: [created]
11+
12+
permissions:
13+
contents: read
14+
15+
# Only a trigger comment shares the pull request's group, so a newer request
16+
# supersedes a running one and any other comment cancels nothing.
17+
concurrency:
18+
group: e2e-ios-on-demand-${{ github.event.issue.number }}-${{ startsWith(github.event.comment.body, '@react-native-bot e2e-tests') && 'run' || github.event.comment.id }}
19+
cancel-in-progress: true
20+
21+
jobs:
22+
request:
23+
if: >-
24+
github.event.issue.pull_request != null &&
25+
startsWith(github.event.comment.body, '@react-native-bot e2e-tests') &&
26+
contains(fromJson('["OWNER","MEMBER","COLLABORATOR"]'), github.event.comment.author_association)
27+
runs-on: ubuntu-latest
28+
permissions:
29+
contents: read
30+
actions: read
31+
pull-requests: write
32+
outputs:
33+
head-sha: ${{ steps.find.outputs.head-sha }}
34+
run-id: ${{ steps.find.outputs.run-id }}
35+
steps:
36+
- name: Find the pull request's build
37+
id: find
38+
env:
39+
GH_TOKEN: ${{ github.token }}
40+
GH_REPO: ${{ github.repository }}
41+
PR: ${{ github.event.issue.number }}
42+
run: |
43+
set -euo pipefail
44+
sha=$(gh api "repos/$GH_REPO/pulls/$PR" --jq .head.sha)
45+
needed="RNTesterApp-NewArch-Debug RNTesterApp-NewArch-Release RNTestProject-Debug RNTestProject-Release react-native-package"
46+
run_id=
47+
for id in $(gh api "repos/$GH_REPO/actions/runs?head_sha=$sha&per_page=30" --jq '.workflow_runs[].id'); do
48+
have=$(gh api "repos/$GH_REPO/actions/runs/$id/artifacts?per_page=100" --jq '.artifacts[].name')
49+
missing=
50+
for name in $needed; do
51+
grep -qx "$name" <<<"$have" || missing="$missing $name"
52+
done
53+
if [ -z "$missing" ]; then
54+
run_id=$id
55+
break
56+
fi
57+
done
58+
if [ -z "$run_id" ]; then
59+
echo "::error::no run of $sha has built all of: $needed"
60+
exit 1
61+
fi
62+
echo "head-sha=$sha" >> "$GITHUB_OUTPUT"
63+
echo "run-id=$run_id" >> "$GITHUB_OUTPUT"
64+
- name: Acknowledge the request
65+
env:
66+
GH_TOKEN: ${{ github.token }}
67+
GH_REPO: ${{ github.repository }}
68+
COMMENT: ${{ github.event.comment.id }}
69+
run: gh api -X POST "repos/$GH_REPO/issues/comments/$COMMENT/reactions" -f content=rocket > /dev/null
70+
71+
rntester:
72+
needs: request
73+
uses: ./.github/workflows/e2e-ios-rntester.yml
74+
permissions:
75+
contents: read
76+
actions: read
77+
id-token: write
78+
with:
79+
argent-label: facebook.e2e-ios-on-demand
80+
artifact-run-id: ${{ needs.request.outputs.run-id }}
81+
flows-ref: ${{ needs.request.outputs.head-sha }}
82+
fail-on-error: true
83+
84+
templateapp:
85+
needs: request
86+
uses: ./.github/workflows/e2e-ios-templateapp.yml
87+
permissions:
88+
contents: read
89+
actions: read
90+
id-token: write
91+
with:
92+
argent-label: facebook.e2e-ios-on-demand
93+
artifact-run-id: ${{ needs.request.outputs.run-id }}
94+
flows-ref: ${{ needs.request.outputs.head-sha }}
95+
fail-on-error: true
96+
97+
report:
98+
needs: [request, rntester, templateapp]
99+
if: always() && needs.request.result != 'skipped'
100+
runs-on: ubuntu-latest
101+
permissions:
102+
pull-requests: write
103+
steps:
104+
- name: Comment the results
105+
env:
106+
GH_TOKEN: ${{ secrets.REACT_NATIVE_BOT_GITHUB_TOKEN || github.token }}
107+
GH_REPO: ${{ github.repository }}
108+
PR: ${{ github.event.issue.number }}
109+
REQUEST: ${{ needs.request.result }}
110+
HEAD_SHA: ${{ needs.request.outputs.head-sha }}
111+
RNTESTER: ${{ needs.rntester.outputs.status }}
112+
TEMPLATEAPP: ${{ needs.templateapp.outputs.status }}
113+
RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}
114+
run: |
115+
set -euo pipefail
116+
if [ "$REQUEST" != "success" ]; then
117+
body="iOS E2E: no run of the pull request's head has built the apps yet, see [the request]($RUN_URL)."
118+
else
119+
body="iOS E2E on ${HEAD_SHA:0:7} ([run]($RUN_URL)): RNTester **${RNTESTER:-skipped}**, template app **${TEMPLATEAPP:-skipped}**."
120+
fi
121+
gh api -X POST "repos/$GH_REPO/issues/$PR/comments" -f body="$body" > /dev/null

‎.github/workflows/e2e-ios-rntester.yml‎

Lines changed: 36 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ name: E2E iOS RNTester
22

33
permissions:
44
contents: read
5+
actions: read
56
id-token: write
67

78
on:
@@ -10,13 +11,28 @@ on:
1011
fail-on-error:
1112
type: boolean
1213
default: false
14+
artifact-run-id:
15+
description: Run to take the app artifacts from; defaults to this run
16+
type: string
17+
default: ''
18+
flows-ref:
19+
description: Ref to take the Maestro flows from; defaults to the checked-out tree
20+
type: string
21+
default: ''
22+
argent-label:
23+
description: Label the calling workflow is registered under on Argent Cloud
24+
type: string
25+
required: true
1326
outputs:
1427
status:
1528
description: 'The result of the E2E tests (success or failure)'
1629
value: ${{ jobs.report.outputs.status }}
1730

1831
jobs:
1932
test:
33+
# A pull_request run has no id token to log into the remote simulators
34+
# with; the suite runs on request from e2e-ios-on-demand.yml instead.
35+
if: github.event_name != 'pull_request'
2036
runs-on: ubuntu-latest
2137
outputs:
2238
status: ${{ steps.report-status.outputs.status }}
@@ -30,17 +46,32 @@ jobs:
3046
- name: Setup Node.js
3147
uses: ./.github/actions/setup-node
3248
- name: Download App
33-
uses: actions/download-artifact@v7
49+
uses: ./.github/actions/download-artifact
3450
with:
3551
name: RNTesterApp-NewArch-${{ matrix.flavor }}
3652
path: /tmp/RNTesterBuild/RNTester.app
53+
run-id: ${{ inputs.artifact-run-id }}
54+
github-token: ${{ github.token }}
55+
- name: Checkout the Maestro flows under test
56+
if: ${{ inputs.flows-ref != '' }}
57+
uses: actions/checkout@v6
58+
with:
59+
ref: ${{ inputs.flows-ref }}
60+
path: .flows-under-test
61+
sparse-checkout: packages/rn-tester/.maestro
62+
- name: Use the Maestro flows under test
63+
if: ${{ inputs.flows-ref != '' }}
64+
run: |
65+
rm -rf packages/rn-tester/.maestro
66+
mv .flows-under-test/packages/rn-tester/.maestro packages/rn-tester/.maestro
3767
- name: Check downloaded folder content
3868
run: ls -lR /tmp/RNTesterBuild
3969
- name: Run E2E Tests
4070
id: run-tests
4171
continue-on-error: true
4272
uses: ./.github/actions/maestro-ios
4373
with:
74+
argent-label: ${{ inputs.argent-label }}
4475
app-path: '/tmp/RNTesterBuild/RNTester.app'
4576
app-id: com.meta.RNTester.localDevelopment
4677
maestro-flow: ./packages/rn-tester/.maestro/
@@ -60,8 +91,11 @@ jobs:
6091
status: ${{ steps.check.outputs.status }}
6192
steps:
6293
- id: check
94+
# A skipped test job is not a failure: the tests do not run on a
95+
# pull_request run.
6396
run: |
64-
if [[ "${{ needs.test.result }}" != "success" ||
97+
test="${{ needs.test.result }}"
98+
if [[ ("$test" != "success" && "$test" != "skipped") ||
6599
"${{ needs.test.outputs.status }}" == "failure" ]]; then
66100
echo "status=failure" >> $GITHUB_OUTPUT
67101
else

‎.github/workflows/e2e-ios-templateapp.yml‎

Lines changed: 42 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ name: E2E iOS Template App
22

33
permissions:
44
contents: read
5+
actions: read
56
id-token: write
67

78
on:
@@ -10,6 +11,18 @@ on:
1011
fail-on-error:
1112
type: boolean
1213
default: false
14+
artifact-run-id:
15+
description: Run to take the app and package artifacts from; defaults to this run, which then builds them
16+
type: string
17+
default: ''
18+
flows-ref:
19+
description: Ref to take the Maestro flows from; defaults to the checked-out tree
20+
type: string
21+
default: ''
22+
argent-label:
23+
description: Label the calling workflow is registered under on Argent Cloud
24+
type: string
25+
required: true
1326
outputs:
1427
status:
1528
description: 'The result of the E2E tests (success or failure)'
@@ -18,6 +31,7 @@ on:
1831
jobs:
1932
# Building needs Xcode; the tests drive a remote simulator from Linux.
2033
build:
34+
if: inputs.artifact-run-id == ''
2135
runs-on: macos-26-large
2236
strategy:
2337
fail-fast: false
@@ -101,6 +115,11 @@ jobs:
101115

102116
test:
103117
needs: build
118+
# A pull_request run has no id token to log into the remote simulators
119+
# with; the suite runs on request from e2e-ios-on-demand.yml instead.
120+
if: >-
121+
always() && github.event_name != 'pull_request' &&
122+
(needs.build.result == 'success' || needs.build.result == 'skipped')
104123
runs-on: ubuntu-latest
105124
outputs:
106125
status: ${{ steps.report-status.outputs.status }}
@@ -120,6 +139,8 @@ jobs:
120139
with:
121140
name: RNTestProject-${{ matrix.flavor }}
122141
path: /tmp/RNTestProjectBuild/RNTestProject.app
142+
run-id: ${{ inputs.artifact-run-id }}
143+
github-token: ${{ github.token }}
123144
- name: Check downloaded folder content
124145
run: ls -l /tmp/RNTestProjectBuild/RNTestProject.app
125146
- name: Download React Native Package
@@ -128,6 +149,20 @@ jobs:
128149
with:
129150
name: react-native-package
130151
path: /tmp/react-native-tmp
152+
run-id: ${{ inputs.artifact-run-id }}
153+
github-token: ${{ github.token }}
154+
- name: Checkout the Maestro flows under test
155+
if: ${{ inputs.flows-ref != '' }}
156+
uses: actions/checkout@v6
157+
with:
158+
ref: ${{ inputs.flows-ref }}
159+
path: .flows-under-test
160+
sparse-checkout: scripts/e2e/.maestro
161+
- name: Use the Maestro flows under test
162+
if: ${{ inputs.flows-ref != '' }}
163+
run: |
164+
rm -rf scripts/e2e/.maestro
165+
mv .flows-under-test/scripts/e2e/.maestro scripts/e2e/.maestro
131166
- name: Configure git
132167
if: ${{ matrix.flavor == 'Debug' }}
133168
shell: bash
@@ -154,6 +189,7 @@ jobs:
154189
continue-on-error: true
155190
uses: ./.github/actions/maestro-ios
156191
with:
192+
argent-label: ${{ inputs.argent-label }}
157193
app-path: '/tmp/RNTestProjectBuild/RNTestProject.app'
158194
app-id: org.reactjs.native.example.RNTestProject
159195
maestro-flow: ./scripts/e2e/.maestro/
@@ -172,9 +208,13 @@ jobs:
172208
status: ${{ steps.check.outputs.status }}
173209
steps:
174210
- id: check
211+
# Skipped jobs are not failures: the build is skipped when the apps
212+
# come from another run, the tests on a pull_request run.
175213
run: |
176-
if [[ "${{ needs.build.result }}" != "success" ||
177-
"${{ needs.test.result }}" != "success" ||
214+
build="${{ needs.build.result }}"
215+
test="${{ needs.test.result }}"
216+
if [[ ("$build" != "success" && "$build" != "skipped") ||
217+
("$test" != "success" && "$test" != "skipped") ||
178218
"${{ needs.test.outputs.status }}" == "failure" ]]; then
179219
echo "status=failure" >> $GITHUB_OUTPUT
180220
else

0 commit comments

Comments
 (0)