diff --git a/.github/workflows/build-and-deploy-worker.yml b/.github/workflows/build-and-deploy-worker.yml index 1f64b44c11..85295723be 100644 --- a/.github/workflows/build-and-deploy-worker.yml +++ b/.github/workflows/build-and-deploy-worker.yml @@ -7,21 +7,28 @@ 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 +46,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 +72,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 +199,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 +208,12 @@ 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" + + 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..145cd63bb0 --- /dev/null +++ b/.github/workflows/worker-preview.yml @@ -0,0 +1,70 @@ +name: worker-preview + +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: + 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 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 = {