Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
14 changes: 14 additions & 0 deletions .github/actions/download-artifact/action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,14 @@ inputs:
path:
description: Destination path
required: true
run-id:
description: Workflow run to download from; defaults to the current run
required: false
default: ''
github-token:
description: Token with actions:read, needed when run-id is another run
required: false
default: ''

outputs:
download-path:
Expand All @@ -25,6 +33,8 @@ runs:
with:
name: ${{ inputs.name }}
path: ${{ inputs.path }}
run-id: ${{ inputs.run-id || github.run_id }}
github-token: ${{ inputs.github-token }}

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

- name: Wait before attempt 3
if: ${{ always() && steps.download-2.outcome == 'failure' }}
Expand All @@ -57,3 +69,5 @@ runs:
with:
name: ${{ inputs.name }}
path: ${{ inputs.path }}
run-id: ${{ inputs.run-id || github.run_id }}
github-token: ${{ inputs.github-token }}
17 changes: 12 additions & 5 deletions .github/actions/maestro-ios/action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -26,18 +26,23 @@ inputs:
required: false
default: ''
description: Maestro device OS name, such as iOS-26-2
argent-label:
required: true
description: Label the calling workflow is registered under on Argent Cloud; the job logs in as it

runs:
using: composite
steps:
- name: Installing Maestro
shell: bash
run: export MAESTRO_VERSION=2.6.1; curl -Ls "https://get.maestro.mobile.dev" | bash
- name: Installing Maestro dependencies
shell: bash
run: |
brew tap facebook/fb
brew install facebook/fb/idb-companion
- name: Install sim-remote
uses: software-mansion-labs/argent-cloud-actions/install@v2
- name: Acquire remote simulator
# Logs in with the job's OIDC id token; released in the post step.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This seems to don't work for forked PRs right? We also can't invoke this with pull_request_target as we won't be able to test the code from the user PR then.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Like I mentioned in the discussion on the contributors discord, we should be able to checkout the PR head even from the pull_request_target trigger, by passing the ref explicitly to the checkout action, while still running the workflow itself from the PR target.
...but then the build from the fork could access protected scopes (secrets, the OIDC token we create here, or even other OIDC tokens you may have set up), the same goes for the js helper scripts from the E2E flow.
I feel like it should be fine that way, since the only step that has defined access to anything would be the test step (so build cannot exfil any secrets/the token), but you could still try to do something in the maestro tests themselves to get access to Argent Cloud.
Maybe having a maintainer's manual approval for the workflow would be a good way to go about this, so that there's a pair of eyes that can verify there's no attempt to steal the token before the workflow runs?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe having a maintainer's manual approval for the workflow would be a good way to go about this, so that there's a pair of eyes that can verify there's no attempt to steal the token before the workflow runs?

I think that the best would be to have something like a @react-native-bot e2e-tests comment that triggers a workflow, running off of target_pull_request which runs the tests. That workflow can then pull the .apk/.ipa from the artifacts of the PR and execute it isnide the remote simulator and report the results.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Made the changes as suggested.

uses: software-mansion-labs/argent-cloud-actions/acquire@v2
with:
label: ${{ inputs.argent-label }}
- name: Set up JDK 17
uses: actions/setup-java@v5
with:
Expand All @@ -57,6 +62,8 @@ runs:
cd ${{ inputs.working-directory }}
yarn start &
sleep 5 # to give metro time to load
- name: Install Maestro shims
uses: software-mansion-labs/argent-cloud-actions/maestro-shims@v2
- name: Run tests
id: run-tests
shell: bash
Expand Down
20 changes: 20 additions & 0 deletions .github/workflow-scripts/maestro-ios.js
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,24 @@ function installAppOnSimulator(appPath, udid) {
childProcess.execSync(`xcrun simctl install "${udid}" "${appPath}"`);
}

// A remote simulator's localhost is the remote host, so Metro needs a tunnel.
function exposeMetroToSimulator(udid) {
const simRemote = process.env.SIM_REMOTE_BIN;
if (!simRemote) {
return;
}

console.log('Exposing local Metro port 8081 to the remote simulator');
childProcess.execSync(`"${simRemote}" reverse start "${udid}" 8081`, {
stdio: 'inherit',
});
}

