--- name: Bundle and Deploy EAS Update on: push: branches: - main workflow_dispatch: inputs: channel: type: choice description: Deployment channel to use options: - testflight - production runtimeVersion: type: string description: Runtime version (in x.x.x format) that this update is for required: true # Deploys happen via EAS using EXPO_TOKEN; the GITHUB_TOKEN only checks out code permissions: contents: read jobs: bundleDeploy: if: github.repository == 'bluesky-social/social-app' name: Bundle and Deploy EAS Update runs-on: ubuntu-latest concurrency: group: ${{ github.workflow }}-${{ github.event_name }}-${{ github.ref }}-deploy cancel-in-progress: true outputs: # A version bump forces a native build even if the fingerprint is unchanged changes-detected: ${{ steps.fingerprint.outputs.includes-changes || steps.version.outputs.version-changed }} steps: - name: Check for EXPO_TOKEN run: > if [ -z "${{ secrets.EXPO_TOKEN }}" ]; then echo "You must provide an EXPO_TOKEN secret linked to this project's Expo account in this repo's secrets. Learn more: https://docs.expo.dev/eas-update/github-actions" exit 1 fi # Validate the version if one is supplied. This should generally happen if the update is for a production client - name: 🧐 Validate version env: RUNTIME_VERSION: ${{ inputs.runtimeVersion }} if: ${{ inputs.runtimeVersion }} run: | if [ -z "$RUNTIME_VERSION" ]; then [[ "$RUNTIME_VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]] && echo "Version is valid" || exit 1 fi - name: ⬇️ Checkout uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: fetch-depth: 0 - name: ⬇️ Fetch commits from base branch if: ${{ github.ref != 'refs/heads/main' }} run: git fetch origin main:main --depth 100 # A change to the version in package.json means a new native release, so # an OTA update must not be deployed and full native builds are required # regardless of what the fingerprint says - name: 🔢 Check for version change id: version if: ${{ github.event_name == 'push' }} env: EVENT_BEFORE: ${{ github.event.before }} run: | CURRENT_VERSION=$(jq -r '.version' package.json) if [ -n "$EVENT_BEFORE" ] && [[ ! "$EVENT_BEFORE" =~ ^0+$ ]] && git cat-file -e "$EVENT_BEFORE:package.json" 2>/dev/null; then PREVIOUS_VERSION=$(git show "$EVENT_BEFORE:package.json" | jq -r '.version') else PREVIOUS_VERSION=$(git show HEAD~1:package.json | jq -r '.version') fi echo "Previous version: $PREVIOUS_VERSION, current version: $CURRENT_VERSION" if [ "$CURRENT_VERSION" != "$PREVIOUS_VERSION" ]; then echo "Version changed, full native builds are required" echo "version-changed=true" >> "$GITHUB_OUTPUT" fi - uses: pnpm/action-setup@0ebf47130e4866e96fce0953f49152a61190b271 # v6.0.9 - name: 🔧 Setup Node uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 with: node-version-file: package.json cache: pnpm - name: 📷 Check fingerprint and install dependencies id: fingerprint uses: bluesky-social/github-actions/fingerprint-native@b5556913e4aef3964cfd5936d0add3fc0d809bdb # v0.2.0 with: profile: ${{ inputs.channel || 'testflight' }} previous-commit-tag: ${{ inputs.runtimeVersion }} - name: 🔤 Compile translations uses: ./.github/actions/compile-i18n - name: Lint check run: pnpm lint - name: Prettier check run: pnpm prettier --check . - name: Type check run: pnpm typecheck - name: 🔨 Setup EAS uses: expo/expo-github-action@eab7a230208c952974db8c3245cfd78402c7b385 # 9.0.0 if: ${{ !steps.fingerprint.outputs.includes-changes && !steps.version.outputs.version-changed }} with: eas-version: '19.0.5' packager: 'pnpm --allow-build=dtrace-provider' token: ${{ secrets.EXPO_TOKEN }} - name: 🪛 Setup jq if: ${{ !steps.fingerprint.outputs.includes-changes && !steps.version.outputs.version-changed }} uses: dcarbone/install-jq-action@4fcb5062d7ce9bc4382d1a352d19ba3ba2c317c1 # v4.0.1 # eas.json not used here, so EXPO_PUBLIC_ENV must be written explicitly - name: ✏️ Write environment variables id: env if: ${{ !steps.fingerprint.outputs.includes-changes && !steps.version.outputs.version-changed }} uses: ./.github/actions/write-env with: env-token: ${{ secrets.ENV_TOKEN }} sentry-dsn: ${{ secrets.SENTRY_DSN }} bitdrift-api-key: ${{ secrets.BITDRIFT_API_KEY }} gcp-project-id: ${{ secrets.EXPO_PUBLIC_GCP_PROJECT_ID }} google-services-token: ${{ secrets.GOOGLE_SERVICES_TOKEN }} expo-public-env: ${{ inputs.channel || 'testflight' }} - name: 🏗️ Create Bundle if: ${{ !steps.fingerprint.outputs.includes-changes && !steps.version.outputs.version-changed }} run: > SENTRY_AUTH_TOKEN=${{ secrets.SENTRY_AUTH_TOKEN }} SENTRY_RELEASE=${{ steps.env.outputs.release-version }} SENTRY_DIST=${{ steps.env.outputs.bundle-identifier }} pnpm export - name: 📦 Package Bundle and 🚀 Deploy if: ${{ !steps.fingerprint.outputs.includes-changes && !steps.version.outputs.version-changed }} run: pnpm use-build-number bash scripts/bundleUpdate.sh env: DENIS_API_KEY: ${{ secrets.DENIS_API_KEY }} RUNTIME_VERSION: ${{ inputs.runtimeVersion }} CHANNEL_NAME: ${{ inputs.channel || 'testflight' }} - name: ⬇️ Restore Cache id: get-base-commit uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 if: ${{ !steps.fingerprint.outputs.includes-changes && !steps.version.outputs.version-changed }} with: path: most-recent-testflight-commit.txt key: most-recent-testflight-commit - name: ✏️ Write commit hash to cache if: ${{ !steps.fingerprint.outputs.includes-changes && !steps.version.outputs.version-changed }} run: echo $GITHUB_SHA > most-recent-testflight-commit.txt buildIfNecessaryIOS: name: Build and Submit iOS needs: [bundleDeploy] # Gotta check if its NOT '[]' because any md5 hash in the outputs is detected as a possible secret and won't be # available here if: ${{ inputs.channel != 'production' && needs.bundleDeploy.outputs.changes-detected && github.repository == 'bluesky-social/social-app' }} uses: ./.github/workflows/build-submit-ios.yml with: profile: testflight testFlightGroup: none # OTA rebuilds don't need the xlarge builder used for releases runner: macos-26 # Pass only the secrets the reusable workflow declares, rather than `secrets: inherit`, # so this workflow never hands the reusable workflow the entire repo secret store. secrets: EXPO_TOKEN: ${{ secrets.EXPO_TOKEN }} ENV_TOKEN: ${{ secrets.ENV_TOKEN }} SENTRY_DSN: ${{ secrets.SENTRY_DSN }} BITDRIFT_API_KEY: ${{ secrets.BITDRIFT_API_KEY }} EXPO_PUBLIC_GCP_PROJECT_ID: ${{ secrets.EXPO_PUBLIC_GCP_PROJECT_ID }} GOOGLE_SERVICES_TOKEN: ${{ secrets.GOOGLE_SERVICES_TOKEN }} SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }} ASC_KEY_ID: ${{ secrets.ASC_KEY_ID }} ASC_ISSUER_ID: ${{ secrets.ASC_ISSUER_ID }} ASC_KEY_P8_BASE64: ${{ secrets.ASC_KEY_P8_BASE64 }} SLACK_CLIENT_ALERT_WEBHOOK: ${{ secrets.SLACK_CLIENT_ALERT_WEBHOOK }} buildIfNecessaryAndroid: name: Build and Submit Android needs: [bundleDeploy] # Gotta check if its NOT '[]' because any md5 hash in the outputs is detected as a possible secret and won't be # available here if: ${{ inputs.channel != 'production' && needs.bundleDeploy.outputs.changes-detected && github.repository == 'bluesky-social/social-app' }} # build-submit-android.yml contains an attachToRelease job that requests contents: write. # That job is skipped here (it needs a production tag build), but GitHub statically # validates the reusable-workflow permission ceiling, so the caller must grant it. permissions: contents: write uses: ./.github/workflows/build-submit-android.yml with: profile: testflight-android runner: ubuntu-latest # Pass only the secrets the reusable workflow declares, rather than `secrets: inherit`, # so this workflow never hands the reusable workflow the entire repo secret store. secrets: EXPO_TOKEN: ${{ secrets.EXPO_TOKEN }} ENV_TOKEN: ${{ secrets.ENV_TOKEN }} SENTRY_DSN: ${{ secrets.SENTRY_DSN }} BITDRIFT_API_KEY: ${{ secrets.BITDRIFT_API_KEY }} EXPO_PUBLIC_GCP_PROJECT_ID: ${{ secrets.EXPO_PUBLIC_GCP_PROJECT_ID }} GOOGLE_SERVICES_TOKEN: ${{ secrets.GOOGLE_SERVICES_TOKEN }} SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }} SLACK_CLIENT_ALERT_WEBHOOK: ${{ secrets.SLACK_CLIENT_ALERT_WEBHOOK }} ANDROID_KEYSTORE_BASE64: ${{ secrets.ANDROID_KEYSTORE_BASE64 }} ANDROID_KEYSTORE_PASSWORD: ${{ secrets.ANDROID_KEYSTORE_PASSWORD }} ANDROID_KEY_ALIAS: ${{ secrets.ANDROID_KEY_ALIAS }} ANDROID_KEY_PASSWORD: ${{ secrets.ANDROID_KEY_PASSWORD }}