Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,9 @@ jobs:
needs: quality
env:
APP_VARIANT: production
CYD_API_ENV: prod
IOS_DISTRIBUTION: app_store
APP_STORE_ANNUAL_PRODUCT_ID: premium_annual
ASC_API_KEY_ID: ${{ secrets.ASC_API_KEY_ID }}
ASC_API_ISSUER_ID: ${{ secrets.ASC_API_ISSUER_ID }}
ASC_API_PRIVATE_KEY_BASE64: ${{ secrets.ASC_API_PRIVATE_KEY_BASE64 }}
Expand Down Expand Up @@ -214,6 +217,7 @@ jobs:
needs: quality
env:
APP_VARIANT: production
CYD_API_ENV: prod
ANDROID_KEYSTORE_BASE64: ${{ secrets.ANDROID_KEYSTORE_BASE64 }}
ANDROID_KEYSTORE_PASSWORD: ${{ secrets.ANDROID_KEYSTORE_PASSWORD }}
ANDROID_KEY_ALIAS: ${{ secrets.ANDROID_KEY_ALIAS }}
Expand Down
205 changes: 205 additions & 0 deletions .github/workflows/testflight-dev.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,205 @@
name: Dev TestFlight

on:
workflow_dispatch:
inputs:
app_store_annual_product_id:
description: App Store annual subscription product ID
required: true
default: premium_annual

run-name: Dev TestFlight (${{ github.ref_name }})

permissions:
contents: read

jobs:
quality:
name: Lint and test
runs-on: ubuntu-latest

steps:
- name: Checkout repository
uses: actions/checkout@v5

- name: Setup Node.js
uses: actions/setup-node@v5
with:
node-version: "24"
cache: "npm"

- name: Install dependencies
run: npm ci

- name: Run lint
run: npm run lint

- name: Run tests
run: npm test

build_ios:
name: Build dev-backed iOS TestFlight
runs-on: macos-26-large
needs: quality
env:
APP_VARIANT: production
CYD_API_ENV: dev
IOS_DISTRIBUTION: app_store
APP_STORE_ANNUAL_PRODUCT_ID: ${{ inputs.app_store_annual_product_id }}
ASC_API_KEY_ID: ${{ secrets.ASC_API_KEY_ID }}
ASC_API_ISSUER_ID: ${{ secrets.ASC_API_ISSUER_ID }}
ASC_API_PRIVATE_KEY_BASE64: ${{ secrets.ASC_API_PRIVATE_KEY_BASE64 }}

steps:
- name: Checkout repository
uses: actions/checkout@v5

- name: Select Xcode 26
run: |
# Pick the latest stable (non-beta) Xcode 26 on the runner.
# We resolve symlinks because some aliases (e.g. Xcode_26.4.0.app)
# don't contain "beta" in their name but point to a beta install.
BEST=""
for app in $(ls -d /Applications/Xcode_26*.app 2>/dev/null | sort -V); do
REAL=$(readlink -f "$app" 2>/dev/null || realpath "$app")
if basename "$REAL" | grep -qi 'beta'; then
echo "Skipping $app (resolves to beta: $REAL)"
continue
fi
BEST="$app"
done

if [ -z "$BEST" ]; then
echo "::error::No non-beta Xcode 26 found on this runner."
ls -ld /Applications/Xcode*.app
exit 1
fi
echo "Selecting $BEST"
sudo xcode-select -s "$BEST/Contents/Developer"
xcodebuild -version

- name: Install iOS platform
run: xcodebuild -downloadPlatform iOS

- name: Setup Node.js
uses: actions/setup-node@v5
with:
node-version: "24"
cache: "npm"

- name: Install dependencies
run: npm ci

- name: Generate native projects (skip pod install)
run: npx expo prebuild --clean --no-install

- name: Configure Xcode Node binary
run: |
echo "NODE_BINARY=$(command -v node)" > ios/.xcode.env.local

- name: Cache CocoaPods
uses: actions/cache@v5
with:
path: ios/Pods
key: pods-${{ runner.os }}-${{ hashFiles('ios/Podfile.lock') }}
restore-keys: |
pods-${{ runner.os }}-

- name: Install CocoaPods dependencies
run: cd ios && pod install

- name: Restore App Store Connect API private key
run: python3 ./scripts/ci/restore-asc-private-key.py

- name: Build iOS locally with Xcode
run: |
set -euo pipefail

