4b7ab46d5c
The OTA pipeline decided OTA-vs-native-rebuild by diffing the current fingerprint against a baseline commit stored in an actions/cache entry keyed most-recent-testflight-commit. That cache only saved on a miss but was restored on every run, so it only advanced when GitHub evicted it. Once eviction stopped (~June 12), the baseline froze, every fingerprint looked changed, and OTA deploys silently stopped - every push ran full native builds instead. Replace the cache baseline with a repo variable (MOST_RECENT_TESTFLIGHT_NATIVE_HASH) advanced by a dedicated recordBaseline job that runs only after both native builds succeed. The fingerprint action (bumped to the hash-aware v0.3.0) compares the current commit's native hash against the variable directly, skipping the baseline checkout+reinstall. recordBaseline is isolated so the PAT that can write repo variables (EAS_BASELINE_VARIABLE_TOKEN - the built-in GITHUB_TOKEN cannot manage variables) lives nowhere else in the pipeline, and refuses to write an empty value. The now-dead cache write steps are removed from the reusable build-submit-ios/android workflows. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
311 lines
14 KiB
YAML
311 lines
14 KiB
YAML
---
|
|
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
|
|
|
|
# denis release tag in bluesky-social/tango whose linux-amd64 binary this
|
|
# workflow downloads to publish OTA bundles. Bump this one line to roll denis.
|
|
env:
|
|
DENIS_RELEASE_TAG: denis-v0.1.1
|
|
|
|
jobs:
|
|
bundleDeploy:
|
|
if: github.repository == 'bluesky-social/social-app'
|
|
name: Bundle and Deploy EAS Update
|
|
runs-on: ubuntu-latest
|
|
# id-token: write lets this job mint an OIDC token to assume the denis
|
|
# publish role; contents: read is still needed for the checkout.
|
|
permissions:
|
|
id-token: write
|
|
contents: read
|
|
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 }}
|
|
# Native-autolinking hash of this commit. recordBaseline persists it as the
|
|
# next baseline once both native builds have shipped this native surface.
|
|
native-hash: ${{ steps.fingerprint.outputs.current-native-hash }}
|
|
|
|
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@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
|
|
with:
|
|
node-version-file: package.json
|
|
cache: pnpm
|
|
|
|
- name: 📷 Check fingerprint and install dependencies
|
|
id: fingerprint
|
|
uses: bluesky-social/github-actions/fingerprint-native@c816fb4e387f8d53b8c81ec20b4cabdee8840d00 # v0.3.0
|
|
with:
|
|
profile: ${{ inputs.channel || 'testflight' }}
|
|
previous-commit-tag: ${{ inputs.runtimeVersion }}
|
|
# Baseline is a repo variable advanced by the recordBaseline job after
|
|
# each successful native deploy, instead of a frozen actions/cache entry.
|
|
# Empty on the very first run, in which case the action falls back to the
|
|
# legacy cache baseline.
|
|
baseline-native-hash: ${{ vars.MOST_RECENT_TESTFLIGHT_NATIVE_HASH }}
|
|
|
|
- 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' }}
|
|
|
|
# The three steps below dual-write the same exported bundle to the new
|
|
# denis/S3 service alongside the legacy ota1 upload above. This is a
|
|
# deliberate temporary dual-write during the ota1 -> denis migration:
|
|
# both paths run and both must succeed. The legacy step above and this
|
|
# block are removed together once denis is the sole origin (Phase 5).
|
|
- name: ☁️ Configure AWS credentials (denis)
|
|
if: ${{ !steps.fingerprint.outputs.includes-changes &&
|
|
!steps.version.outputs.version-changed }}
|
|
uses: aws-actions/configure-aws-credentials@254c19bd240aabef8777f48595e9d2d7b972184b # v6.2.1
|
|
with:
|
|
role-to-assume: arn:aws:iam::007404326489:role/denis-ci-publish
|
|
aws-region: us-east-2
|
|
|
|
- name: ⬇️ Setup denis CLI
|
|
if: ${{ !steps.fingerprint.outputs.includes-changes &&
|
|
!steps.version.outputs.version-changed }}
|
|
uses: ./.github/actions/setup-denis
|
|
with:
|
|
release-tag: ${{ env.DENIS_RELEASE_TAG }}
|
|
app-id: ${{ vars.SYNC_INTERNAL_APP_ID }}
|
|
private-key: ${{ secrets.SYNC_INTERNAL_PK }}
|
|
|
|
- name: 🚀 Publish OTA to denis (S3)
|
|
if: ${{ !steps.fingerprint.outputs.includes-changes &&
|
|
!steps.version.outputs.version-changed }}
|
|
run: pnpm use-build-number bash scripts/denisPublish.sh
|
|
env:
|
|
RUNTIME_VERSION: ${{ inputs.runtimeVersion }}
|
|
CHANNEL_NAME: ${{ inputs.channel || 'testflight' }}
|
|
|
|
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 }}
|
|
|
|
# Advance the fingerprint baseline only after BOTH native builds have shipped
|
|
# the new native surface. This replaces the old actions/cache baseline, which
|
|
# only advanced on cache eviction and so silently froze - freezing meant every
|
|
# fingerprint looked changed and OTA updates stopped deploying entirely.
|
|
#
|
|
# This runs only on the native-build path (both build jobs succeeded). On the
|
|
# OTA path those jobs are skipped, so this job is skipped too - correct, since
|
|
# an OTA update by definition leaves the native surface (and thus the hash)
|
|
# unchanged, so there is nothing to advance.
|
|
#
|
|
# Isolated as its own job so the token that can write repo variables lives
|
|
# nowhere else in the pipeline. The built-in GITHUB_TOKEN cannot manage Actions
|
|
# variables under any `permissions:` setting, so a PAT/App token with
|
|
# `variables: write` is required (EAS_BASELINE_VARIABLE_TOKEN).
|
|
recordBaseline:
|
|
name: Record fingerprint baseline
|
|
runs-on: ubuntu-latest
|
|
needs: [bundleDeploy, buildIfNecessaryIOS, buildIfNecessaryAndroid]
|
|
if: ${{ (inputs.channel || 'testflight') == 'testflight' &&
|
|
needs.buildIfNecessaryIOS.result == 'success' &&
|
|
needs.buildIfNecessaryAndroid.result == 'success' &&
|
|
github.repository == 'bluesky-social/social-app' }}
|
|
# No repo checkout or GITHUB_TOKEN work happens here; only the PAT is used.
|
|
permissions: {}
|
|
steps:
|
|
- name: ✏️ Advance baseline repo variable
|
|
env:
|
|
# PAT/App token with `variables: write`; see the job comment above
|
|
GH_TOKEN: ${{ secrets.EAS_BASELINE_VARIABLE_TOKEN }}
|
|
REPO: ${{ github.repository }}
|
|
NATIVE_HASH: ${{ needs.bundleDeploy.outputs.native-hash }}
|
|
run: |
|
|
# Fail loudly rather than writing an empty baseline. A blank value here
|
|
# would make the next run's fast-path comparison always mismatch and
|
|
# force perpetual native builds - the exact silent failure we're fixing.
|
|
if [ -z "$NATIVE_HASH" ]; then
|
|
echo "::error::bundleDeploy did not emit a native hash; refusing to write an empty baseline."
|
|
exit 1
|
|
fi
|
|
echo "Advancing MOST_RECENT_TESTFLIGHT_NATIVE_HASH to $NATIVE_HASH"
|
|
gh variable set MOST_RECENT_TESTFLIGHT_NATIVE_HASH \
|
|
--repo "$REPO" \
|
|
--body "$NATIVE_HASH"
|