diff --git a/.github/actions/maestro-ios/action.yml b/.github/actions/maestro-ios/action.yml index 6ebc7327fb0b..782005fb4899 100644 --- a/.github/actions/maestro-ios/action.yml +++ b/.github/actions/maestro-ios/action.yml @@ -1,5 +1,5 @@ name: Maestro E2E iOS -description: Runs E2E Tests on iOS using Maestro +description: Runs E2E Tests on iOS using Maestro against a remote simulator via sim-remote inputs: app-path: required: true @@ -27,9 +27,33 @@ runs: run: export MAESTRO_VERSION=2.6.1; curl -Ls "https://get.maestro.mobile.dev" | bash - name: Installing Maestro dependencies shell: bash + # libplist-utils provides plistutil, which backs the sim-remote plutil + # shim (Maestro pipes `simctl listapps` output through `plutil`). run: | - brew tap facebook/fb - brew install facebook/fb/idb-companion + sudo apt-get update + sudo apt-get install -y libplist-utils + - name: Install sim-remote + shell: bash + run: | + case "$(uname -m)" in + x86_64) TARGET="x86_64-unknown-linux-gnu" ;; + aarch64 | arm64) TARGET="aarch64-unknown-linux-gnu" ;; + *) echo "Unsupported architecture: $(uname -m)"; exit 1 ;; + esac + mkdir -p "$HOME/.local/bin" + curl -fL --retry 3 -o "$HOME/.local/bin/sim-remote" \ + "https://github.com/software-mansion/sim-remote-releases/releases/download/softu/sim-remote-${TARGET}" + chmod +x "$HOME/.local/bin/sim-remote" + echo "$HOME/.local/bin" >> "$GITHUB_PATH" + - name: Acquire remote simulator + shell: bash + # Credentials come from the calling job's env: + # SIM_ROUTER_URL, SIM_ROUTER_USERNAME, SIM_ROUTER_API_KEY. + run: | + # An unset SIM_ROUTER_URL secret arrives as an empty env var, which + # would override the router URL baked into the binary — drop it. + [ -n "${SIM_ROUTER_URL:-}" ] || unset SIM_ROUTER_URL + sim-remote login --timeout 900 - name: Set up JDK 17 uses: actions/setup-java@v5 with: @@ -53,6 +77,10 @@ runs: id: run-tests shell: bash run: | + # Put the Maestro shims (xcrun, xcodebuild, plutil, ...) on PATH so + # stock Maestro drives the remote simulator through sim-remote. + eval "$(sim-remote install-shims)" + # Avoid exit from the job if one of the command returns an error. # Maestro can fail in case of flakyness, we have some retry logic. set +e @@ -64,6 +92,10 @@ runs: "Hermes" \ "${{ inputs.flavor }}" \ "${{ inputs.working-directory }}" + - name: Release remote simulator + if: ${{ always() }} + shell: bash + run: sim-remote logout || true - name: Store video record if: always() uses: actions/upload-artifact@v6 diff --git a/.github/workflow-scripts/maestro-ios.js b/.github/workflow-scripts/maestro-ios.js index 6b7a536f72d2..2859fd90988c 100644 --- a/.github/workflow-scripts/maestro-ios.js +++ b/.github/workflow-scripts/maestro-ios.js @@ -32,10 +32,12 @@ function findAvailableSimulator() { const devices = Object.values(JSON.parse(String(output)).devices) .flat() .reverse(); - const simulator = devices.find(device => /^iPhone .* Pro$/.test(device.name)); + const simulator = + devices.find(device => /^iPhone .* Pro$/.test(device.name)) ?? + devices.find(device => /^iPhone /.test(device.name)); if (simulator == null) { - throw new Error('Unable to find an available iPhone Pro simulator'); + throw new Error('Unable to find an available iPhone simulator'); } return simulator; @@ -54,9 +56,26 @@ function launchSimulator(simulator) { } } -function installAppOnSimulator(appPath) { +function installAppOnSimulator(appPath, udid) { console.log(`Installing app at path ${appPath}`); - childProcess.execSync(`xcrun simctl install booted "${appPath}"`); + childProcess.execSync(`xcrun simctl install "${udid}" "${appPath}"`); +} + +// When running against a remote simulator through the sim-remote Maestro +// shims (`sim-remote install-shims` exports SIM_REMOTE_BIN), localhost inside +// the simulator is the remote host, not this machine — so the app cannot +// reach the local Metro server on its own. A reverse tunnel exposes the local +// Metro port to the simulator via localhost. +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() { @@ -80,13 +99,13 @@ async function launchAppOnSimulator(appId, udid, isDebug) { } } -function startVideoRecording(jsengine, currentAttempt) { +function startVideoRecording(udid, currentAttempt) { console.log( `Start video record using pid: video_record_${currentAttempt}.pid`, ); const recordingArgs = - `simctl io booted recordVideo --force video_record_${currentAttempt}.mov`.split( + `simctl io ${udid} recordVideo --force video_record_${currentAttempt}.mov`.split( ' ', ); const recordingProcess = childProcess.spawn('xcrun', recordingArgs, { @@ -109,7 +128,7 @@ function stopVideoRecording(recordingProcess) { } function executeFlowWithRetries(appId, udid, flow, jsengine, currentAttempt) { - const recProcess = startVideoRecording(jsengine, currentAttempt); + const recProcess = startVideoRecording(udid, currentAttempt); try { const timeout = 1000 * 60 * 10; // 10 minutes const command = `$HOME/.maestro/bin/maestro --udid="${udid}" test "${flow}" --format junit -e APP_ID="${appId}"`; @@ -176,8 +195,11 @@ async function main(args = process.argv.slice(2)) { const simulator = findAvailableSimulator(); launchSimulator(simulator); - installAppOnSimulator(appPath); + installAppOnSimulator(appPath, simulator.udid); bringSimulatorInForeground(); + if (isDebug) { + exposeMetroToSimulator(simulator.udid); + } await launchAppOnSimulator(appId, simulator.udid, isDebug); executeFlows(appId, simulator.udid, maestroFlow, jsengine); console.log('Test finished'); diff --git a/.github/workflows/analyze-pr.yml b/.github/workflows/analyze-pr.yml index 9d8b6c56b094..a03e0c49a858 100644 --- a/.github/workflows/analyze-pr.yml +++ b/.github/workflows/analyze-pr.yml @@ -1,8 +1,7 @@ name: Analyze Pull Request on: - pull_request_target: - types: [opened, edited, reopened, synchronize] + workflow_dispatch: permissions: pull-requests: write diff --git a/.github/workflows/api-changes.yml b/.github/workflows/api-changes.yml index e714a29fc6b6..ec49ae87268c 100644 --- a/.github/workflows/api-changes.yml +++ b/.github/workflows/api-changes.yml @@ -1,8 +1,7 @@ name: Analyze API Changes on: - pull_request_target: - types: [opened, edited, reopened, synchronize] + workflow_dispatch: permissions: pull-requests: write diff --git a/.github/workflows/autorebase.yml b/.github/workflows/autorebase.yml index aee759bf78b0..5df1ff7cd52a 100644 --- a/.github/workflows/autorebase.yml +++ b/.github/workflows/autorebase.yml @@ -3,8 +3,8 @@ name: Automatic Rebase # containing the text "/rebase". It uses the cirrus-actions/rebase action. # See https://github.com/cirrus-actions/rebase on: - issue_comment: - types: [created] + workflow_dispatch: + permissions: contents: read jobs: diff --git a/.github/workflows/cache-reaper.yml b/.github/workflows/cache-reaper.yml index 85ce2a8c88c1..d20d5369990d 100644 --- a/.github/workflows/cache-reaper.yml +++ b/.github/workflows/cache-reaper.yml @@ -2,9 +2,6 @@ name: Keep Github Actions Cache < 10GB on: workflow_dispatch: - schedule: - # Run every 2hrs during weekdays - - cron: '0 0/2 * * 1-5' jobs: cache-cleaner: diff --git a/.github/workflows/check-for-reproducer.yml b/.github/workflows/check-for-reproducer.yml index 72b122a905fb..3d0a8a5b699d 100644 --- a/.github/workflows/check-for-reproducer.yml +++ b/.github/workflows/check-for-reproducer.yml @@ -1,8 +1,7 @@ name: Check for reproducer # This workflow is triggered when issue is created or edited. on: - issues: - types: [opened, edited] + workflow_dispatch: jobs: check-for-reproducer: diff --git a/.github/workflows/close-pr.yml b/.github/workflows/close-pr.yml index 6af1b037fcf3..b1809659add8 100644 --- a/.github/workflows/close-pr.yml +++ b/.github/workflows/close-pr.yml @@ -1,5 +1,6 @@ name: Label closed PR as merged and leave a comment -on: push +on: + workflow_dispatch: permissions: contents: read diff --git a/.github/workflows/e2e-ios-rntester.yml b/.github/workflows/e2e-ios-rntester.yml index f7ec8894d3d9..d4b3c43c1696 100644 --- a/.github/workflows/e2e-ios-rntester.yml +++ b/.github/workflows/e2e-ios-rntester.yml @@ -16,7 +16,11 @@ on: jobs: test: - runs-on: macos-26-large + runs-on: ubuntu-latest + env: + SIM_ROUTER_URL: ${{ secrets.SIM_ROUTER_URL }} + SIM_ROUTER_USERNAME: ${{ vars.SIM_ROUTER_USERNAME }} + SIM_ROUTER_API_KEY: ${{ secrets.SIM_ROUTER_API_KEY }} outputs: status: ${{ steps.report-status.outputs.status }} strategy: diff --git a/.github/workflows/e2e-ios-templateapp.yml b/.github/workflows/e2e-ios-templateapp.yml index 20e5a9c5dff3..cc092e9acfb5 100644 --- a/.github/workflows/e2e-ios-templateapp.yml +++ b/.github/workflows/e2e-ios-templateapp.yml @@ -15,10 +15,10 @@ on: value: ${{ jobs.report.outputs.status }} jobs: - test: + # Building the app needs Xcode, so it stays on a macOS runner. The E2E run + # itself drives a remote simulator via sim-remote, so it runs on Linux. + build: runs-on: macos-26-large - outputs: - status: ${{ steps.report-status.outputs.status }} strategy: fail-fast: false matrix: @@ -62,7 +62,7 @@ jobs: run: | git config --global user.email "react-native-bot@meta.com" git config --global user.name "React Native Bot" - - name: Prepare artifacts + - name: Build the app run: | REACT_NATIVE_PKG=$(find /tmp/react-native-tmp -type f -name "*.tgz") echo "React Native tgs is $REACT_NATIVE_PKG" @@ -92,12 +92,72 @@ jobs: -sdk "iphonesimulator" \ -destination "generic/platform=iOS Simulator" \ -derivedDataPath "/tmp/RNTestProject" + - name: Upload app + uses: actions/upload-artifact@v6 + with: + name: RNTestProject-${{ matrix.flavor }} + path: /tmp/RNTestProject/Build/Products/${{ matrix.flavor }}-iphonesimulator/RNTestProject.app + + test: + needs: build + runs-on: ubuntu-latest + env: + SIM_ROUTER_URL: ${{ secrets.SIM_ROUTER_URL }} + SIM_ROUTER_USERNAME: ${{ vars.SIM_ROUTER_USERNAME }} + SIM_ROUTER_API_KEY: ${{ secrets.SIM_ROUTER_API_KEY }} + outputs: + status: ${{ steps.report-status.outputs.status }} + strategy: + fail-fast: false + matrix: + flavor: [Debug, Release] + steps: + - name: Checkout + uses: actions/checkout@v6 + - name: Setup node.js + uses: ./.github/actions/setup-node + - name: Run yarn + uses: ./.github/actions/yarn-install + - name: Download app + uses: actions/download-artifact@v7 + with: + name: RNTestProject-${{ matrix.flavor }} + path: /tmp/RNTestProjectBuild/RNTestProject.app + - name: Check downloaded folder content + run: ls -l /tmp/RNTestProjectBuild/RNTestProject.app + - name: Download React Native Package + if: ${{ matrix.flavor == 'Debug' }} + uses: actions/download-artifact@v7 + with: + name: react-native-package + path: /tmp/react-native-tmp + - name: Configure git + if: ${{ matrix.flavor == 'Debug' }} + shell: bash + run: | + git config --global user.email "react-native-bot@meta.com" + git config --global user.name "React Native Bot" + - name: Prepare project for Metro + if: ${{ matrix.flavor == 'Debug' }} + # In Debug the app loads its bundle from Metro, which must run from an + # initialized project. Re-initialize it here (JS only — no pods); the + # native app itself comes prebuilt from the `build` job. + run: | + REACT_NATIVE_PKG=$(find /tmp/react-native-tmp -type f -name "*.tgz") + echo "React Native tgs is $REACT_NATIVE_PKG" + + BRANCH=${{ github.ref_name }} + if ! [[ $BRANCH == *-stable* ]]; then + BRANCH=main + fi + + node ./scripts/e2e/init-project-e2e.js --projectName RNTestProject --currentBranch $BRANCH --directory /tmp/RNTestProject --pathToLocalReactNative $REACT_NATIVE_PKG - name: Run E2E Tests id: run-tests continue-on-error: true uses: ./.github/actions/maestro-ios with: - app-path: '/tmp/RNTestProject/Build/Products/${{ matrix.flavor }}-iphonesimulator/RNTestProject.app' + app-path: '/tmp/RNTestProjectBuild/RNTestProject.app' app-id: org.reactjs.native.example.RNTestProject maestro-flow: ./scripts/e2e/.maestro/ flavor: ${{ matrix.flavor }} diff --git a/.github/workflows/monitor-new-issues.yml b/.github/workflows/monitor-new-issues.yml index 476daf11880d..c47dcb5ad0c0 100644 --- a/.github/workflows/monitor-new-issues.yml +++ b/.github/workflows/monitor-new-issues.yml @@ -1,8 +1,6 @@ name: Monitor React Native New Issues on: - schedule: - - cron: '0 0,6,12,18 * * *' workflow_dispatch: # Reminder for when we have to update the schedule (before Jan 2026): diff --git a/.github/workflows/needs-attention.yml b/.github/workflows/needs-attention.yml index 85ac9761d919..29ccbf5a6601 100644 --- a/.github/workflows/needs-attention.yml +++ b/.github/workflows/needs-attention.yml @@ -1,8 +1,7 @@ name: Issue Needs Attention # This workflow is triggered on issue comments. on: - issue_comment: - types: [created] + workflow_dispatch: permissions: contents: read diff --git a/.github/workflows/on-issue-labeled.yml b/.github/workflows/on-issue-labeled.yml index b59f9afdbdfa..b9f7b8add93c 100644 --- a/.github/workflows/on-issue-labeled.yml +++ b/.github/workflows/on-issue-labeled.yml @@ -1,8 +1,7 @@ name: On Issue Labeled # This workflow is triggered when a label is added to an issue. on: - issues: - types: [labeled] + workflow_dispatch: permissions: contents: write diff --git a/.github/workflows/publish-npm.yml b/.github/workflows/publish-npm.yml index a3e097774b32..6ca213211f75 100644 --- a/.github/workflows/publish-npm.yml +++ b/.github/workflows/publish-npm.yml @@ -15,17 +15,7 @@ name: Publish to npm on: - push: - tags: - - 'v0.*.*' # This should match v0.X.Y - - 'v0.*.*-rc.*' # This should match v0.X.Y-RC.0 - branches: - - 'main' - - '*-stable' workflow_dispatch: - # nightly build @ 2:15 AM UTC - schedule: - - cron: '15 2 * * *' permissions: contents: read diff --git a/.github/workflows/stale-bot.yml b/.github/workflows/stale-bot.yml index ab7ada8c9c51..8eac569b0d77 100644 --- a/.github/workflows/stale-bot.yml +++ b/.github/workflows/stale-bot.yml @@ -1,7 +1,7 @@ name: Stale bot on: - schedule: - - cron: '*/10 5 * * *' + workflow_dispatch: + jobs: stale: runs-on: ubuntu-latest diff --git a/.github/workflows/test-all.yml b/.github/workflows/test-all.yml index d2c63d843967..9908b4e1aadc 100644 --- a/.github/workflows/test-all.yml +++ b/.github/workflows/test-all.yml @@ -2,11 +2,6 @@ name: Test All on: workflow_dispatch: - pull_request: - push: - branches: - - main - - '*-stable' permissions: contents: read diff --git a/.github/workflows/validate-cxx-api-snapshots.yml b/.github/workflows/validate-cxx-api-snapshots.yml index e0c7caa2f5d8..dfbd21cfa393 100644 --- a/.github/workflows/validate-cxx-api-snapshots.yml +++ b/.github/workflows/validate-cxx-api-snapshots.yml @@ -2,25 +2,6 @@ name: Validate C++ API Snapshots on: workflow_dispatch: - pull_request: - paths: - - 'packages/react-native/ReactCommon/**' - - 'packages/react-native/ReactAndroid/**' - - 'packages/react-native/React/**' - - 'packages/react-native/ReactApple/**' - - 'packages/react-native/Libraries/**' - - 'scripts/cxx-api/**' - push: - branches: - - main - - '*-stable' - paths: - - 'packages/react-native/ReactCommon/**' - - 'packages/react-native/ReactAndroid/**' - - 'packages/react-native/React/**' - - 'packages/react-native/ReactApple/**' - - 'packages/react-native/Libraries/**' - - 'scripts/cxx-api/**' env: DOXYGEN_VERSION: '1.16.1' diff --git a/.github/workflows/validate-dotslash-artifacts.yml b/.github/workflows/validate-dotslash-artifacts.yml index 2970d4329056..88c549abf250 100644 --- a/.github/workflows/validate-dotslash-artifacts.yml +++ b/.github/workflows/validate-dotslash-artifacts.yml @@ -2,27 +2,6 @@ name: Validate DotSlash Artifacts on: workflow_dispatch: - release: - types: [published] - push: - branches: - - main - paths: - - packages/debugger-shell/bin/react-native-devtools - - 'scripts/releases/**' - - package.json - - yarn.lock - pull_request: - branches: - - main - paths: - - packages/debugger-shell/bin/react-native-devtools - - 'scripts/releases/**' - - package.json - - yarn.lock - # Same time as the nightly build: 2:15 AM UTC - schedule: - - cron: '15 2 * * *' jobs: validate-dotslash-artifacts: