22ef002e3f
setGitHubOutput.sh derives the build number from `eas build:version:get`, which reads EAS's remote counter. A --local build doesn't advance that counter, but use-build-number-with-bump bakes counter+1 into the IPA — so the queried number can be one less than what actually lands in App Store Connect. That made distribute_only poll for a nonexistent build and would have announced the wrong number in Slack. Read CFBundleVersion straight from the built IPA instead, which is the value uploaded to ASC. Repoint the fastlane assignment, the workflow_call build-number output, and the production Slack message at it. Android's version code has the same drift source but is display-only there (no version-code lookup gates submission), so it's left as-is. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
272 lines
11 KiB
YAML
272 lines
11 KiB
YAML
---
|
|
name: Build and Submit iOS
|
|
|
|
on:
|
|
workflow_dispatch:
|
|
inputs:
|
|
profile:
|
|
type: choice
|
|
description: Build profile to use
|
|
options:
|
|
- testflight
|
|
- production
|
|
assignTestFlightGroup:
|
|
type: boolean
|
|
description: Assign the build to the "QA Team" TestFlight group after submitting
|
|
default: false
|
|
workflow_call:
|
|
inputs:
|
|
profile:
|
|
type: string
|
|
description: Build profile to use
|
|
required: true
|
|
assignTestFlightGroup:
|
|
type: boolean
|
|
description: Assign the build to the "QA Team" TestFlight group after submitting
|
|
default: false
|
|
releaseNotes:
|
|
type: string
|
|
description: Notes to set as the TestFlight "What to Test" changelog
|
|
required: false
|
|
default: ''
|
|
outputs:
|
|
package-version:
|
|
description: Version from package.json
|
|
value: ${{ jobs.build.outputs.package-version }}
|
|
build-number:
|
|
description: iOS build number
|
|
value: ${{ jobs.build.outputs.build-number }}
|
|
|
|
# Deploys happen via EAS using EXPO_TOKEN; the GITHUB_TOKEN only checks out code
|
|
permissions:
|
|
contents: read
|
|
|
|
jobs:
|
|
build:
|
|
if: github.repository == 'bluesky-social/social-app'
|
|
name: Build and Submit iOS
|
|
runs-on: macos-26-xlarge
|
|
concurrency:
|
|
group: ios-build
|
|
cancel-in-progress: false
|
|
outputs:
|
|
package-version: ${{ steps.get-build-info.outputs.PACKAGE_VERSION }}
|
|
build-number: ${{ steps.ipa-build-number.outputs.build-number }}
|
|
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
|
|
|
|
- name: ⬇️ Checkout
|
|
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
|
with:
|
|
fetch-depth: 5
|
|
|
|
- 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: 🪛 Setup jq
|
|
uses: dcarbone/install-jq-action@b7ef57d46ece78760b4019dbc4080a1ba2a40b45 # v3.2.0
|
|
|
|
- name: ⚙️ Install dependencies
|
|
run: pnpm install --frozen-lockfile
|
|
|
|
- name: 🔨 Setup Expo CLI
|
|
uses: expo/expo-github-action@eab7a230208c952974db8c3245cfd78402c7b385 # 9.0.0
|
|
with:
|
|
eas-version: '19.0.5'
|
|
packager: 'pnpm --allow-build=dtrace-provider'
|
|
token: ${{ secrets.EXPO_TOKEN }}
|
|
|
|
- uses: maxim-lobanov/setup-xcode@ed7a3b1fda3918c0306d1b724322adc0b8cc0a90 # v1.7.0
|
|
with:
|
|
xcode-version: "26.4"
|
|
|
|
- name: ☕️ Assert Cocoapods version
|
|
run: |
|
|
EXPECTED=1.16.2
|
|
ACTUAL=$(pod --version)
|
|
if [ "$ACTUAL" != "$EXPECTED" ]; then
|
|
echo "Expected Cocoapods $EXPECTED but runner has $ACTUAL."
|
|
echo "The version ships preinstalled with the macOS runner image: https://github.com/actions/runner-images/blob/main/images/macos/macos-26-Readme.md"
|
|
echo "If the runner image changed, update EXPECTED here or reinstall the pinned version."
|
|
exit 1
|
|
fi
|
|
|
|
- name: 💾 Cache Pods
|
|
uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
|
|
id: pods-cache
|
|
with:
|
|
path: ./ios/Pods
|
|
# We'll use the pnpm-lock.yaml for our hash since we don't yet have a Podfile.lock. Pod versions will not
|
|
# change unless the pnpm version changes as well.
|
|
key: ${{ runner.os }}-pods-${{ hashFiles('pnpm-lock.yaml') }}
|
|
|
|
- name: 🔤 Compile translations
|
|
run: pnpm intl:build 2>&1 | tee i18n.log
|
|
|
|
- name: Check for i18n compilation errors
|
|
run: if grep -q "invalid syntax" "i18n.log"; then echo "\n\nFound compilation
|
|
errors!\n\n" && exit 1; else echo "\n\nNo compilation errors!\n\n"; fi
|
|
|
|
# EXPO_PUBLIC_ENV is handled in eas.json
|
|
- name: ✏️ Write environment variables
|
|
id: env
|
|
run: |
|
|
echo "${{ secrets.ENV_TOKEN }}" > .env
|
|
echo "EXPO_PUBLIC_RELEASE_VERSION=$(jq -r '.version' package.json)" >> .env
|
|
echo "EXPO_PUBLIC_RELEASE_VERSION=$(jq -r '.version' package.json)" >> $GITHUB_OUTPUT
|
|
echo "EXPO_PUBLIC_BUNDLE_IDENTIFIER=$(git rev-parse HEAD)" >> .env
|
|
echo "EXPO_PUBLIC_BUNDLE_IDENTIFIER=$(git rev-parse HEAD)" >> $GITHUB_OUTPUT
|
|
echo "EXPO_PUBLIC_BUNDLE_DATE=$(date -u +"%y%m%d%H")" >> .env
|
|
echo "EXPO_PUBLIC_SENTRY_DSN=${{ secrets.SENTRY_DSN }}" >> .env
|
|
echo "EXPO_PUBLIC_BITDRIFT_API_KEY=${{ secrets.BITDRIFT_API_KEY }}" >> .env
|
|
echo "EXPO_PUBLIC_GCP_PROJECT_ID=${{ secrets.EXPO_PUBLIC_GCP_PROJECT_ID }}" >> .env
|
|
echo "${{ secrets.GOOGLE_SERVICES_TOKEN }}" > google-services.json
|
|
|
|
- name: 🏗️ EAS Build
|
|
env:
|
|
PROFILE: ${{ inputs.profile || 'testflight' }}
|
|
run: >
|
|
SENTRY_AUTH_TOKEN=${{ secrets.SENTRY_AUTH_TOKEN }}
|
|
SENTRY_RELEASE=${{ steps.env.outputs.EXPO_PUBLIC_RELEASE_VERSION }}
|
|
SENTRY_DIST=${{ steps.env.outputs.EXPO_PUBLIC_BUNDLE_IDENTIFIER }}
|
|
pnpm use-build-number-with-bump
|
|
pnpm eas build -p ios
|
|
--profile $PROFILE
|
|
--local --output build.tar.gz --non-interactive
|
|
|
|
- name: 📂 Extract build artifact
|
|
run: |
|
|
if [ -f "build.tar.gz" ]; then
|
|
echo "Extracting build.tar.gz..."
|
|
rm -rf ios-build
|
|
mkdir -p ios-build
|
|
tar -xzf build.tar.gz -C ios-build
|
|
echo "Extraction completed successfully"
|
|
|
|
echo ""
|
|
echo "Top-level extracted files:"
|
|
find ios-build -maxdepth 3 -print
|
|
|
|
echo ""
|
|
echo "Searching for IPA..."
|
|
IPA_PATH="$(find ios-build -type f -name '*.ipa' -print -quit)"
|
|
if [ -z "$IPA_PATH" ]; then
|
|
echo "ERROR: No .ipa found anywhere under ios-build."
|
|
echo "Archive contents:"
|
|
tar -tzf build.tar.gz | sed -n '1,200p'
|
|
exit 1
|
|
fi
|
|
|
|
BUILD_DIR="$(dirname "$IPA_PATH")"
|
|
echo "Found IPA at: $IPA_PATH"
|
|
echo "Build dir: $BUILD_DIR"
|
|
echo ""
|
|
echo "Build dir contents:"
|
|
ls -la "$BUILD_DIR"
|
|
echo "BUILD_DIR=$BUILD_DIR" >> $GITHUB_ENV
|
|
else
|
|
echo "Archive file not found!"
|
|
exit 1
|
|
fi
|
|
|
|
- name: 🚀 Deploy
|
|
run: pnpm eas submit -p ios --non-interactive --path "$BUILD_DIR/Bluesky.ipa"
|
|
|
|
- name: 🪲 Upload dSYM to Sentry
|
|
run: >
|
|
SENTRY_ORG=blueskyweb
|
|
SENTRY_PROJECT=app
|
|
SENTRY_AUTH_TOKEN=${{ secrets.SENTRY_AUTH_TOKEN }}
|
|
pnpm sentry-cli debug-files upload "$BUILD_DIR/Bluesky.app.dSYM.zip" --include-sources
|
|
|
|
- name: 📚 Get version from package.json
|
|
id: get-build-info
|
|
run: bash scripts/setGitHubOutput.sh
|
|
|
|
# Read the build number straight from the IPA's CFBundleVersion. This is the value
|
|
# baked in at build time by use-build-number-with-bump (remote counter + 1) and the
|
|
# number that actually lands in App Store Connect. `eas build:version:get` reads the
|
|
# remote counter, which a --local build does not advance, so it can be off by one —
|
|
# using it here would make distribute_only poll for a nonexistent build.
|
|
- name: 🔢 Read build number from IPA
|
|
id: ipa-build-number
|
|
run: |
|
|
plist_dir="$(mktemp -d)"
|
|
unzip -o -q "$BUILD_DIR/Bluesky.ipa" 'Payload/*.app/Info.plist' -d "$plist_dir"
|
|
plist="$(find "$plist_dir" -name Info.plist -print -quit)"
|
|
build_number="$(/usr/libexec/PlistBuddy -c 'Print CFBundleVersion' "$plist")"
|
|
rm -rf "$plist_dir"
|
|
if [ -z "$build_number" ]; then
|
|
echo "ERROR: could not read CFBundleVersion from IPA"
|
|
exit 1
|
|
fi
|
|
echo "IPA build number: $build_number"
|
|
echo "build-number=$build_number" >> "$GITHUB_OUTPUT"
|
|
|
|
# eas submit only uploads to App Store Connect; it can't assign a build to a
|
|
# TestFlight group. fastlane's distribute_only mode skips the upload and assigns the
|
|
# already-submitted build to the group, polling until Apple finishes processing it.
|
|
# The "What to Test" changelog is supplied by the caller (e.g. the nightly workflow).
|
|
- name: 🧪 Assign build to TestFlight group
|
|
if: ${{ inputs.assignTestFlightGroup }}
|
|
env:
|
|
ASC_KEY_ID: ${{ secrets.ASC_KEY_ID }}
|
|
ASC_ISSUER_ID: ${{ secrets.ASC_ISSUER_ID }}
|
|
ASC_KEY_P8_BASE64: ${{ secrets.ASC_KEY_P8_BASE64 }}
|
|
APP_VERSION: ${{ steps.get-build-info.outputs.PACKAGE_VERSION }}
|
|
BUILD_NUMBER: ${{ steps.ipa-build-number.outputs.build-number }}
|
|
RELEASE_NOTES: ${{ inputs.releaseNotes }}
|
|
run: |
|
|
# Ensure the API key material is removed even if fastlane exits non-zero
|
|
# (the step runs under `bash -e`, which would otherwise abort before cleanup).
|
|
trap 'rm -f asc_api_key.p8 asc_api_key.json' EXIT
|
|
echo "$ASC_KEY_P8_BASE64" | base64 --decode > asc_api_key.p8
|
|
printf '{"key_id":"%s","issuer_id":"%s","key_filepath":"%s","in_house":false}' \
|
|
"$ASC_KEY_ID" "$ASC_ISSUER_ID" "$PWD/asc_api_key.p8" > asc_api_key.json
|
|
changelog_args=()
|
|
if [ -n "$RELEASE_NOTES" ]; then
|
|
changelog_args=(changelog:"$RELEASE_NOTES")
|
|
fi
|
|
fastlane run upload_to_testflight \
|
|
api_key_path:"$PWD/asc_api_key.json" \
|
|
distribute_only:true \
|
|
app_identifier:"xyz.blueskyweb.app" \
|
|
app_version:"$APP_VERSION" \
|
|
build_number:"$BUILD_NUMBER" \
|
|
"${changelog_args[@]}" \
|
|
groups:"QA Team"
|
|
|
|
- name: 🔔 Notify Slack of Production Build
|
|
if: ${{ inputs.profile == 'production' }}
|
|
uses: slackapi/slack-github-action@45a88b9581bfab2566dc881e2cd66d334e621e2c # v3.0.3
|
|
with:
|
|
webhook: ${{ secrets.SLACK_CLIENT_ALERT_WEBHOOK }}
|
|
webhook-type: incoming-webhook
|
|
payload-templated: true
|
|
payload: |
|
|
{"text": "iOS production build for App Store submission is ready!\n```Artifact: Check TestFlight to know when it is available\nVersion Number: ${{ steps.get-build-info.outputs.PACKAGE_VERSION }}\nBuild Number: ${{ steps.ipa-build-number.outputs.build-number }}```"}
|
|
|
|
- name: ⬇️ Restore Cache
|
|
id: get-base-commit
|
|
uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
|
|
if: ${{ inputs.profile == 'testflight' }}
|
|
with:
|
|
path: most-recent-testflight-commit.txt
|
|
key: most-recent-testflight-commit
|
|
|
|
- name: ✏️ Write commit hash to cache
|
|
env:
|
|
GITHUB_SHA: ${{ github.sha }}
|
|
if: ${{ inputs.profile == 'testflight' }}
|
|
run: echo $GITHUB_SHA > most-recent-testflight-commit.txt
|