aa06d68ca6
Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
372 lines
16 KiB
YAML
372 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
|
|
|
|
# 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: |
|
|
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@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@0ebf47130e4866e96fce0953f49152a61190b271 # v6.0.9
|
|
|
|
- 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
|
|
|
|
# Pin ONE bundle version for both publishes below. Each script used to call
|
|
# `date +%s` itself, so the same bytes reached denis and ota1 under versions
|
|
# seconds apart (observed: 1785102575 vs 1785102614). The version is part of
|
|
# the asset URL path, so each origin then served a manifest referencing a
|
|
# path only it had -- meaning a manifest fetched from one origin and assets
|
|
# fetched from the other 404. Both scripts fall back to `date +%s` when this
|
|
# is unset, so single-publisher callers are unaffected.
|
|
- name: 🔢 Pin bundle version
|
|
if: ${{ !steps.fingerprint.outputs.includes-changes &&
|
|
!steps.version.outputs.version-changed }}
|
|
run: echo "BUNDLE_VERSION=$(date +%s)" >> "$GITHUB_ENV"
|
|
|
|
# denis on EKS has been the sole origin for updates.bsky.app since
|
|
# 2026-07-26, so it publishes FIRST: it is the path that actually serves
|
|
# clients. The legacy ota1 upload runs after it, and exists only so that
|
|
# rolling the Bunny origin back to ota1 would find current bundles there.
|
|
#
|
|
# The ordering is load-bearing, not cosmetic. While the legacy step ran
|
|
# first, its failure skipped these steps and nothing reached EITHER origin
|
|
# -- the dual-write took down the working path with it. Both steps are
|
|
# still required to pass, so a stale ota1 remains a loud failure, but the
|
|
# publish that serves users has already landed before the legacy one can
|
|
# fail.
|
|
#
|
|
# Both halves are removed together when ota1 is decommissioned (Phase 5).
|
|
- name: ☁️ Configure AWS credentials (denis)
|
|
if: ${{ !steps.fingerprint.outputs.includes-changes &&
|
|
!steps.version.outputs.version-changed }}
|
|
uses: aws-actions/configure-aws-credentials@517a711dbcd0e402f90c77e7e2f81e849156e31d # v6.2.2
|
|
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' }}
|
|
|
|
- name: 📦 Package Bundle and 🚀 Deploy (legacy ota1)
|
|
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' }}
|
|
|
|
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.
|
|
#
|
|
# 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
|