function bringSimulatorInForeground() {
// Nothing to foreground on a remote simulator.
if (process.env.SIM_REMOTE_BIN) {
return;
}
console.log('Bringing simulator in foreground');
childProcess.execSync('open -a simulator');
}
Expand Down Expand Up @@ -266,6 +283,9 @@ async function main(args = process.argv.slice(2)) {
launchSimulator(simulator);
installAppOnSimulator(appPath, simulator.udid);
bringSimulatorInForeground();
if (isDebug) {
exposeMetroToSimulator(simulator.udid);
}
await launchAppOnSimulator(appId, simulator.udid, isDebug);
await executeFlows(appId, simulator.udid, maestroFlow, jsengine);
console.log('Test finished');
Expand Down
119 changes: 119 additions & 0 deletions .github/workflows/e2e-ios-on-demand.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
name: E2E iOS on demand

# Runs the iOS Maestro E2E suites against a pull request when a maintainer
# comments `@react-native-bot e2e-tests` on it. The apps come from the
# artifacts a run of the pull request's head already built, the Maestro flows
# from the pull request, and the workflows and scripts from the default branch.

on:
issue_comment:
types: [created]

permissions:
contents: read

concurrency:
group: e2e-ios-on-demand-${{ github.event.issue.number }}
cancel-in-progress: true

jobs:
request:
if: >-
github.event.issue.pull_request != null &&
startsWith(github.event.comment.body, '@react-native-bot e2e-tests') &&
contains(fromJson('["OWNER","MEMBER","COLLABORATOR"]'), github.event.comment.author_association)
runs-on: ubuntu-latest
permissions:
contents: read
actions: read
pull-requests: write
outputs:
head-sha: ${{ steps.find.outputs.head-sha }}
run-id: ${{ steps.find.outputs.run-id }}
steps:
- name: Find the pull request's build
id: find
env:
GH_TOKEN: ${{ github.token }}
GH_REPO: ${{ github.repository }}
PR: ${{ github.event.issue.number }}
run: |
set -euo pipefail
sha=$(gh api "repos/$GH_REPO/pulls/$PR" --jq .head.sha)
needed="RNTesterApp-NewArch-Debug RNTesterApp-NewArch-Release RNTestProject-Debug RNTestProject-Release react-native-package"
run_id=
for id in $(gh api "repos/$GH_REPO/actions/runs?head_sha=$sha&per_page=30" --jq '.workflow_runs[].id'); do
have=$(gh api "repos/$GH_REPO/actions/runs/$id/artifacts?per_page=100" --jq '.artifacts[].name')
missing=
for name in $needed; do
grep -qx "$name" <<<"$have" || missing="$missing $name"
done
if [ -z "$missing" ]; then
run_id=$id
break
fi
done
if [ -z "$run_id" ]; then
echo "::error::no run of $sha has built all of: $needed"
exit 1
fi
echo "head-sha=$sha" >> "$GITHUB_OUTPUT"
echo "run-id=$run_id" >> "$GITHUB_OUTPUT"
- name: Acknowledge the request
env:
GH_TOKEN: ${{ github.token }}
GH_REPO: ${{ github.repository }}
COMMENT: ${{ github.event.comment.id }}
run: gh api -X POST "repos/$GH_REPO/issues/comments/$COMMENT/reactions" -f content=rocket > /dev/null

rntester:
needs: request
uses: ./.github/workflows/e2e-ios-rntester.yml
permissions:
contents: read
actions: read
id-token: write
with:
argent-label: facebook.e2e-ios-on-demand
artifact-run-id: ${{ needs.request.outputs.run-id }}
flows-ref: ${{ needs.request.outputs.head-sha }}
fail-on-error: true

templateapp:
needs: request
uses: ./.github/workflows/e2e-ios-templateapp.yml
permissions:
contents: read
actions: read
id-token: write
with:
argent-label: facebook.e2e-ios-on-demand
artifact-run-id: ${{ needs.request.outputs.run-id }}
flows-ref: ${{ needs.request.outputs.head-sha }}
fail-on-error: true

report:
needs: [request, rntester, templateapp]
if: always()
runs-on: ubuntu-latest
permissions:
pull-requests: write
steps:
- name: Comment the results
env:
GH_TOKEN: ${{ secrets.REACT_NATIVE_BOT_GITHUB_TOKEN || github.token }}
GH_REPO: ${{ github.repository }}
PR: ${{ github.event.issue.number }}
REQUEST: ${{ needs.request.result }}
HEAD_SHA: ${{ needs.request.outputs.head-sha }}
RNTESTER: ${{ needs.rntester.outputs.status }}
TEMPLATEAPP: ${{ needs.templateapp.outputs.status }}
RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}
run: |
set -euo pipefail
if [ "$REQUEST" != "success" ]; then
body="iOS E2E: no run of the pull request's head has built the apps yet, see [the request]($RUN_URL)."
else
body="iOS E2E on ${HEAD_SHA:0:7} ([run]($RUN_URL)): RNTester **${RNTESTER:-skipped}**, template app **${TEMPLATEAPP:-skipped}**."
fi
gh api -X POST "repos/$GH_REPO/issues/$PR/comments" -f body="$body" > /dev/null
45 changes: 40 additions & 5 deletions .github/workflows/e2e-ios-rntester.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,21 +2,38 @@ name: E2E iOS RNTester

