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
25 changes: 18 additions & 7 deletions .github/workflows/build-and-deploy-worker.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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"
Expand All @@ -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 \
Expand Down Expand Up @@ -192,17 +199,21 @@ 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"

- name: Promote worker to ${{ inputs.environment }}
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
70 changes: 70 additions & 0 deletions .github/workflows/worker-preview.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
name: worker-preview

on:
pull_request:
types: [labeled, unlabeled, synchronize, closed]

@devin-ai-integration devin-ai-integration Bot Sep 29, 2026 •

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🟡 Reopened PR keeps a missing preview

Reopening a closed PR with the preview label does not trigger deployment because reopened is absent. Closing deleted its tarballs, so the labeled PR has no preview until another push or relabel.

Devin Review


Was this helpful? React with 👍 or 👎 to provide feedback.


concurrency:
group: worker-preview-${{ github.event.pull_request.number }}
cancel-in-progress: true
Comment on lines +7 to +9

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔴 Unrelated labels cancel preview deployment

Changing another label while a preview deploys makes cancel-in-progress cancel the deploy. The replacement run skips both jobs, leaving the preview missing or outdated.

Learn more

The workflow subscribes to all label changes, but deploy-worker-preview runs only when the preview label is added and remove-worker-preview runs only when it is removed. Concurrency applies before those job conditions, so an unrelated label change cancels a running deployment without replacing it. The same issue affects cleanup: closing a PR immediately after removing preview cancels the removal run, while the close run sees no preview label and skips removal.

Example: Add preview to PR 123 and, while its worker builds, add needs-review. The second run cancels the build, skips both jobs, and leaves pr-123 without its new worker.

Recommended fix: Ensure irrelevant label events cannot enter the preview's canceling concurrency group. For example, handle deployment and cleanup in separate workflows or concurrency groups with event-specific admission, and let a close event repeat cleanup even if preview was already removed.

Devin Review


Was this helpful? React with 👍 or 👎 to provide feedback.


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" <<EOF
Worker from ${{ github.event.pull_request.head.sha }} is deployed as preview \`pr-${{ github.event.pull_request.number }}\`.

To use it on staging, set this env variable in the eas.json build profile \`env\` or in the workflow job \`env\`:

\`\`\`
__EAS_WORKER_PREVIEW=pr-${{ github.event.pull_request.number }}
\`\`\`

To use it for a simulator session on staging:

\`\`\`
EXPO_STAGING=1 eas simulator:start --worker-preview pr-${{ github.event.pull_request.number }}
\`\`\`
EOF

remove-worker-preview:
if: >-
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
23 changes: 23 additions & 0 deletions packages/eas-cli/src/commands/simulator/__tests__/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
6 changes: 6 additions & 0 deletions packages/eas-cli/src/commands/simulator/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
}),
Comment on lines +146 to +150

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔍 Check changelog exemption for new flag

The new simulator flag has no changelog entry, although CONTRIBUTING.md requests one for every PR. Confirm whether the changelog exemption applies.

Devin Review


Was this helpful? React with 👍 or 👎 to provide feedback.

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.',
Expand Down Expand Up @@ -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'],
});
Expand Down
5 changes: 5 additions & 0 deletions packages/eas-cli/src/graphql/generated.ts

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading