Skip to content

Translations update from Hosted Weblate #1641

Translations update from Hosted Weblate

Translations update from Hosted Weblate #1641

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'
env:
# Declared here rather than in android/gradle.properties: llama.rn's own
# android/gradle.properties sets rnllamaBuildFromSource, and a subproject's
# value beats the root project's, so only an environment property or -P wins.
ORG_GRADLE_PROJECT_rnllamaBuildFromSource: 'true'
steps:
- name: Check out code
uses: actions/checkout@v3
# Must precede setup-node and setup-java: /opt/hostedtoolcache is where
# those install. The Hexagon SDK adds ~3.1 GB on top of a from-source build.
- name: Maximize build space
run: |
df -h /
sudo rm -rf /usr/share/dotnet /opt/ghc /opt/hostedtoolcache
df -h /
# 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: Derive the rnllama variant allowlist from the payload manifest
run: |
set -euo pipefail
VARIANTS=$(node scripts/verify-android-payload.js --print-variants)
echo "Declared allowlist: $VARIANTS"
echo "ORG_GRADLE_PROJECT_rnllamaVariants=$VARIANTS" >> "$GITHUB_ENV"
# Keyed on the manifest as well as yarn.lock: the manifest decides the
# variant set, so changing it must invalidate the configured build tree.
- name: Cache llama.rn native build tree
uses: actions/cache@v4
with:
path: node_modules/llama.rn/android/.cxx
key: ${{ runner.os }}-cxx-${{ hashFiles('yarn.lock', 'scripts/android-payload-manifest.json') }}
- name: Set up JDK 17
uses: actions/setup-java@v3
with:
java-version: '17'
distribution: 'temurin'
cache: 'gradle'
- name: Set up the Hexagon SDK
uses: ./.github/actions/setup-hexagon-sdk
- name: Set up ccache
uses: ./.github/actions/setup-ccache
- 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: |
set -euo pipefail
# Compared against "true" rather than merely checked for emptiness:
# the payload gate cannot tell the modes apart (upstream's prebuilt
# exports the same symbols), so this guard is the only thing holding
# the declaration.
if [ "${ORG_GRADLE_PROJECT_rnllamaBuildFromSource:-}" != "true" ]; then
echo "::error::ORG_GRADLE_PROJECT_rnllamaBuildFromSource is '${ORG_GRADLE_PROJECT_rnllamaBuildFromSource:-<unset>}', expected 'true'. Building from prebuilts is the documented escape hatch, but it must be a deliberate edit to this workflow."
exit 1
fi
./gradlew assembleProdRelease 2>&1 | tee android-build.log
- name: ccache statistics
if: always()
run: ccache --show-stats || echo "ccache unavailable (its setup step did not complete)"
# COUPLING: the string grepped for below is upstream's, printed by
# node_modules/llama.rn/android/build.gradle next to its rnllamaVariants
# lookup. A llama.rn upgrade that rewords it fails this step. That is the
# intended direction — it is the only live proof the environment reaches
# gradle — but the fix is to update the literal, not to delete the check.
- name: Verify the variant allowlist reached the llama.rn build
run: |
set -euo pipefail
VARIANTS=$(node scripts/verify-android-payload.js --print-variants)
if ! grep -qF "Building rnllama variants: $VARIANTS" android/android-build.log; then
echo "::error::llama.rn did not report building the declared variant set."
echo "Expected: $VARIANTS"
echo "In the build log:"
grep -F "rnllama variants" android/android-build.log || echo " (no such line at all)"
echo ""
echo "Two different causes look identical here. Either ORG_GRADLE_PROJECT_rnllamaVariants"
echo "did not reach the llama.rn subproject, or upstream changed the wording of the line"
echo "this step greps for (android/build.gradle, the println next to rnllamaVariants)."
echo "Check the wording first — a llama.rn upgrade is the more common reason."
exit 1
fi
echo "OK: llama.rn reported the declared variant list (extras, if any, are permitted and are reported by the payload check)."
- name: Verify the Android payload
run: |
node scripts/verify-android-payload.js \
--apk android/app/build/outputs/apk/prod/release/app-prod-release.apk \
--report payload-report.txt
# The report is evidence about the artifact, not the artifact, so it
# uploads whether or not the check passed. The APK below does not.
- name: Upload payload report
if: always()
uses: actions/upload-artifact@v4
with:
name: payload-report
path: payload-report.txt
if-no-files-found: ignore
- 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