permissions:
contents: read
actions: read
id-token: write

on:
workflow_call:
inputs:
fail-on-error:
type: boolean
default: false
artifact-run-id:
description: Run to take the app artifacts from; defaults to this run
type: string
default: ''
flows-ref:
description: Ref to take the Maestro flows from; defaults to the checked-out tree
type: string
default: ''
argent-label:
description: Label the calling workflow is registered under on Argent Cloud
type: string
required: true
outputs:
status:
description: 'The result of the E2E tests (success or failure)'
value: ${{ jobs.report.outputs.status }}

jobs:
test:
runs-on: macos-26-large
# A pull_request run has no id token to log into the remote simulators
# with; the suite runs on request from e2e-ios-on-demand.yml instead.
if: github.event_name != 'pull_request'
runs-on: ubuntu-latest
outputs:
status: ${{ steps.report-status.outputs.status }}
strategy:
Expand All @@ -29,23 +46,38 @@ jobs:
- name: Setup Node.js
uses: ./.github/actions/setup-node
- name: Download App
uses: actions/download-artifact@v7
uses: ./.github/actions/download-artifact
with:
name: RNTesterApp-NewArch-${{ matrix.flavor }}
path: /tmp/RNTesterBuild/RNTester.app
run-id: ${{ inputs.artifact-run-id }}
github-token: ${{ github.token }}
- name: Checkout the Maestro flows under test
if: ${{ inputs.flows-ref != '' }}
uses: actions/checkout@v6
with:
ref: ${{ inputs.flows-ref }}
path: .flows-under-test
sparse-checkout: packages/rn-tester/.maestro
- name: Use the Maestro flows under test
if: ${{ inputs.flows-ref != '' }}
run: |
rm -rf packages/rn-tester/.maestro
mv .flows-under-test/packages/rn-tester/.maestro packages/rn-tester/.maestro
- name: Check downloaded folder content
run: ls -lR /tmp/RNTesterBuild
- name: Run E2E Tests
id: run-tests
continue-on-error: true
uses: ./.github/actions/maestro-ios
with:
argent-label: ${{ inputs.argent-label }}
app-path: '/tmp/RNTesterBuild/RNTester.app'
app-id: com.meta.RNTester.localDevelopment
maestro-flow: ./packages/rn-tester/.maestro/
flavor: ${{ matrix.flavor }}
device-model: iPhone-17-Pro
device-os: iOS-26-2
device-model: iPhone-18-Pro
device-os: iOS-27-0
- name: Report status
id: report-status
if: ${{ always() && steps.run-tests.outcome == 'failure' }}
Expand All @@ -59,8 +91,11 @@ jobs:
status: ${{ steps.check.outputs.status }}
steps:
- id: check
# A skipped test job is not a failure: the tests do not run on a
# pull_request run.
run: |
if [[ "${{ needs.test.result }}" != "success" ||
test="${{ needs.test.result }}"
if [[ ("$test" != "success" && "$test" != "skipped") ||
"${{ needs.test.outputs.status }}" == "failure" ]]; then
echo "status=failure" >> $GITHUB_OUTPUT
else
Expand Down
Loading
Loading