From 5475761b57b37f635cda1bd81049bc2641e28028 Mon Sep 17 00:00:00 2001 From: szdziedzic Date: Tue, 29 Sep 2026 19:36:12 +0200 Subject: [PATCH 1/3] [ci] Deploy worker previews from PRs with the "preview" label Co-Authored-By: Claude Opus 5.5 --- .github/workflows/build-and-deploy-worker.yml | 27 +++++-- .github/workflows/worker-preview.yml | 76 +++++++++++++++++++ 2 files changed, 96 insertions(+), 7 deletions(-) create mode 100644 .github/workflows/worker-preview.yml diff --git a/.github/workflows/build-and-deploy-worker.yml b/.github/workflows/build-and-deploy-worker.yml index 1f64b44c11..8c3d854be4 100644 --- a/.github/workflows/build-and-deploy-worker.yml +++ b/.github/workflows/build-and-deploy-worker.yml @@ -2,26 +2,34 @@ name: build-and-deploy-worker # Assumes main branch always specifies an environment (staging/production). # If environment is empty: PR testing build (no upload, no deploy) # If environment is set: Build (unless cached on main), upload, and deploy +# If environment is "preview-*": Same as above, but deploys a worker preview (see worker-preview.yml) on: workflow_dispatch: inputs: environment: - description: '"staging", "production" or empty' + description: '"staging", "production", "preview-*" or empty' type: string required: true workflow_call: inputs: environment: - description: '"staging", "production" or empty' + description: '"staging", "production", "preview-*" or empty' type: string required: true + ref: + description: Commit SHA to build the worker from (defaults to github.sha) + type: string + required: false + default: '' jobs: build-and-deploy: runs-on: ubuntu-latest permissions: id-token: write + env: + WORKER_SHA: ${{ inputs.ref || github.sha }} steps: - uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4 @@ -39,7 +47,7 @@ jobs: if: inputs.environment != '' run: | for platform in android ios; do - GCP_TARBALL="worker-$platform-sha1-$GITHUB_SHA.tar.gz" + GCP_TARBALL="worker-$platform-sha1-$WORKER_SHA.tar.gz" if ! gsutil stat "gs://eas-build-worker-tarballs/$GCP_TARBALL"; then echo "worker_exists=false" >> "$GITHUB_OUTPUT" @@ -65,7 +73,7 @@ jobs: # Run workflow and capture output (includes run ID) WORKFLOW_OUTPUT=$(eas workflow:run build-worker.yml \ - --ref ${{ github.sha }} \ + --ref "$WORKER_SHA" \ --input upload_artifact=$SHOULD_UPLOAD_ARTIFACT \ --non-interactive \ --no-wait \ @@ -192,8 +200,8 @@ jobs: # Don't upload PR builds (no environment) or when using cached tarballs if: inputs.environment != '' && steps.check_worker_exists.outputs.worker_exists == 'false' run: | - IOS_GCP_TARBALL="worker-ios-sha1-$GITHUB_SHA.tar.gz" - ANDROID_GCP_TARBALL="worker-android-sha1-$GITHUB_SHA.tar.gz" + IOS_GCP_TARBALL="worker-ios-sha1-$WORKER_SHA.tar.gz" + ANDROID_GCP_TARBALL="worker-android-sha1-$WORKER_SHA.tar.gz" curl -L "${{ steps.eas_artifacts.outputs.download_ios_url }}" | gsutil cp - "gs://eas-build-worker-tarballs/$IOS_GCP_TARBALL" curl -L "${{ steps.eas_artifacts.outputs.download_android_url }}" | gsutil cp - "gs://eas-build-worker-tarballs/$ANDROID_GCP_TARBALL" @@ -201,8 +209,13 @@ jobs: if: inputs.environment != '' run: | for platform in android ios; do - GCP_TARBALL="worker-$platform-sha1-$GITHUB_SHA.tar.gz" + GCP_TARBALL="worker-$platform-sha1-$WORKER_SHA.tar.gz" WORKER_STAGING_TARBALL="worker-$platform-${{ inputs.environment }}.tar.gz" gsutil cp "gs://eas-build-worker-tarballs/$GCP_TARBALL" "gs://eas-build-worker-tarballs/$WORKER_STAGING_TARBALL" + + # Preview tarballs are overwritten on every push to the PR, so make sure they're not served from cache. + if [[ "${{ inputs.environment }}" == preview-* ]]; then + gsutil setmeta -h "Cache-Control:no-cache, no-store, must-revalidate" "gs://eas-build-worker-tarballs/$WORKER_STAGING_TARBALL" + fi done diff --git a/.github/workflows/worker-preview.yml b/.github/workflows/worker-preview.yml new file mode 100644 index 0000000000..36bb9fbd04 --- /dev/null +++ b/.github/workflows/worker-preview.yml @@ -0,0 +1,76 @@ +name: worker-preview +# Deploys the worker built from a PR to worker-{android,ios}-preview-pr-.tar.gz when +# the PR has the "preview" label. The regular staging worker is not affected. To run a staging +# build or workflow job on the preview worker, set the `__EAS_WORKER_PREVIEW=pr-` env +# variable in the eas.json build profile `env` or in the workflow job `env`. +# The preview tarballs are removed when the label is removed or the PR is closed. + +on: + pull_request: + types: [labeled, unlabeled, synchronize, closed] + +concurrency: + group: worker-preview-${{ github.event.pull_request.number }} + cancel-in-progress: true + +jobs: + deploy-worker-preview: + # Secrets and GCP credentials are not available for PRs from forks. + if: >- + github.event.pull_request.head.repo.full_name == github.repository && + github.event.action != 'closed' && + github.event.action != 'unlabeled' && + contains(github.event.pull_request.labels.*.name, 'preview') && + (github.event.action != 'labeled' || github.event.label.name == 'preview') + permissions: + contents: read + id-token: write + uses: ./.github/workflows/build-and-deploy-worker.yml + secrets: inherit + with: + environment: preview-pr-${{ github.event.pull_request.number }} + ref: ${{ github.event.pull_request.head.sha }} + + summary: + needs: deploy-worker-preview + runs-on: ubuntu-latest + steps: + - name: Print usage instructions + run: | + cat >> "$GITHUB_STEP_SUMMARY" <- + github.event.pull_request.head.repo.full_name == github.repository && + ( + (github.event.action == 'unlabeled' && github.event.label.name == 'preview') || + (github.event.action == 'closed' && contains(github.event.pull_request.labels.*.name, 'preview')) + ) + runs-on: ubuntu-latest + permissions: + contents: read + id-token: write + steps: + - uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4 + + - uses: ./.github/internal-actions/setup-gcloud + + - name: Remove preview worker tarballs + run: | + for platform in android ios; do + gsutil rm "gs://eas-build-worker-tarballs/worker-$platform-preview-pr-${{ github.event.pull_request.number }}.tar.gz" || true + done From 219a7187805a1120311b928be104e4c10c08566d Mon Sep 17 00:00:00 2001 From: szdziedzic Date: Tue, 29 Sep 2026 19:36:12 +0200 Subject: [PATCH 2/3] [eas-cli] Add hidden --worker-preview flag to simulator:start Co-Authored-By: Claude Opus 5.5 --- .../simulator/__tests__/index.test.ts | 23 +++++++++++++++++++ .../eas-cli/src/commands/simulator/index.ts | 6 +++++ packages/eas-cli/src/graphql/generated.ts | 5 ++++ 3 files changed, 34 insertions(+) diff --git a/packages/eas-cli/src/commands/simulator/__tests__/index.test.ts b/packages/eas-cli/src/commands/simulator/__tests__/index.test.ts index 99d738465b..cbd864df50 100644 --- a/packages/eas-cli/src/commands/simulator/__tests__/index.test.ts +++ b/packages/eas-cli/src/commands/simulator/__tests__/index.test.ts @@ -602,6 +602,29 @@ describe(Simulator, () => { } ); + it('omits workerPreview when --worker-preview is not set', async () => { + const { command } = createCommand(['--platform', 'ios', '--non-interactive']); + await command.runAsync(); + + expect(mockCreateDeviceRunSessionAsync.mock.calls[0][1]).not.toHaveProperty('workerPreview'); + }); + + it('forwards --worker-preview to the create mutation', async () => { + const { command } = createCommand([ + '--platform', + 'ios', + '--non-interactive', + '--worker-preview', + 'pr-1234', + ]); + await command.runAsync(); + + expect(mockCreateDeviceRunSessionAsync).toHaveBeenCalledWith( + graphqlClient, + expect.objectContaining({ workerPreview: 'pr-1234' }) + ); + }); + it('forwards --build-id to the create mutation', async () => { const { command } = createCommand([ '--platform', diff --git a/packages/eas-cli/src/commands/simulator/index.ts b/packages/eas-cli/src/commands/simulator/index.ts index 8c2f05685a..ed70fbcb53 100644 --- a/packages/eas-cli/src/commands/simulator/index.ts +++ b/packages/eas-cli/src/commands/simulator/index.ts @@ -143,6 +143,11 @@ export default class Simulator extends EasCommand { hidden: true, options: Object.values(DEVICE_RUN_SESSION_RESOURCE_CLASS_FLAG_VALUES), })(), + 'worker-preview': Flags.string({ + description: + 'Run the session on a worker preview built from an expo/eas-cli pull request, e.g. "pr-1234". Only supported on staging.', + hidden: true, + }), egress: Flags.option({ description: 'With "local", the simulator system proxy points at this machine: HTTP(S) and WebSocket requests that honor it (WebKit, URLSession) and clients that read proxy environment variables (gRPC, libcurl) exit from this machine and fail while the egress client is disconnected. Connections that ignore both are refused inside the simulator and listed, with the library that tried, in the Logs section of the session page on expo.dev. The egress client must keep running for the life of the session. Only supported with --platform ios.', @@ -298,6 +303,7 @@ export default class Simulator extends EasCommand { ...(openUrl ? { openUrl } : {}), ...(resourceClass ? { resourceClass } : {}), ...(egress ? { egress } : {}), + ...(flags['worker-preview'] ? { workerPreview: flags['worker-preview'] } : {}), maxRunTimeMinutes: flags['max-duration-minutes'], maxIdleTimeMinutes: flags['max-idle-time-minutes'], }); diff --git a/packages/eas-cli/src/graphql/generated.ts b/packages/eas-cli/src/graphql/generated.ts index 610be03094..d314b6ffa4 100644 --- a/packages/eas-cli/src/graphql/generated.ts +++ b/packages/eas-cli/src/graphql/generated.ts @@ -6429,6 +6429,11 @@ export type CreateDeviceRunSessionInput = { /** Free-form labels for grouping sessions, for example one per app variant. */ tags?: InputMaybe>; type: DeviceRunSessionType; + /** + * Internal. Run the session on a worker preview built from an expo/eas-cli pull request + * (e.g. "pr-1234"). Only supported on staging. + */ + workerPreview?: InputMaybe; }; export type CreateEchoChatInput = { From fcf6fb7c239c21b913f43efd9fdc40a205dd096c Mon Sep 17 00:00:00 2001 From: szdziedzic Date: Tue, 29 Sep 2026 19:51:53 +0200 Subject: [PATCH 3/3] [ci] Drop worker preview comments Co-Authored-By: Claude Opus 5.5 --- .github/workflows/build-and-deploy-worker.yml | 2 -- .github/workflows/worker-preview.yml | 6 ------ 2 files changed, 8 deletions(-) diff --git a/.github/workflows/build-and-deploy-worker.yml b/.github/workflows/build-and-deploy-worker.yml index 8c3d854be4..85295723be 100644 --- a/.github/workflows/build-and-deploy-worker.yml +++ b/.github/workflows/build-and-deploy-worker.yml @@ -2,7 +2,6 @@ name: build-and-deploy-worker # Assumes main branch always specifies an environment (staging/production). # If environment is empty: PR testing build (no upload, no deploy) # If environment is set: Build (unless cached on main), upload, and deploy -# If environment is "preview-*": Same as above, but deploys a worker preview (see worker-preview.yml) on: workflow_dispatch: @@ -214,7 +213,6 @@ jobs: gsutil cp "gs://eas-build-worker-tarballs/$GCP_TARBALL" "gs://eas-build-worker-tarballs/$WORKER_STAGING_TARBALL" - # Preview tarballs are overwritten on every push to the PR, so make sure they're not served from cache. if [[ "${{ inputs.environment }}" == preview-* ]]; then gsutil setmeta -h "Cache-Control:no-cache, no-store, must-revalidate" "gs://eas-build-worker-tarballs/$WORKER_STAGING_TARBALL" fi diff --git a/.github/workflows/worker-preview.yml b/.github/workflows/worker-preview.yml index 36bb9fbd04..145cd63bb0 100644 --- a/.github/workflows/worker-preview.yml +++ b/.github/workflows/worker-preview.yml @@ -1,9 +1,4 @@ name: worker-preview -# Deploys the worker built from a PR to worker-{android,ios}-preview-pr-.tar.gz when -# the PR has the "preview" label. The regular staging worker is not affected. To run a staging -# build or workflow job on the preview worker, set the `__EAS_WORKER_PREVIEW=pr-` env -# variable in the eas.json build profile `env` or in the workflow job `env`. -# The preview tarballs are removed when the label is removed or the PR is closed. on: pull_request: @@ -15,7 +10,6 @@ concurrency: jobs: deploy-worker-preview: - # Secrets and GCP credentials are not available for PRs from forks. if: >- github.event.pull_request.head.repo.full_name == github.repository && github.event.action != 'closed' &&