Add Metro configuration to resolve native-only React Native APIs with web-compatible shims. This enables packages like react-native-pager-view to work on the web platform by providing shim implementations for codegenNativeComponent, codegenNativeCommands, and CodegenTypes that are unavailable in react-native-web. Also simplify Android signing configuration in CI by using standard Gradle property names (MYAPP_UPLOAD_*) and remove the build.gradle patching step.
385 lines
14 KiB
YAML
385 lines
14 KiB
YAML
name: Frontend CI
|
|
|
|
on:
|
|
push:
|
|
branches:
|
|
- main
|
|
- master
|
|
- develop
|
|
- dev
|
|
pull_request:
|
|
branches:
|
|
- main
|
|
- master
|
|
workflow_dispatch:
|
|
inputs:
|
|
publish_ota:
|
|
description: 'Publish Android OTA'
|
|
required: false
|
|
default: 'true'
|
|
|
|
env:
|
|
REGISTRY: code.littlelan.cn
|
|
IMAGE_NAME: with_you/frontend-web
|
|
OTA_PLATFORM: android
|
|
OTA_PUBLISH_URL: https://updates.littlelan.cn/admin/publish
|
|
OTA_MANIFEST_URL: https://updates.littlelan.cn/api/manifest
|
|
|
|
jobs:
|
|
ota-android:
|
|
runs-on: ubuntu-latest
|
|
if: github.event_name != 'pull_request' && (github.event_name != 'workflow_dispatch' || github.event.inputs.publish_ota == 'true')
|
|
permissions:
|
|
contents: read
|
|
steps:
|
|
- name: Checkout code
|
|
uses: actions/checkout@v4
|
|
|
|
- name: Set up Node
|
|
uses: actions/setup-node@v4
|
|
with:
|
|
node-version: '25.6.1'
|
|
cache: 'npm'
|
|
|
|
- name: Install dependencies
|
|
run: npm ci
|
|
|
|
- name: Resolve runtime version
|
|
id: runtime
|
|
run: |
|
|
RUNTIME_VERSION="$(node -p "require('./app.json').expo.version")"
|
|
echo "runtime_version=${RUNTIME_VERSION}" >> "$GITHUB_OUTPUT"
|
|
echo "Resolved runtimeVersion: ${RUNTIME_VERSION}"
|
|
|
|
- name: Export Android update bundle
|
|
run: |
|
|
rm -rf dist-android dist-android-update.zip
|
|
npx expo export --platform android --output-dir dist-android
|
|
npx expo config --type public --json > dist-android/expoConfig.json
|
|
|
|
- name: Archive Android update bundle
|
|
run: |
|
|
python - <<'PY'
|
|
import os
|
|
import zipfile
|
|
|
|
with zipfile.ZipFile('dist-android-update.zip', 'w', zipfile.ZIP_DEFLATED) as zf:
|
|
for root, _, files in os.walk('dist-android'):
|
|
for name in files:
|
|
src = os.path.join(root, name)
|
|
arc = os.path.relpath(src, 'dist-android')
|
|
zf.write(src, arc)
|
|
PY
|
|
|
|
- name: Publish OTA
|
|
env:
|
|
OTA_AUTH_TOKEN: ${{ secrets.OTA_AUTH_TOKEN }}
|
|
run: |
|
|
test -n "${OTA_AUTH_TOKEN}" || (echo "Missing secret OTA_AUTH_TOKEN" && exit 1)
|
|
curl -fSs -X POST "${OTA_PUBLISH_URL}?runtimeVersion=${{ steps.runtime.outputs.runtime_version }}&platform=${OTA_PLATFORM}" \
|
|
-H "Authorization: Bearer ${OTA_AUTH_TOKEN}" \
|
|
-H "Content-Type: application/zip" \
|
|
--data-binary @"dist-android-update.zip"
|
|
|
|
- name: Verify OTA manifest
|
|
run: |
|
|
REMOTE="$(curl -sS "${OTA_MANIFEST_URL}" \
|
|
-H "expo-platform: ${OTA_PLATFORM}" \
|
|
-H "expo-runtime-version: ${{ steps.runtime.outputs.runtime_version }}" \
|
|
-H "expo-protocol-version: 1" \
|
|
| python -c "import sys, json, re; s=sys.stdin.read(); m=re.search(r'\\{\\\"id\\\":.*\\\"extra\\\":\\{.*\\}\\}', s); j=json.loads(m.group(0)); print(j['launchAsset']['url'].split('/')[-1].split('&')[0])")"
|
|
LOCAL="$(python -c "import json; m=json.load(open('dist-android/metadata.json')); print(m['fileMetadata']['android']['bundle'].split('/')[-1])")"
|
|
echo "Remote bundle: ${REMOTE}"
|
|
echo "Local bundle: ${LOCAL}"
|
|
test "${REMOTE}" = "${LOCAL}"
|
|
|
|
build-android-apk:
|
|
runs-on: android-builder
|
|
container:
|
|
image: reactnativecommunity/react-native-android:latest
|
|
env:
|
|
GRADLE_OPTS: "-Dorg.gradle.daemon=false -Dorg.gradle.parallel=true -Dorg.gradle.workers.max=4 -Xmx8g -XX:MaxMetaspaceSize=1g -XX:+UseG1GC -XX:SoftRefLRUPolicyMSPerMB=0 -XX:ReservedCodeCacheSize=512m"
|
|
_JAVA_OPTIONS: "-Xmx8g -XX:MaxMetaspaceSize=1g -XX:+UseG1GC -XX:SoftRefLRUPolicyMSPerMB=0 -XX:ReservedCodeCacheSize=512m"
|
|
NODE_OPTIONS: "--max-old-space-size=8192"
|
|
NODE_ENV: "production"
|
|
NDK_NUM_JOBS: "4"
|
|
CMAKE_BUILD_PARALLEL_LEVEL: "4"
|
|
GRADLE_USER_HOME: /root/.gradle
|
|
permissions:
|
|
contents: read
|
|
steps:
|
|
- name: Checkout code
|
|
uses: actions/checkout@v4
|
|
|
|
- name: Set up Java
|
|
uses: actions/setup-java@v4
|
|
with:
|
|
distribution: temurin
|
|
java-version: '21'
|
|
|
|
- name: Set up Node
|
|
uses: actions/setup-node@v4
|
|
with:
|
|
node-version: '25.6.1'
|
|
cache: 'npm'
|
|
|
|
- name: Cache Gradle
|
|
uses: actions/cache@v4
|
|
with:
|
|
path: |
|
|
~/.gradle/caches
|
|
~/.gradle/wrapper
|
|
~/.android/build-cache
|
|
key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties', '**/gradle.properties') }}
|
|
restore-keys: |
|
|
${{ runner.os }}-gradle-
|
|
|
|
- name: Cache node_modules
|
|
uses: actions/cache@v4
|
|
id: cache-node-modules
|
|
with:
|
|
path: node_modules
|
|
key: ${{ runner.os }}-node-modules-${{ hashFiles('package-lock.json') }}
|
|
restore-keys: |
|
|
${{ runner.os }}-node-modules-
|
|
|
|
- name: Install dependencies
|
|
if: steps.cache-node-modules.outputs.cache-hit != 'true'
|
|
run: npm ci
|
|
|
|
- name: Generate Android native project
|
|
run: npx expo prebuild --platform android
|
|
|
|
- name: Decode Android signing keystore
|
|
run: |
|
|
echo "${{ secrets.ANDROID_KEYSTORE_BASE64 }}" | base64 -d > android/app/withyou-release-key.keystore
|
|
|
|
- name: Configure Gradle with Aliyun Maven mirror
|
|
run: |
|
|
cd android
|
|
|
|
# Update settings.gradle with Aliyun Maven mirror
|
|
cat > settings.gradle << 'SETTINGS_EOF'
|
|
pluginManagement {
|
|
repositories {
|
|
google()
|
|
mavenCentral()
|
|
gradlePluginPortal()
|
|
maven { url 'https://maven.aliyun.com/repository/public' }
|
|
maven { url 'https://maven.aliyun.com/repository/google' }
|
|
maven { url 'https://maven.aliyun.com/repository/central' }
|
|
maven { url 'https://maven.aliyun.com/repository/gradle-plugin' }
|
|
}
|
|
def reactNativeGradlePlugin = new File(
|
|
providers.exec {
|
|
workingDir(rootDir)
|
|
commandLine("node", "--print", "require.resolve('@react-native/gradle-plugin/package.json', { paths: [require.resolve('react-native/package.json')] })")
|
|
}.standardOutput.asText.get().trim()
|
|
).getParentFile().absolutePath
|
|
includeBuild(reactNativeGradlePlugin)
|
|
|
|
def expoPluginsPath = new File(
|
|
providers.exec {
|
|
workingDir(rootDir)
|
|
commandLine("node", "--print", "require.resolve('expo-modules-autolinking/package.json', { paths: [require.resolve('expo/package.json')] })")
|
|
}.standardOutput.asText.get().trim(),
|
|
"../android/expo-gradle-plugin"
|
|
).absolutePath
|
|
includeBuild(expoPluginsPath)
|
|
}
|
|
|
|
plugins {
|
|
id("com.facebook.react.settings")
|
|
id("expo-autolinking-settings")
|
|
}
|
|
|
|
extensions.configure(com.facebook.react.ReactSettingsExtension) { ex ->
|
|
if (System.getenv('EXPO_USE_COMMUNITY_AUTOLINKING') == '1') {
|
|
ex.autolinkLibrariesFromCommand()
|
|
} else {
|
|
ex.autolinkLibrariesFromCommand(expoAutolinking.rnConfigCommand)
|
|
}
|
|
}
|
|
expoAutolinking.useExpoModules()
|
|
|
|
rootProject.name = '威友'
|
|
|
|
expoAutolinking.useExpoVersionCatalog()
|
|
|
|
include ':app'
|
|
includeBuild(expoAutolinking.reactNativeGradlePlugin)
|
|
SETTINGS_EOF
|
|
|
|
# Update build.gradle with Aliyun Maven mirror
|
|
cat > build.gradle << 'BUILD_EOF'
|
|
// Top-level build file where you can add configuration options common to all sub-projects/modules.
|
|
|
|
buildscript {
|
|
repositories {
|
|
google()
|
|
mavenCentral()
|
|
maven { url 'https://maven.aliyun.com/repository/public' }
|
|
maven { url 'https://maven.aliyun.com/repository/google' }
|
|
maven { url 'https://maven.aliyun.com/repository/central' }
|
|
maven { url 'https://maven.aliyun.com/repository/gradle-plugin' }
|
|
}
|
|
dependencies {
|
|
classpath('com.android.tools.build:gradle')
|
|
classpath('com.facebook.react:react-native-gradle-plugin')
|
|
classpath('org.jetbrains.kotlin:kotlin-gradle-plugin')
|
|
}
|
|
}
|
|
|
|
allprojects {
|
|
repositories {
|
|
google()
|
|
mavenCentral()
|
|
maven { url 'https://www.jitpack.io' }
|
|
maven { url 'https://maven.aliyun.com/repository/public' }
|
|
maven { url 'https://maven.aliyun.com/repository/google' }
|
|
maven { url 'https://maven.aliyun.com/repository/central' }
|
|
}
|
|
}
|
|
|
|
apply plugin: "expo-root-project"
|
|
apply plugin: "com.facebook.react.rootproject"
|
|
BUILD_EOF
|
|
|
|
# Update gradle.properties
|
|
cat > gradle.properties << 'PROPS_EOF'
|
|
org.gradle.daemon=false
|
|
org.gradle.parallel=true
|
|
org.gradle.configureondemand=true
|
|
org.gradle.workers.max=4
|
|
org.gradle.jvmargs=-Xmx8g -XX:MaxMetaspaceSize=1g -XX:+UseG1GC -XX:SoftRefLRUPolicyMSPerMB=0 -XX:ReservedCodeCacheSize=512m -XX:+HeapDumpOnOutOfMemoryError
|
|
android.enableJetifier=false
|
|
android.useAndroidX=true
|
|
hermesEnabled=true
|
|
reactNativeArchitectures=arm64-v8a
|
|
newArchEnabled=true
|
|
edgeToEdgeEnabled=true
|
|
expo.gif.enabled=true
|
|
expo.webp.enabled=true
|
|
expo.webp.animated=false
|
|
expo.useLegacyPackaging=false
|
|
PROPS_EOF
|
|
|
|
- name: Configure Gradle signing properties
|
|
run: |
|
|
cd android
|
|
cat >> gradle.properties << SIGNING_PROPS
|
|
MYAPP_UPLOAD_STORE_FILE=withyou-release-key.keystore
|
|
MYAPP_UPLOAD_STORE_PASSWORD=${{ secrets.ANDROID_KEYSTORE_PASSWORD }}
|
|
MYAPP_UPLOAD_KEY_ALIAS=withyou-key
|
|
MYAPP_UPLOAD_KEY_PASSWORD=${{ secrets.ANDROID_KEY_PASSWORD }}
|
|
SIGNING_PROPS
|
|
|
|
- name: Build Android release APK (arm64 only)
|
|
run: |
|
|
cd android
|
|
chmod +x gradlew
|
|
./gradlew :app:assembleRelease -PreactNativeArchitectures=arm64-v8a --max-workers=4 --parallel
|
|
|
|
- name: Upload APK artifact
|
|
uses: actions/upload-artifact@v3
|
|
with:
|
|
name: withyou-android-release-apk
|
|
path: android/app/build/outputs/apk/release/app-release.apk
|
|
|
|
- name: Resolve runtime version
|
|
id: runtime
|
|
run: |
|
|
RUNTIME_VERSION="$(node -p "require('./app.json').expo.version")"
|
|
echo "runtime_version=${RUNTIME_VERSION}" >> "$GITHUB_OUTPUT"
|
|
echo "Resolved runtimeVersion: ${RUNTIME_VERSION}"
|
|
|
|
- name: Upload APK to Updates Server
|
|
env:
|
|
OTA_AUTH_TOKEN: ${{ secrets.OTA_AUTH_TOKEN }}
|
|
UPDATES_SERVER_URL: https://updates.littlelan.cn
|
|
run: |
|
|
test -n "${OTA_AUTH_TOKEN}" || (echo "Missing secret OTA_AUTH_TOKEN" && exit 1)
|
|
|
|
APK_PATH="android/app/build/outputs/apk/release/app-release.apk"
|
|
RUNTIME_VERSION="${{ steps.runtime.outputs.runtime_version }}"
|
|
|
|
echo "Uploading APK for runtime version: ${RUNTIME_VERSION}"
|
|
|
|
HTTP_CODE=$(curl -s -o response.json -w "%{http_code}" \
|
|
-X POST \
|
|
"${UPDATES_SERVER_URL}/admin/apk/upload?runtimeVersion=${RUNTIME_VERSION}" \
|
|
-H "Authorization: Bearer ${OTA_AUTH_TOKEN}" \
|
|
-H "Content-Type: application/vnd.android.package-archive" \
|
|
--data-binary @"${APK_PATH}")
|
|
|
|
echo "HTTP Response Code: ${HTTP_CODE}"
|
|
cat response.json
|
|
|
|
if [ "${HTTP_CODE}" -ne 200 ]; then
|
|
echo "Failed to upload APK"
|
|
exit 1
|
|
fi
|
|
|
|
echo "APK uploaded successfully!"
|
|
|
|
build-and-push-web:
|
|
runs-on: ubuntu-latest
|
|
permissions:
|
|
contents: read
|
|
packages: write
|
|
steps:
|
|
- name: Checkout code
|
|
uses: actions/checkout@v4
|
|
|
|
- name: Log in to Gitea Container Registry
|
|
if: github.event_name != 'pull_request'
|
|
uses: docker/login-action@v3
|
|
with:
|
|
registry: ${{ env.REGISTRY }}
|
|
username: ${{ secrets.GIT_USERNAME }}
|
|
password: ${{ secrets.GIT_TOKEN }}
|
|
|
|
- name: Extract metadata for Docker
|
|
id: meta
|
|
uses: docker/metadata-action@v5
|
|
with:
|
|
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
|
|
tags: |
|
|
type=raw,value=latest,enable={{is_default_branch}}
|
|
type=ref,event=branch
|
|
type=semver,pattern={{version}}
|
|
type=semver,pattern={{major}}.{{minor}}
|
|
type=sha
|
|
|
|
- name: Set up Docker Buildx
|
|
uses: docker/setup-buildx-action@v3
|
|
|
|
- name: Build and push Docker image
|
|
uses: docker/build-push-action@v5
|
|
with:
|
|
context: .
|
|
file: Dockerfile.web
|
|
push: ${{ github.event_name != 'pull_request' }}
|
|
tags: ${{ steps.meta.outputs.tags }}
|
|
labels: ${{ steps.meta.outputs.labels }}
|
|
platforms: linux/amd64
|
|
provenance: false
|
|
cache-from: type=registry,ref=code.littlelan.cn/with_you/frontend-web:buildcache
|
|
cache-to: type=registry,ref=code.littlelan.cn/with_you/frontend-web:buildcache,mode=max
|
|
|
|
- name: Show image tags
|
|
run: |
|
|
echo "Built image tags:"
|
|
echo "${{ steps.meta.outputs.tags }}"
|
|
echo "Digest: ${{ steps.meta.outputs.digest }}"
|
|
|
|
- name: Summary
|
|
if: always()
|
|
run: |
|
|
echo "## Frontend CI Summary" >> "$GITHUB_STEP_SUMMARY"
|
|
echo "" >> "$GITHUB_STEP_SUMMARY"
|
|
echo "**Web image:** ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}" >> "$GITHUB_STEP_SUMMARY"
|
|
echo "**Tags:**" >> "$GITHUB_STEP_SUMMARY"
|
|
echo "${{ steps.meta.outputs.tags }}" >> "$GITHUB_STEP_SUMMARY"
|
|
echo "**Digest:** ${{ steps.meta.outputs.digest }}" >> "$GITHUB_STEP_SUMMARY"
|