08d7946c49
Native nightly builds are redundant now that changes ship over the air, so the nightly workflow no longer builds anything. It now only posts the list of commits since the previous post to the QA team's Slack channel, and posts nothing when there are no new commits. The commit marker artifact keeps its name so the first run continues from the last nightly. The OTA workflow's fallback native iOS build is assigned to the "QA Team" TestFlight group so testers still get native builds when one is needed. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0157o74F6wX3aTghhc7DLbzd
363 lines
16 KiB
YAML
363 lines
16 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
|
|
iosBuildNumber:
|
|
type: string
|
|
description: iOS build number of the native build this update targets
|
|
(required for production)
|
|
androidVersionCode:
|
|
type: string
|
|
description: Android version code of the native build this update
|
|
targets (required for production)
|
|
|
|
# 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; 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: 🔑 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: |
|
|
[[ "$RUNTIME_VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]] && echo "Version is valid" || exit 1
|
|
|
|
# Production OTAs are bound to the specific native build they target, so
|
|
# the build numbers must be entered manually rather than read from the
|
|
# global EAS counters, which advance with every testflight build and so
|
|
# point past older production releases
|
|
- name: 🧐 Validate build numbers
|
|
if: ${{ inputs.channel == 'production' }}
|
|
env:
|
|
IOS_BUILD_NUMBER: ${{ inputs.iosBuildNumber }}
|
|
ANDROID_VERSION_CODE: ${{ inputs.androidVersionCode }}
|
|
run: |
|
|
[[ "$IOS_BUILD_NUMBER" =~ ^[0-9]+$ ]] ||
|
|
(echo "A numeric iosBuildNumber is required for production updates" && exit 1)
|
|
[[ "$ANDROID_VERSION_CODE" =~ ^[0-9]+$ ]] ||
|
|
(echo "A numeric androidVersionCode is required for production updates" && exit 1)
|
|
|
|
- name: ⬇️ Checkout
|
|
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
|
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@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
|
|
if: ${{ (inputs.channel || 'testflight') == 'testflight' }}
|
|
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: ${{ inputs.channel || 'testflight' }}
|
|
previous-commit-tag: ${{ inputs.runtimeVersion }}
|
|
# 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: ${{ 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: ☁️ 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:
|
|
RUNTIME_VERSION: ${{ inputs.runtimeVersion }}
|
|
CHANNEL_NAME: ${{ inputs.channel || 'testflight' }}
|
|
# When set (required for production), these take precedence over the
|
|
# global EAS counters inside the use-build-number wrapper
|
|
BSKY_IOS_BUILD_NUMBER: ${{ inputs.iosBuildNumber }}
|
|
BSKY_ANDROID_VERSION_CODE: ${{ inputs.androidVersionCode }}
|
|
|
|
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: "QA Team"
|
|
# 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.
|
|
#
|
|
# 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() &&
|
|
(inputs.channel || 'testflight') == 'testflight' &&
|
|
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
|