Skip to content

Instantly share code, notes, and snippets.

@javascripter
Created July 28, 2025 08:22
Show Gist options
  • Save javascripter/4b55a8fc218f79d622c782fbdb524f02 to your computer and use it in GitHub Desktop.
Save javascripter/4b55a8fc218f79d622c782fbdb524f02 to your computer and use it in GitHub Desktop.
Canary EAS Rollout workflow (using PR label)
name: EAS Rollout
on:
pull_request:
types: [labeled, unlabeled, closed, synchronize]
permissions:
pull-requests: write
contents: read
jobs:
rollout-start:
if:
| # Check if rollout label was added to PR or if new commit was pushed to PR with rollout label
(github.event.action == 'labeled' &&
github.event.label.name == 'rollout') ||
(github.event.action == 'synchronize' &&
contains(github.event.pull_request.labels.*.name, 'rollout'))
runs-on: ubuntu-latest
concurrency:
group: rollout-${{ github.event.pull_request.number }}
cancel-in-progress: false
steps:
- name: πŸ— Setup repo
uses: actions/checkout@v4
with:
fetch-depth: 0 # πŸ‘ˆ Required to retrieve git history
- name: πŸ— Setup Node.js
uses: actions/setup-node@v4
with:
node-version: 20
cache: 'npm' # or 'yarn', 'pnpm' depending on your package manager
- name: πŸ“¦ Install dependencies
run: npm ci # or yarn install, pnpm install
- name: πŸ— Setup Expo
uses: expo/expo-github-action@v8
with:
eas-version: latest
token: ${{ secrets.EXPO_TOKEN }}
- name: πŸ— Setup Config
id: config
run: | # Modify this section to match your app's config
RUNTIME=$(npx expo config --json|jq -r '.runtimeVersion')
CHANNEL=$(jq -r '.build.production.channel' < eas.json)
echo "runtime=$RUNTIME" >> $GITHUB_OUTPUT
echo "channel=$CHANNEL" >> $GITHUB_OUTPUT
echo "branch=rollout-pr-${{ github.event.number }}" >> $GITHUB_OUTPUT
echo "pr=${{ github.event.number }}" >> $GITHUB_OUTPUT
- name: πŸš€ EAS Update
run: |
eas branch:create ${{ steps.config.outputs.branch }} --non-interactive || true
eas update \
--branch=${{ steps.config.outputs.branch }} \
--message "Canary rollout for PR #${{ steps.config.outputs.pr }}"
env:
EXPO_TOKEN: ${{ secrets.EXPO_TOKEN }}
- name: πŸš€ Start rollout
id: rollout
continue-on-error: true
run: |
BRANCH=${{ steps.config.outputs.branch }}
CHANNEL=${{ steps.config.outputs.channel }}
RUNTIME=${{ steps.config.outputs.runtime }}
echo "πŸ” Checking if rollout already exists on channel: $CHANNEL"
OUT=$(eas channel:rollout "$CHANNEL" \
--action=view \
--json \
--non-interactive || echo "{}")
CURRENT_BRANCH=$(echo "$OUT" | jq -r '.currentRolloutInfo.rolledOutBranch.name // empty')
if [ "$CURRENT_BRANCH" = "$BRANCH" ]; then
echo "βœ… Rollout already active for this PR branch ($BRANCH)"
echo "created=false" >> $GITHUB_OUTPUT
exit 0
fi
if [ -n "$CURRENT_BRANCH" ]; then
echo "🚫 Rollout already active for another branch ($CURRENT_BRANCH)"
echo "created=failed" >> $GITHUB_OUTPUT
exit 1
fi
echo "πŸš€ Creating rollout for $BRANCH"
eas channel:rollout "$CHANNEL" \
--action=create \
--branch="$BRANCH" \
--percent=5 \
--runtime-version="$RUNTIME" \
--non-interactive
echo "created=true" >> $GITHUB_OUTPUT
env:
EXPO_TOKEN: ${{ secrets.EXPO_TOKEN }}
- name: πŸ’¬ Comment status on success
uses: marocchino/sticky-pull-request-comment@v2
if: steps.rollout.outcome == 'success' && steps.rollout.outputs.created == 'true'
with:
number: ${{ steps.config.outputs.pr }}
header: 'Expo Canary Rollout Status'
message: |
# βœ… Rollout Started
**Runtime:** `${{ steps.config.outputs.runtime }}`
**Channel:** `${{ steps.config.outputs.channel }}`
**Branch:** `${{ steps.config.outputs.branch }}`
**Rollout status:** `${{ steps.rollout.outcome }}`
PR #${{ steps.config.outputs.pr }} is now being rolled out to 5% of users on production.
Remove the label manually or close the PR to end the rollout.
- name: πŸ’¬ Comment status on failure
uses: marocchino/sticky-pull-request-comment@v2
if: steps.rollout.outcome != 'success'
with:
number: ${{ steps.config.outputs.pr }}
header: 'Expo Canary Rollout Status'
message: |
# ❌ Rollout Failed
**Runtime:** `${{ steps.config.outputs.runtime }}`
**Channel:** `${{ steps.config.outputs.channel }}`
**Branch:** `${{ steps.config.outputs.branch }}`
**Rollout status:** `${{ steps.rollout.outcome }}`
A rollout is already in progress on `${{ steps.config.outputs.channel }}`.
Try again after the current rollout ends by removing the label and re-adding it.
rollout-end:
if:
| # Check if rollout label was removed or PR was closed with rollout label
(github.event.action == 'unlabeled' &&
github.event.label.name == 'rollout') ||
(github.event.action == 'closed' &&
contains(github.event.pull_request.labels.*.name, 'rollout'))
runs-on: ubuntu-latest
concurrency:
group: rollout-${{ github.event.pull_request.number }}
cancel-in-progress: false
steps:
- name: πŸ— Setup repo
uses: actions/checkout@v4
with:
fetch-depth: 0 # πŸ‘ˆ Required to retrieve git history
- name: πŸ— Setup Node.js
uses: actions/setup-node@v4
with:
node-version: 20
cache: 'npm' # or 'yarn', 'pnpm' depending on your package manager
- name: πŸ“¦ Install dependencies
run: npm ci # or yarn install, pnpm install
- name: πŸ— Setup Expo
uses: expo/expo-github-action@v8
with:
eas-version: latest
token: ${{ secrets.EXPO_TOKEN }}
- name: πŸ— Setup Config
id: config
run: | # Modify this section to match your app's config
# Adjust these paths to match your configuration files
RUNTIME=$(npx expo config --json|jq -r '.runtimeVersion')
CHANNEL=$(jq -r '.build.production.channel' < eas.json)
echo "runtime=$RUNTIME" >> $GITHUB_OUTPUT
echo "channel=$CHANNEL" >> $GITHUB_OUTPUT
echo "branch=rollout-pr-${{ github.event.number }}" >> $GITHUB_OUTPUT
echo "pr=${{ github.event.number }}" >> $GITHUB_OUTPUT
- name: πŸš€ End rollout
id: end-rollout
run: |
BRANCH=${{ steps.config.outputs.branch }}
CHANNEL=${{ steps.config.outputs.channel }}
RUNTIME=${{ steps.config.outputs.runtime }}
echo "πŸ” Checking rollout ownership for branch $BRANCH on channel $CHANNEL"
OUT=$(eas channel:rollout $CHANNEL \
--action=view --json --non-interactive)
CURRENT_BRANCH=$(echo "$OUT" | jq -r '.currentRolloutInfo.rolledOutBranch.name // empty')
if [ -z "$CURRENT_BRANCH" ]; then
echo "ℹ️ No active rollout on this channel."
exit 0
fi
if [ "$CURRENT_BRANCH" = "$BRANCH" ]; then
echo "βœ… Rollout belongs to this PR – ending rollout"
eas channel:rollout $CHANNEL \
--action=end \
--outcome=revert \
--runtime-version=$RUNTIME \
--non-interactive
else
echo "⚠️ Rollout is for branch '$CURRENT_BRANCH' (not this PR: '$BRANCH') – skipping end"
echo "failure_message=Rollout is for branch $CURRENT_BRANCH (not this PR: $BRANCH)" >> $GITHUB_OUTPUT
exit 1
fi
env:
EXPO_TOKEN: ${{ secrets.EXPO_TOKEN }}
- name: πŸ’¬ Comment status on success
uses: marocchino/sticky-pull-request-comment@v2
if: steps.end-rollout.outcome == 'success'
with:
number: ${{ steps.config.outputs.pr }}
header: 'Expo Canary Rollout Status'
message: |
# βœ… Rollout Ended
**Runtime:** `${{ steps.config.outputs.runtime }}`
**Channel:** `${{ steps.config.outputs.channel }}`
**Branch:** `${{ steps.config.outputs.branch }}`
**Reason:** `${{ github.event.action == 'closed' && 'PR closed' || 'rollout label removed' }}`
- name: πŸ’¬ Comment status on failure
uses: marocchino/sticky-pull-request-comment@v2
if: steps.end-rollout.outcome != 'success'
with:
number: ${{ steps.config.outputs.pr }}
header: 'Expo Canary Rollout Status'
message: |
# ❌ Rollout Failed to End
**Runtime:** `${{ steps.config.outputs.runtime }}`
**Channel:** `${{ steps.config.outputs.channel }}`
**Branch:** `${{ steps.config.outputs.branch }}`
**Reason:** `${{ github.event.action == 'closed' && 'PR closed' || 'rollout label removed' }}`
${{ steps.end-rollout.outputs.failure_message && format('{0}', steps.end-rollout.outputs.failure_message) || '' }}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment