Skip to content

Version bump to 1.0.20 #27

Version bump to 1.0.20

Version bump to 1.0.20 #27

Workflow file for this run

name: Release
on:
push:
tags:
- "v*"
permissions:
contents: write
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 iOS
runs-on: macos-26
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 }}
steps:
- name: Checkout repository
uses: actions/checkout@v5
- name: Select Xcode 26
run: |
# Prefer Xcode 26.3 because 26.4.1 currently crashes the Swift frontend
# while compiling ExpoModulesCore during archive on GitHub's macOS 26 image.
if [ -d /Applications/Xcode_26.3.app ]; then
BEST=/Applications/Xcode_26.3.app
elif [ -d /Applications/Xcode_26.3.0.app ]; then
BEST=/Applications/Xcode_26.3.0.app
else
BEST=""
fi
# If 26.3 is unavailable, fall back to the latest stable (non-beta)
# Xcode 26 on the runner. We resolve symlinks because some aliases
# don't contain "beta" in their name but point to a beta install.
for app in $(ls -d /Applications/Xcode_26*.app 2>/dev/null | sort -V); do
if [ -n "$BEST" ]; then
break
fi
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.ipa
- name: Upload iOS artifact
uses: actions/upload-artifact@v5
with:
name: ios-ipa-${{ github.ref_name }}
path: cyd-mobile.ipa
if-no-files-found: error
build_ios_alt:
name: Build iOS (alt store)
runs-on: macos-26
needs: quality
env:
APP_VARIANT: production
CYD_API_ENV: prod
IOS_DISTRIBUTION: alt
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 }}
steps:
- name: Checkout repository
uses: actions/checkout@v5
- name: Select Xcode 26
run: |
# Prefer Xcode 26.3 because 26.4.1 currently crashes the Swift frontend
# while compiling ExpoModulesCore during archive on GitHub's macOS 26 image.
if [ -d /Applications/Xcode_26.3.app ]; then
BEST=/Applications/Xcode_26.3.app
elif [ -d /Applications/Xcode_26.3.0.app ]; then
BEST=/Applications/Xcode_26.3.0.app
else
BEST=""
fi
# If 26.3 is unavailable, fall back to the latest stable (non-beta)
# Xcode 26 on the runner. We resolve symlinks because some aliases
# don't contain "beta" in their name but point to a beta install.
for app in $(ls -d /Applications/Xcode_26*.app 2>/dev/null | sort -V); do
if [ -n "$BEST" ]; then
break
fi
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 alt store locally with Xcode
run: |
set -euo pipefail
ARCHIVE_PATH="$RUNNER_TEMP/CydAlt.xcarchive"
EXPORT_DIR="$RUNNER_TEMP/ios-alt-export"
EXPORT_OPTS="$RUNNER_TEMP/ExportOptionsAlt.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 alt iOS build"
exit 1
fi
cp "$IOS_BUILD_FILE" cyd-mobile-alt.ipa
- name: Upload iOS alt artifact
uses: actions/upload-artifact@v5
with:
name: ios-alt-ipa-${{ github.ref_name }}
path: cyd-mobile-alt.ipa
if-no-files-found: error
submit_ios:
name: Submit iOS to App Store
runs-on: macos-latest
needs: [build_ios, build_android]
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-ipa-${{ github.ref_name }}
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.ipa
submit_ios_alt:
name: Submit iOS (alt store) to App Store
runs-on: macos-latest
needs: [build_ios_alt, build_android]
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 alt artifact
uses: actions/download-artifact@v5
with:
name: ios-alt-ipa-${{ github.ref_name }}
path: dist/ios-alt
- name: Restore App Store Connect API private key
run: python3 ./scripts/ci/restore-asc-private-key.py
- name: Submit iOS alt build
run: ./scripts/ci/submit-ios.sh dist/ios-alt/cyd-mobile-alt.ipa
build_android:
name: Build Android
runs-on: ubuntu-latest-android-builder
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 }}
ANDROID_KEY_PASSWORD: ${{ secrets.ANDROID_KEY_PASSWORD }}
steps:
- name: Checkout repository
uses: actions/checkout@v5
- name: Setup Node.js
uses: actions/setup-node@v5
with:
node-version: "24"
cache: "npm"
- name: Setup Java
uses: actions/setup-java@v5
with:
distribution: temurin
java-version: "17"
- name: Setup Android SDK
uses: android-actions/setup-android@v3
- name: Install dependencies
run: npm ci
- name: Generate native iOS/Android projects
run: npm run prebuild
- name: Decode upload keystore
run: |
echo "$ANDROID_KEYSTORE_BASE64" | base64 --decode > android/app/upload.jks
- name: Configure release signing
run: |
python3 ./scripts/ci/configure-android-signing.py
echo "--- Signing config in build.gradle ---"
grep -A5 'signingConfigs' android/app/build.gradle | head -25
- name: Tune Gradle for CI runner
run: |
cat >> android/gradle.properties <<EOF
# Upload keystore credentials
UPLOAD_STORE_PASSWORD=$ANDROID_KEYSTORE_PASSWORD
UPLOAD_KEY_ALIAS=$ANDROID_KEY_ALIAS
UPLOAD_KEY_PASSWORD=$ANDROID_KEY_PASSWORD
EOF
cat >> android/gradle.properties <<'EOF'
# CI overrides for 8-core / 32GB runner
org.gradle.jvmargs=-Xmx6g -XX:MaxMetaspaceSize=1g -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8
org.gradle.workers.max=8
org.gradle.caching=true
org.gradle.daemon=false
EOF
- name: Cache Gradle dependencies
uses: actions/cache@v5
with:
path: |
~/.gradle/caches
~/.gradle/wrapper
key: gradle-${{ runner.os }}-${{ hashFiles('android/gradle/wrapper/gradle-wrapper.properties', 'android/build.gradle', 'android/app/build.gradle') }}
restore-keys: |
gradle-${{ runner.os }}-
- name: Build Android locally with Gradle
run: |
export ANDROID_HOME="$ANDROID_SDK_ROOT"
cd android
chmod +x ./gradlew
./gradlew bundleRelease assembleRelease --build-cache --parallel --max-workers=8
cd ..
ANDROID_BUNDLE_FILE=android/app/build/outputs/bundle/release/app-release.aab
ANDROID_APK_FILE=android/app/build/outputs/apk/release/app-release.apk
if [ ! -f "$ANDROID_BUNDLE_FILE" ]; then
echo "No .aab file found after local Android build"
exit 1
fi
if [ ! -f "$ANDROID_APK_FILE" ]; then
echo "No .apk file found after local Android build"
exit 1
fi
cp "$ANDROID_BUNDLE_FILE" cyd-mobile.aab
cp "$ANDROID_APK_FILE" cyd-mobile.apk
- name: Upload Android artifact
uses: actions/upload-artifact@v5
with:
name: android-aab-${{ github.ref_name }}
path: cyd-mobile.aab
if-no-files-found: error
- name: Upload Android APK artifact
uses: actions/upload-artifact@v5
with:
name: android-apk-${{ github.ref_name }}
path: cyd-mobile.apk
if-no-files-found: error
upload_release_assets:
name: Upload APK to GitHub Release
runs-on: ubuntu-latest
needs: [build_android]
steps:
- name: Checkout repository
uses: actions/checkout@v5
- name: Download Android APK artifact
uses: actions/download-artifact@v5
with:
name: android-apk-${{ github.ref_name }}
path: dist/android
- name: Upload release assets
uses: softprops/action-gh-release@v2
with:
files: |
dist/android/cyd-mobile.apk
fail_on_unmatched_files: true
submit_android:
name: Submit Android to Google Play
runs-on: ubuntu-latest
needs: [build_ios, build_android]
env:
GOOGLE_PLAY_SERVICE_ACCOUNT_JSON_BASE64: ${{ secrets.GOOGLE_PLAY_SERVICE_ACCOUNT_JSON_BASE64 }}
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: Download Android artifact
uses: actions/download-artifact@v5
with:
name: android-aab-${{ github.ref_name }}
path: dist/android
- name: Restore Google Play service account key
run: python3 ./scripts/ci/restore-google-play-service-account.py
- name: Submit Android build
run: node ./scripts/ci/submit-android.js dist/android/cyd-mobile.aab