Adds the Gradle Toolchains Foojay Resolver plugin to enable automatic JDK downloads and toolchain management, allowing the project to specify required JDK versions without manual installation.
398 lines
15 KiB
YAML
398 lines
15 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: Switch Gradle distribution to Tencent mirror
|
|
run: |
|
|
PROPS="android/gradle/wrapper/gradle-wrapper.properties"
|
|
if [ -f "$PROPS" ]; then
|
|
sed -i 's|distributionUrl=https\\://services.gradle.org/distributions/|distributionUrl=https\\://mirrors.cloud.tencent.com/gradle/|' "$PROPS"
|
|
echo "Updated gradle-wrapper.properties:"
|
|
cat "$PROPS"
|
|
else
|
|
echo "gradle-wrapper.properties not found"
|
|
exit 1
|
|
fi
|
|
|
|
- name: Decode Android signing keystore
|
|
run: |
|
|
echo "${{ secrets.ANDROID_KEYSTORE_BASE64 }}" | base64 -d > android/app/withyou-release-key.keystore
|
|
|
|
- name: Configure Gradle with China Maven mirrors
|
|
run: |
|
|
cd android
|
|
|
|
# Update settings.gradle with China Maven mirrors
|
|
cat > settings.gradle << 'SETTINGS_EOF'
|
|
pluginManagement {
|
|
repositories {
|
|
maven { url 'https://maven.aliyun.com/repository/gradle-plugin' }
|
|
maven { url 'https://maven.aliyun.com/repository/google' }
|
|
maven { url 'https://maven.aliyun.com/repository/public' }
|
|
maven { url 'https://maven.aliyun.com/repository/central' }
|
|
google()
|
|
mavenCentral()
|
|
gradlePluginPortal()
|
|
}
|
|
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")
|
|
id("org.gradle.toolchains.foojay-resolver-convention") version "0.5.0"
|
|
}
|
|
|
|
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 China Maven mirrors
|
|
cat > build.gradle << 'BUILD_EOF'
|
|
// Top-level build file where you can add configuration options common to all sub-projects/modules.
|
|
|
|
buildscript {
|
|
repositories {
|
|
maven { url 'https://maven.aliyun.com/repository/gradle-plugin' }
|
|
maven { url 'https://maven.aliyun.com/repository/google' }
|
|
maven { url 'https://maven.aliyun.com/repository/public' }
|
|
maven { url 'https://maven.aliyun.com/repository/central' }
|
|
google()
|
|
mavenCentral()
|
|
}
|
|
dependencies {
|
|
classpath('com.android.tools.build:gradle')
|
|
classpath('com.facebook.react:react-native-gradle-plugin')
|
|
classpath('org.jetbrains.kotlin:kotlin-gradle-plugin')
|
|
}
|
|
}
|
|
|
|
allprojects {
|
|
repositories {
|
|
maven { url 'https://maven.aliyun.com/repository/google' }
|
|
maven { url 'https://maven.aliyun.com/repository/public' }
|
|
maven { url 'https://maven.aliyun.com/repository/central' }
|
|
maven { url 'https://www.jitpack.io' }
|
|
google()
|
|
mavenCentral()
|
|
}
|
|
}
|
|
|
|
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"
|