ARCHIVE_PATH="$RUNNER_TEMP/Cyd.xcarchive"
EXPORT_DIR="$RUNNER_TEMP/ios-export"
EXPORT_OPTS="$RUNNER_TEMP/ExportOptions.plist"

cat > "$EXPORT_OPTS" <<'PLIST'
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>method</key>
<string>app-store-connect</string>
<key>signingStyle</key>
<string>automatic</string>
<key>teamID</key>
<string>G762K6CH36</string>
<key>stripSwiftSymbols</key>
<true/>
<key>uploadSymbols</key>
<true/>
<key>manageAppVersionAndBuildNumber</key>
<true/>
</dict>
</plist>
PLIST

xcodebuild \
-workspace ios/Cyd.xcworkspace \
-scheme Cyd \
-configuration Release \
-destination 'generic/platform=iOS' \
-archivePath "$ARCHIVE_PATH" \
-allowProvisioningUpdates \
-authenticationKeyPath "$HOME/.appstoreconnect/private_keys/AuthKey_${ASC_API_KEY_ID}.p8" \
-authenticationKeyID "$ASC_API_KEY_ID" \
-authenticationKeyIssuerID "$ASC_API_ISSUER_ID" \
CODE_SIGN_IDENTITY=- \
AD_HOC_CODE_SIGNING_ALLOWED=YES \
archive

xcodebuild \
-exportArchive \
-archivePath "$ARCHIVE_PATH" \
-exportPath "$EXPORT_DIR" \
-exportOptionsPlist "$EXPORT_OPTS" \
-allowProvisioningUpdates \
-authenticationKeyPath "$HOME/.appstoreconnect/private_keys/AuthKey_${ASC_API_KEY_ID}.p8" \
-authenticationKeyID "$ASC_API_KEY_ID" \
-authenticationKeyIssuerID "$ASC_API_ISSUER_ID"

IOS_BUILD_FILE=$(ls -t "$EXPORT_DIR"/*.ipa | head -1)
if [ -z "$IOS_BUILD_FILE" ]; then
echo "No .ipa file found after local iOS build"
exit 1
fi

cp "$IOS_BUILD_FILE" cyd-mobile-dev-testflight.ipa

- name: Upload iOS artifact
uses: actions/upload-artifact@v5
with:
name: ios-dev-testflight-ipa-${{ github.run_number }}
path: cyd-mobile-dev-testflight.ipa
if-no-files-found: error

submit_ios:
name: Upload dev-backed iOS build to TestFlight
runs-on: macos-latest
needs: build_ios
env:
ASC_API_KEY_ID: ${{ secrets.ASC_API_KEY_ID }}
ASC_API_ISSUER_ID: ${{ secrets.ASC_API_ISSUER_ID }}
ASC_API_PRIVATE_KEY_BASE64: ${{ secrets.ASC_API_PRIVATE_KEY_BASE64 }}

steps:
- name: Checkout repository
uses: actions/checkout@v5

- name: Download iOS artifact
uses: actions/download-artifact@v5
with:
name: ios-dev-testflight-ipa-${{ github.run_number }}
path: dist/ios

- name: Restore App Store Connect API private key
run: python3 ./scripts/ci/restore-asc-private-key.py

- name: Submit iOS build
run: ./scripts/ci/submit-ios.sh dist/ios/cyd-mobile-dev-testflight.ipa
8 changes: 8 additions & 0 deletions app.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,10 @@ export default ({ config }: ConfigContext): ExpoConfig => {
// EAS Build sets APP_VARIANT for different build profiles
// For local development, default to "development"
const isProduction = process.env.APP_VARIANT === "production";
const cydApiEnv = process.env.CYD_API_ENV ?? (isProduction ? "prod" : "dev");
const iosDistribution = process.env.IOS_DISTRIBUTION ?? "app_store";
const appStoreAnnualProductId =
process.env.APP_STORE_ANNUAL_PRODUCT_ID ?? "premium_annual";

return {
...config,
Expand Down Expand Up @@ -64,6 +68,7 @@ export default ({ config }: ConfigContext): ExpoConfig => {
},
plugins: [
"expo-router",
"expo-iap",
[
"expo-build-properties",
{
Expand Down Expand Up @@ -115,6 +120,9 @@ export default ({ config }: ConfigContext): ExpoConfig => {
reactCompiler: true,
},
extra: {
cydApiEnv,
iosDistribution,
appStoreAnnualProductId,
router: {},
eas: {
projectId: "192811c5-4578-4acb-b55d-4da9d2a5f44a",
Expand Down
Loading