fix(network): bound every request and surface the failure #1791
Workflow file for this run
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: iOS Build | |
| on: | |
| pull_request: | |
| types: [ready_for_review, synchronize, labeled] | |
| push: | |
| branches: | |
| - development | |
| concurrency: | |
| # Keyed by ref so a PR build cannot cancel the development build, which is the | |
| # one that uploads to TestFlight. | |
| group: ios-build-${{ github.ref }} | |
| cancel-in-progress: true | |
| jobs: | |
| lint-and-test: | |
| name: Lint & Unit Tests | |
| runs-on: ubuntu-latest | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@v4 | |
| with: | |
| repository: > | |
| ${{ github.event_name == 'push' && github.ref == 'refs/heads/development' && github.repository || | |
| github.event.pull_request.head.repo.full_name }} | |
| ref: > | |
| ${{ github.event_name == 'push' && github.ref == 'refs/heads/development' && 'development' || | |
| github.event.pull_request.head.sha }} | |
| - name: Setup Node | |
| uses: actions/setup-node@v4 | |
| with: | |
| node-version: '20' | |
| cache: 'yarn' | |
| - name: Install dependencies | |
| run: yarn install --frozen-lockfile | |
| - name: Lint | |
| run: yarn lint | |
| - name: Typecheck | |
| run: yarn typecheck | |
| - name: Unit tests | |
| run: yarn test:ci | |
| ios-build: | |
| name: iOS Build | |
| needs: lint-and-test | |
| if: > | |
| (github.event_name == 'pull_request') || | |
| (github.event_name == 'push' && github.ref == 'refs/heads/development') | |
| runs-on: macos-26 | |
| steps: | |
| - uses: maxim-lobanov/setup-xcode@v1 | |
| with: | |
| xcode-version: '26.3' | |
| - name: Set Swift toolchain for Xcode 26 | |
| run: echo "TOOLCHAINS=com.apple.dt.toolchain.XcodeDefault" >> $GITHUB_ENV | |
| - name: Show host machine information | |
| run: | | |
| uname -a | |
| xcodebuild -showsdks | |
| - name: Checkout PR head | |
| uses: actions/checkout@v4 | |
| with: | |
| repository: > | |
| ${{ github.event_name == 'push' && github.ref == 'refs/heads/development' && github.repository || | |
| github.event.pull_request.head.repo.full_name }} | |
| ref: > | |
| ${{ github.event_name == 'push' && github.ref == 'refs/heads/development' && 'development' || | |
| github.event.pull_request.head.sha }} | |
| - name: Set up Ruby | |
| uses: ruby/setup-ruby@v1 | |
| with: | |
| ruby-version: "3.0.0" | |
| - name: Setup Cocoapods | |
| uses: maxim-lobanov/setup-cocoapods@v1 | |
| with: | |
| version: 1.15.2 | |
| - name: Create Google Services PLIST File | |
| env: | |
| GOOGLE_PLIST: ${{ secrets.GOOGLE_PLIST }} | |
| run: | | |
| echo "$GOOGLE_PLIST" | base64 --decode -o ./ios/GoogleService-Info.plist | |
| - name: Install the Apple certificate and provisioning profile | |
| env: | |
| BUILD_CERTIFICATE_BASE64: ${{ secrets.CERTIFICATES_P12 }} | |
| P12_PASSWORD: ${{ secrets.CERTIFICATE_PASSWORD }} | |
| BUILD_PROVISION_PROFILE_BASE64: ${{ secrets.PROVISIONING_PROFILE }} | |
| EXTEN_PROVISION_PROFILE_BASE64: ${{ secrets.EXTEN_PROVISIONING_PROFILE }} | |
| NOTIFI_PROVISION_PROFILE_BASE64: ${{ secrets.NOTIFI_PROVISIONING_PROFILE }} | |
| KEYCHAIN_PASSWORD: ${{ secrets.APPLE_APP_PASS }} | |
| EXPORT_OPTIONS_PLIST: ${{ secrets.EXPORT_OPTIONS_PLIST }} | |
| run: | | |
| CERTIFICATE_PATH=$RUNNER_TEMP/build_certificate.p12 | |
| PP_PATH=$RUNNER_TEMP/build_pp.mobileprovision | |
| EX_PATH=$RUNNER_TEMP/build_ex.mobileprovision | |
| NOT_PATH=$RUNNER_TEMP/build_not.mobileprovision | |
| KEYCHAIN_PATH=$RUNNER_TEMP/app-signing.keychain-db | |
| echo -n "$BUILD_CERTIFICATE_BASE64" | base64 --decode -o $CERTIFICATE_PATH | |
| echo -n "$BUILD_PROVISION_PROFILE_BASE64" | base64 --decode -o $PP_PATH | |
| echo -n "$EXTEN_PROVISION_PROFILE_BASE64" | base64 --decode -o $EX_PATH | |
| echo -n "$NOTIFI_PROVISION_PROFILE_BASE64" | base64 --decode -o $NOT_PATH | |
| security create-keychain -p "$KEYCHAIN_PASSWORD" $KEYCHAIN_PATH | |
| security set-keychain-settings -lut 21600 $KEYCHAIN_PATH | |
| security unlock-keychain -p "$KEYCHAIN_PASSWORD" $KEYCHAIN_PATH | |
| security import $CERTIFICATE_PATH -P "$P12_PASSWORD" -A -t cert -f pkcs12 -k $KEYCHAIN_PATH | |
| security set-key-partition-list -S apple-tool:,apple: -k "$KEYCHAIN_PASSWORD" $KEYCHAIN_PATH | |
| security list-keychain -d user -s $KEYCHAIN_PATH | |
| mkdir -p ~/Library/MobileDevice/Provisioning\ Profiles | |
| cp $PP_PATH ~/Library/MobileDevice/Provisioning\ Profiles | |
| cp $EX_PATH ~/Library/MobileDevice/Provisioning\ Profiles | |
| cp $NOT_PATH ~/Library/MobileDevice/Provisioning\ Profiles | |
| for PROVISION in `ls $RUNNER_TEMP/*.mobileprovision` | |
| do | |
| UUID=`/usr/libexec/PlistBuddy -c 'Print :UUID' /dev/stdin <<< $(security cms -D -i $PROVISION)` | |
| cp $PROVISION ~/Library/MobileDevice/Provisioning\ Profiles/$UUID.mobileprovision | |
| done | |
| - name: Verify signing certificate and provisioning profile expiry | |
| # Fails fast and loudly when the iOS Distribution cert is missing or any | |
| # provisioning profile is already expired (or expires within 7 days), so | |
| # we don't bury the real cause inside a 1500-line xcodebuild log later. | |
| env: | |
| KEYCHAIN_PATH: ${{ runner.temp }}/app-signing.keychain-db | |
| WARN_DAYS: '7' | |
| run: | | |
| set -euo pipefail | |
| echo "::group::Code signing identities" | |
| security find-identity -v -p codesigning "$KEYCHAIN_PATH" || true | |
| echo "::endgroup::" | |
| # Require at least one valid Apple/iPhone Distribution identity with a private key. | |
| if ! security find-identity -v -p codesigning "$KEYCHAIN_PATH" \ | |
| | grep -E '"(Apple|iPhone) Distribution' >/dev/null; then | |
| echo "::error title=Distribution certificate missing::No valid 'Apple Distribution' or 'iPhone Distribution' identity with a private key was found in the build keychain. Re-export the .p12 from a Mac that has both the cert and its private key, base64-encode it, and update the CERTIFICATES_P12 secret. Also confirm CERTIFICATE_PASSWORD matches the .p12 export password." | |
| exit 1 | |
| fi | |
| NOW_EPOCH=$(date -u +%s) | |
| WARN_SECS=$(( WARN_DAYS * 24 * 60 * 60 )) | |
| FAIL=0 | |
| echo "::group::Provisioning profile expiry" | |
| for PROVISION in "$RUNNER_TEMP"/*.mobileprovision; do | |
| [ -f "$PROVISION" ] || continue | |
| PLIST=$(security cms -D -i "$PROVISION") | |
| NAME=$(/usr/libexec/PlistBuddy -c 'Print :Name' /dev/stdin <<< "$PLIST" 2>/dev/null || echo "(unknown)") | |
| UUID=$(/usr/libexec/PlistBuddy -c 'Print :UUID' /dev/stdin <<< "$PLIST" 2>/dev/null || echo "(unknown)") | |
| EXPIRATION=$(/usr/libexec/PlistBuddy -c 'Print :ExpirationDate' /dev/stdin <<< "$PLIST" 2>/dev/null || echo "") | |
| if [ -z "$EXPIRATION" ]; then | |
| echo "::error title=Provisioning profile unreadable::Could not read ExpirationDate from $(basename "$PROVISION") (name='$NAME', uuid='$UUID')" | |
| FAIL=1 | |
| continue | |
| fi | |
| # PlistBuddy prints dates like "Fri Apr 10 16:43:34 GMT 2026" — parse to epoch. | |
| EXP_EPOCH=$(date -u -j -f "%a %b %d %T %Z %Y" "$EXPIRATION" +%s 2>/dev/null || echo 0) | |
| if [ "$EXP_EPOCH" = "0" ]; then | |
| echo "::error title=Provisioning profile date parse failed::Could not parse ExpirationDate '$EXPIRATION' for profile '$NAME'" | |
| FAIL=1 | |
| continue | |
| fi | |
| REMAINING=$(( EXP_EPOCH - NOW_EPOCH )) | |
| REMAINING_DAYS=$(( REMAINING / 86400 )) | |
| if [ "$REMAINING" -le 0 ]; then | |
| echo "::error title=Provisioning profile expired::'$NAME' (uuid=$UUID) expired on $EXPIRATION. Regenerate it in the Apple Developer portal and update the matching GitHub secret (PROVISIONING_PROFILE / EXTEN_PROVISIONING_PROFILE / NOTIFI_PROVISIONING_PROFILE)." | |
| FAIL=1 | |
| elif [ "$REMAINING" -le "$WARN_SECS" ]; then | |
| echo "::warning title=Provisioning profile expiring soon::'$NAME' expires in $REMAINING_DAYS day(s) on $EXPIRATION. Regenerate before the next build." | |
| else | |
| echo "OK $NAME — $REMAINING_DAYS day(s) remaining (expires $EXPIRATION)" | |
| fi | |
| done | |
| echo "::endgroup::" | |
| if [ "$FAIL" -ne 0 ]; then | |
| echo "::error::One or more provisioning profiles or signing certificates are invalid. See errors above." | |
| exit 1 | |
| fi | |
| - name: Create .env file | |
| run: | | |
| echo "ACTIVITY_WEBSOCKET_URL=${{ secrets.ACTIVITY_WEBSOCKET_URL }}" >> .env | |
| echo "SENTRY_AUTH_TOKEN=${{ secrets.SENTRY_AUTH_TOKEN }}" >> .env | |
| echo "DEFAULT_PIN=${{ secrets.DEFAULT_PIN }}" >> .env | |
| echo "ECENCY_BACKEND_API=${{ secrets.ECENCY_BACKEND_API }}" >> .env | |
| echo "NEW_IMAGE_API=${{ secrets.NEW_IMAGE_API }}" >> .env | |
| echo "PIN_KEY=${{ secrets.PIN_KEY }}" >> .env | |
| echo "USER_AGENT=${{ secrets.USER_AGENT }}" >> .env | |
| echo "PLAUSIBLE_API_KEY=${{ secrets.PLAUSIBLE_API_KEY }}" >> .env | |
| - name: Setup Node | |
| uses: actions/setup-node@v4 | |
| with: | |
| node-version: '20' | |
| - name: Install npm dependency | |
| run: yarn install | |
| - name: Install Cocoapod dependencies | |
| run: | | |
| cd ios | |
| pod install | |
| - name: Set version code | |
| run: | | |
| TIMESTAMP=$(date +%s) | |
| echo "Setting version code to: $TIMESTAMP" | |
| sed -i '' "s/CURRENT_PROJECT_VERSION = [0-9]*/CURRENT_PROJECT_VERSION = $TIMESTAMP/" ios/Ecency.xcodeproj/project.pbxproj | |
| echo "IOS_BUILD_NUMBER=$TIMESTAMP" >> $GITHUB_ENV | |
| - name: Resolve iOS Sentry release metadata | |
| run: | | |
| BUNDLE_ID=$(xcodebuild -workspace ios/Ecency.xcworkspace -scheme Ecency -configuration Release -showBuildSettings | awk '/ PRODUCT_BUNDLE_IDENTIFIER / {print $3; exit}') | |
| VERSION=$(node -p "require('./package.json').version") | |
| DIST="${IOS_BUILD_NUMBER}" | |
| RELEASE="${BUNDLE_ID}@${VERSION}+${DIST}" | |
| echo "SENTRY_RELEASE=$RELEASE" >> $GITHUB_ENV | |
| echo "SENTRY_DIST=$DIST" >> $GITHUB_ENV | |
| echo "Using Sentry release: $RELEASE" | |
| echo "Using Sentry dist: $DIST" | |
| - name: Build iOS JS Bundle | |
| run: | | |
| yarn run bundle:ios | |
| - name: Copy JS bundle and sourcemap to temp directory | |
| run: | | |
| mkdir -p $RUNNER_TEMP/js | |
| cp ios/main.jsbundle $RUNNER_TEMP/js/main.jsbundle | |
| cp ios/main.jsbundle.map $RUNNER_TEMP/js/main.jsbundle.map | |
| # Source maps are only used for error symbolication, so a transient Sentry | |
| # API error (e.g. HTTP 500) must not block the release build. The shared | |
| # retry helper retries transient failures; continue-on-error lets a | |
| # sustained Sentry outage degrade gracefully instead of failing the build. | |
| - name: Upload source maps to Sentry (iOS) | |
| continue-on-error: true | |
| env: | |
| SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }} | |
| run: | | |
| retry="$GITHUB_WORKSPACE/.github/scripts/sentry-retry.sh" | |
| bash "$retry" npx sentry-cli releases new "$SENTRY_RELEASE" --org ecency --project ecency-mobile | |
| bash "$retry" npx sentry-cli releases files "$SENTRY_RELEASE" upload-sourcemaps \ | |
| --dist "$SENTRY_DIST" \ | |
| --org ecency --project ecency-mobile \ | |
| --rewrite \ | |
| --strip-common-prefix \ | |
| $RUNNER_TEMP/js/main.jsbundle $RUNNER_TEMP/js/main.jsbundle.map | |
| bash "$retry" npx sentry-cli releases finalize "$SENTRY_RELEASE" --org ecency --project ecency-mobile | |
| - name: Build archive | |
| run: | | |
| cd ios | |
| xcodebuild -workspace Ecency.xcworkspace \ | |
| -scheme "Ecency" \ | |
| -sdk iphoneos \ | |
| -configuration Release \ | |
| -destination generic/platform=iOS \ | |
| -archivePath $RUNNER_TEMP/Ecency.xcarchive \ | |
| clean archive | |
| - name: Upload iOS dSYMs to Sentry | |
| continue-on-error: true | |
| env: | |
| SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }} | |
| run: | | |
| retry="$GITHUB_WORKSPACE/.github/scripts/sentry-retry.sh" | |
| bash "$retry" npx sentry-cli debug-files upload \ | |
| --org ecency \ | |
| --project ecency-mobile \ | |
| --include-sources \ | |
| "$RUNNER_TEMP/Ecency.xcarchive/dSYMs" | |
| - name: Export IPA | |
| env: | |
| EXPORT_OPTIONS_PLIST: ${{ secrets.EXPORT_OPTIONS_PLIST }} | |
| run: | | |
| EXPORT_OPTS_PATH=$RUNNER_TEMP/ExportOptions.plist | |
| echo -n "$EXPORT_OPTIONS_PLIST" | base64 --decode -o $EXPORT_OPTS_PATH | |
| xcodebuild -exportArchive \ | |
| -archivePath $RUNNER_TEMP/Ecency.xcarchive \ | |
| -exportOptionsPlist $EXPORT_OPTS_PATH \ | |
| -exportPath $RUNNER_TEMP/build | |
| - name: List build contents | |
| run: | | |
| ls -la $RUNNER_TEMP/build | |
| - name: Upload App | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: Ecency-iOS | |
| path: ${{ runner.temp }}/build/Ecency.ipa | |
| retention-days: 90 | |
| ios-deploy: | |
| name: Deploy iOS Build | |
| needs: ios-build | |
| # Only deploy if the trigger is a pull_request with 'deploy-pr-build' label or a push to 'development' | |
| if: > | |
| (github.event_name == 'pull_request' && contains(github.event.pull_request.labels.*.name, 'deploy-pr-build')) || | |
| (github.event_name == 'push' && github.ref == 'refs/heads/development') | |
| runs-on: macos-latest | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - name: Get iOS Build from artifacts | |
| uses: actions/download-artifact@v4 | |
| - name: Upload to AppStoreConnect | |
| env: | |
| APPLE_ID: ${{ secrets.APPLE_ID }} | |
| APPLE_APP_PASS: ${{ secrets.APPLE_APP_PASS }} | |
| run: xcrun altool --upload-app -f "Ecency-iOS/Ecency.ipa" -t ios -u "$APPLE_ID" -p "$APPLE_APP_PASS" |