Split production and TestFlight OTA workflows
This commit is contained in:
@@ -0,0 +1,309 @@
|
||||
---
|
||||
name: Deploy TestFlight OTA
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
|
||||
# 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 TestFlight OTA
|
||||
runs-on: ubuntu-latest
|
||||
# id-token: write lets this job mint an OIDC token to assume the denis
|
||||
# publish role; actions: read loads the fingerprint baseline artifact;
|
||||
# contents: read is still needed for the checkout.
|
||||
permissions:
|
||||
id-token: write
|
||||
actions: read
|
||||
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 }}
|
||||
|
||||
steps:
|
||||
- name: ⬇️ Checkout
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- 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
|
||||
|
||||
- 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@0977fd99725f1db4007ccb2928dbb4e90d06cc86 # v6.0.10
|
||||
|
||||
- name: 🔧 Setup Node
|
||||
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
|
||||
with:
|
||||
node-version-file: package.json
|
||||
cache: pnpm
|
||||
|
||||
- name: ⬇️ Load fingerprint baseline
|
||||
id: baseline
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
REPOSITORY_ID: ${{ github.repository_id }}
|
||||
run: |
|
||||
url=$(gh api \
|
||||
"repos/${GITHUB_REPOSITORY}/actions/artifacts?name=testflight-native-fingerprint&per_page=100" \
|
||||
--jq "[.artifacts[] | select(
|
||||
.expired == false and
|
||||
.workflow_run.head_branch == \"main\" and
|
||||
.workflow_run.head_repository_id == (\$ENV.REPOSITORY_ID | tonumber)
|
||||
)] | max_by(.created_at) | .archive_download_url" \
|
||||
2>/dev/null || true)
|
||||
|
||||
if [ -n "$url" ] && [ "$url" != "null" ]; then
|
||||
mkdir baseline-artifact
|
||||
if curl -sSL -H "Authorization: Bearer $GH_TOKEN" -o baseline.zip "$url" \
|
||||
&& unzip -q baseline.zip -d baseline-artifact; then
|
||||
if jq -e '.sources | type == "array"' \
|
||||
baseline-artifact/native-fingerprint.json >/dev/null; then
|
||||
echo "path=baseline-artifact/native-fingerprint.json" >> "$GITHUB_OUTPUT"
|
||||
else
|
||||
echo "::warning::Ignoring invalid fingerprint baseline artifact."
|
||||
fi
|
||||
else
|
||||
echo "::warning::Could not download fingerprint baseline artifact."
|
||||
fi
|
||||
fi
|
||||
|
||||
- name: 📷 Check fingerprint and install dependencies
|
||||
id: fingerprint
|
||||
uses: bluesky-social/github-actions/fingerprint-native@abc6a46eb4badf243f55bfd7d6cec42722456300 # v0.3.0
|
||||
with:
|
||||
profile: testflight
|
||||
# The recordBaseline job uploads this marker after a successful deploy;
|
||||
# on the native path, that requires both builds to succeed. A missing
|
||||
# marker forces native builds so they can seed the baseline safely.
|
||||
baseline-fingerprint-path: ${{ steps.baseline.outputs.path }}
|
||||
|
||||
# Hand the full fingerprint to recordBaseline through a short-lived
|
||||
# artifact. It is uploaded unconditionally but promoted to the persistent
|
||||
# baseline only after both native builds succeed.
|
||||
- name: 🚀 Upload native fingerprint
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
with:
|
||||
name: native-fingerprint-${{ github.run_id }}
|
||||
path: ${{ steps.fingerprint.outputs.current-fingerprint-path }}
|
||||
retention-days: 1
|
||||
if-no-files-found: error
|
||||
|
||||
- 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: 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: ☁️ Configure AWS credentials (denis)
|
||||
if: ${{ !steps.fingerprint.outputs.includes-changes &&
|
||||
!steps.version.outputs.version-changed }}
|
||||
uses: aws-actions/configure-aws-credentials@e6de054238d6b7531b4efff3b6587d9aade6a06c # v6.2.3
|
||||
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:
|
||||
CHANNEL_NAME: 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: ${{ 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: ${{ 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.
|
||||
#
|
||||
# On the native-build path, this runs only after both builds succeed. A
|
||||
# successful OTA deploy also records its fingerprint, refreshing the
|
||||
# persistent marker's retention without changing the native baseline.
|
||||
#
|
||||
# The persistent artifact replaces the old actions/cache marker without
|
||||
# requiring a PAT or mutable repository variable. Each successful deploy adds
|
||||
# an immutable marker; the next run reads the newest non-expired one using the
|
||||
# built-in GITHUB_TOKEN.
|
||||
recordBaseline:
|
||||
name: Record fingerprint baseline
|
||||
runs-on: ubuntu-latest
|
||||
needs: [bundleDeploy, buildIfNecessaryIOS, buildIfNecessaryAndroid]
|
||||
if: ${{ always() &&
|
||||
needs.bundleDeploy.result == 'success' &&
|
||||
(needs.bundleDeploy.outputs.changes-detected != 'true' ||
|
||||
(needs.buildIfNecessaryIOS.result == 'success' &&
|
||||
needs.buildIfNecessaryAndroid.result == 'success')) &&
|
||||
github.repository == 'bluesky-social/social-app' }}
|
||||
permissions:
|
||||
actions: read
|
||||
steps:
|
||||
- name: ⬇️ Download native fingerprint
|
||||
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
|
||||
with:
|
||||
name: native-fingerprint-${{ github.run_id }}
|
||||
|
||||
- name: 🧐 Validate native fingerprint
|
||||
run: >
|
||||
jq -e '.sources | type == "array"' native-fingerprint.json >/dev/null ||
|
||||
(echo "::error::native fingerprint artifact was invalid; refusing to record it as the baseline." && exit 1)
|
||||
|
||||
- name: 🚀 Record fingerprint baseline
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
with:
|
||||
name: testflight-native-fingerprint
|
||||
path: native-fingerprint.json
|
||||
retention-days: 90
|
||||
if-no-files-found: error
|
||||
Reference in New Issue
Block a user