Skip to content

[Bug]: gate PalsHub buy button on real purchase eligibility, not device locale #1402

[Bug]: gate PalsHub buy button on real purchase eligibility, not device locale

[Bug]: gate PalsHub buy button on real purchase eligibility, not device locale #1402

Workflow file for this run

name: CI Pipeline
on:
pull_request:
branches:
- main
push:
branches:
- main
permissions:
contents: read
jobs:
# Detect if changes are translation-only (src/locales/ files)
detect-changes:
runs-on: ubuntu-latest
outputs:
translation_only: ${{ steps.check.outputs.translation_only }}
steps:
- name: Check out code
uses: actions/checkout@v3
with:
fetch-depth: 0
- name: Check if translation-only
id: check
run: |
if [ "${{ github.event_name }}" = "pull_request" ]; then
CHANGED=$(git diff --name-only origin/${{ github.base_ref }}...HEAD)
else
CHANGED=$(git diff --name-only HEAD~1)
fi
echo "Changed files:"
echo "$CHANGED"
# Check if ALL changed files are under src/locales/
TRANSLATION_ONLY="true"
while IFS= read -r file; do
if [ -z "$file" ]; then
continue
fi
case "$file" in
src/locales/*) ;;
*) TRANSLATION_ONLY="false"; break ;;
esac
done <<< "$CHANGED"
echo "translation_only=$TRANSLATION_ONLY" >> "$GITHUB_OUTPUT"
echo "Translation only: $TRANSLATION_ONLY"
# Job for linting, type checking, unit testing
build-and-test:
runs-on: ubuntu-latest
steps:
# Step 1: Check out the repository
- name: Check out code
uses: actions/checkout@v3
# Cache node_modules
- name: Cache node_modules
uses: actions/cache@v3
with:
path: node_modules
key: ${{ runner.os }}-node-${{ hashFiles('yarn.lock') }}
# Step 2: Set up Node.js environment
- name: Set up Node.js
uses: actions/setup-node@v3
with:
node-version: '22.21.0'
cache: 'yarn'
- name: Install Yarn
run: npm install -g yarn
# Step 3: Install dependencies and iOS Pods
- name: Install dependencies
run: yarn install
# Step 4: Validate l10n files
- name: Validate l10n files
run: node scripts/validate-l10n.js
# Step 4b: Verify font registration (I8 — every fontFamily must
# be bundled on iOS + Android + listed in Info.plist UIAppFonts).
- name: Verify font registration
run: node scripts/verify-fonts.js
# Step 5: Run linters (ESLint)
- name: Run ESLint
run: yarn lint
# Step 5: Run TypeScript type checks
- name: Run TypeScript type check
run: yarn typecheck
# Step 6: Run unit tests (Jest)
- name: Run unit tests
run: yarn test --coverage
# Job for Android build
build-android:
runs-on: ubuntu-latest
needs: [build-and-test, detect-changes]
if: needs.detect-changes.outputs.translation_only != 'true'
steps:
- name: Check out code
uses: actions/checkout@v3
# Set up Node.js environment
- name: Set up Node.js
uses: actions/setup-node@v3
with:
node-version: '22.21.0'
cache: 'yarn'
# Cache node_modules
- name: Cache node_modules
uses: actions/cache@v3
with:
path: node_modules
key: ${{ runner.os }}-node-${{ hashFiles('yarn.lock') }}
- name: Install Yarn
run: npm install -g yarn
- name: Install dependencies
run: yarn install
- name: Set up JDK 17
uses: actions/setup-java@v3
with:
java-version: '17'
distribution: 'temurin'
cache: 'gradle'
- name: Create dummy google-services.json for CI
run: |
cat > android/app/google-services.json << 'EOL'
{
"project_info": {
"project_number": "000000000000",
"project_id": "dummy-project-for-ci",
"storage_bucket": "dummy-project-for-ci.appspot.com"
},
"client": [{
"client_info": {
"mobilesdk_app_id": "1:000000000000:android:0000000000000000",
"android_client_info": {
"package_name": "com.pocketpalai"
}
},
"api_key": [{
"current_key": "dummy-api-key-for-ci-builds"
}]
}]
}
EOL
- name: Create dummy .env file for CI
run: |
cat > .env << 'EOL'
FIREBASE_FUNCTIONS_URL=https://dummy-firebase-url.com
# PalsHub/Supabase Configuration
SUPABASE_URL=https://dummy-supabase-url.supabase.co
SUPABASE_ANON_KEY=dummy-anon-key-for-ci-builds
PALSHUB_API_BASE_URL=https://dummy-palshub-api.com
# App Configuration
APP_URL=pocketpal://app
# Feature Flags (true/false)
ENABLE_PALSHUB_INTEGRATION=true
ENABLE_AUTHENTICATION=true
ENABLE_OFFLINE_MODE=true
# Google Sign-In Configuration
GOOGLE_IOS_CLIENT_ID=dummy-ios-client-id.apps.googleusercontent.com
GOOGLE_WEB_CLIENT_ID=dummy-web-client-id.apps.googleusercontent.com
EOL
- name: Create dummy release keystore for CI
working-directory: android/app
run: |
# Generate a dummy keystore for CI builds
keytool -genkeypair -v \
-storetype PKCS12 \
-keystore pocketpal-release-key.keystore \
-alias pocketpal_key_alias \
-keyalg RSA \
-keysize 2048 \
-validity 10000 \
-storepass dummy-ci-password \
-keypass dummy-ci-password \
-dname "CN=CI Build, OU=CI, O=PocketPal, L=CI, S=CI, C=US"
- name: Build Android Release
working-directory: android
env:
APP_RELEASE_STORE_PASSWORD: dummy-ci-password
APP_RELEASE_KEY_PASSWORD: dummy-ci-password
run: ./gradlew assembleProdRelease
- name: Verify prod APK has no automation-bridge code (DCE sanity check)
run: |
set -euo pipefail
APK=android/app/build/outputs/apk/prod/release/app-prod-release.apk
echo "Checking bundle inside $APK for E2E markers..."
WORK=$(mktemp -d)
unzip -p "$APK" assets/index.android.bundle > "$WORK/bundle" || {
echo "FAIL: could not extract assets/index.android.bundle from APK"
exit 1
}
MARKERS=(
'AUTOMATION_BRIDGE'
'memory-snapshot-label'
'memory-snapshot-result'
'BENCH_RUN_MATRIX'
'bench-runner-screen-status'
)
FAIL=0
for m in "${MARKERS[@]}"; do
if grep -q -F "$m" "$WORK/bundle"; then
echo "FAIL: prod bundle contains automation marker: $m"
FAIL=1
fi
done
if [ "$FAIL" -ne 0 ]; then
echo ""
echo "The automation bridge was not DCE-stripped from the prod build."
echo "Check that __E2E__ is being replaced at build time by babel-plugin-transform-define."
echo "See src/__automation__/README.md for the DCE contract."
exit 1
fi
echo "OK: prod bundle contains zero automation markers."
- name: Upload Android APK
uses: actions/upload-artifact@v4
with:
name: android-release-apk
path: android/app/build/outputs/apk/prod/release/app-prod-release.apk
# Job for iOS build
build-ios:
runs-on: macos-26 # Keep CI on the same Xcode major as the release workflow
needs: [build-and-test, detect-changes]
if: needs.detect-changes.outputs.translation_only != 'true'
env:
NODE_OPTIONS: '--max-old-space-size=8192'
steps:
- name: Check out code
uses: actions/checkout@v3
# Set up Node.js environment
- name: Set up Node.js
uses: actions/setup-node@v3
with:
node-version: '22.21.0'
cache: 'yarn'
# Cache node_modules
- name: Cache node_modules
uses: actions/cache@v3
with:
path: node_modules
key: ${{ runner.os }}-node-${{ hashFiles('yarn.lock') }}
- name: Install Yarn
run: npm install -g yarn
- name: Install dependencies
run: yarn install
- name: Create Env.xcconfig
run: |
cat > ios/Config/Env.xcconfig << 'EOL'
// This file contains environment-specific build settings.
GOOGLE_WEB_CLIENT_ID = dummy-web-client-id.apps.googleusercontent.com
REVERSED_GOOGLE_IOS_CLIENT_ID = com.googleusercontent.apps.dummy-ios-client-id
EOL
- name: Create dummy .env file for CI
run: |
cat > .env << 'EOL'
FIREBASE_FUNCTIONS_URL=https://dummy-firebase-url.com
# PalsHub/Supabase Configuration
SUPABASE_URL=https://dummy-supabase-url.supabase.co
SUPABASE_ANON_KEY=dummy-anon-key-for-ci-builds
PALSHUB_API_BASE_URL=https://dummy-palshub-api.com
# App Configuration
APP_URL=pocketpal://app
# Feature Flags (true/false)
ENABLE_PALSHUB_INTEGRATION=true
ENABLE_AUTHENTICATION=true
ENABLE_OFFLINE_MODE=true
# Google Sign-In Configuration
GOOGLE_IOS_CLIENT_ID=dummy-ios-client-id.apps.googleusercontent.com
GOOGLE_WEB_CLIENT_ID=dummy-web-client-id.apps.googleusercontent.com
EOL
- name: Create dummy GoogleService-Info.plist for CI
run: |
cat > ios/GoogleService-Info.plist << 'EOL'
<?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>CLIENT_ID</key>
<string>dummy-client-id.apps.googleusercontent.com</string>
<key>REVERSED_CLIENT_ID</key>
<string>com.googleusercontent.apps.dummy-client-id</string>
<key>API_KEY</key>
<string>dummy-api-key-for-ci-builds</string>
<key>GCM_SENDER_ID</key>
<string>000000000000</string>
<key>PLIST_VERSION</key>
<string>1</string>
<key>BUNDLE_ID</key>
<string>ai.pocketpal</string>
<key>PROJECT_ID</key>
<string>dummy-project-for-ci</string>
<key>STORAGE_BUCKET</key>
<string>dummy-project-for-ci.appspot.com</string>
<key>IS_ADS_ENABLED</key>
<false/>
<key>IS_ANALYTICS_ENABLED</key>
<false/>
<key>IS_APPINVITE_ENABLED</key>
<false/>
<key>IS_GCM_ENABLED</key>
<true/>
<key>IS_SIGNIN_ENABLED</key>
<true/>
<key>GOOGLE_APP_ID</key>
<string>1:000000000000:ios:0000000000000000</string>
</dict>
</plist>
EOL
- name: Install CocoaPods dependencies
run: |
cd ios
pod install
cd ..
- name: Select default Xcode
run: |
sudo xcode-select -s /Applications/Xcode.app
xcodebuild -version
- name: Build iOS Release
run: |
cd ios
# List available simulators for debugging
xcrun simctl list devices available
xcodebuild \
-workspace PocketPal.xcworkspace \
-scheme PocketPal \
-configuration Release \
-sdk iphonesimulator \
-destination 'generic/platform=iOS Simulator' \
CODE_SIGN_IDENTITY="" \
CODE_SIGNING_REQUIRED=NO \
CODE_SIGNING_ALLOWED=NO \
ONLY_ACTIVE_ARCH=NO \
2>&1 | tee ../ios-build.log
EXIT_CODE=${PIPESTATUS[0]}
if [ $EXIT_CODE -ne 0 ]; then
echo "::error::iOS build failed (exit $EXIT_CODE)"
echo "::group::Error context"
grep -B 5 -A 20 "error:" ../ios-build.log | tail -200 || tail -100 ../ios-build.log
echo "::endgroup::"
exit $EXIT_CODE
fi