Further reduce worker counts, JVM heap, Node memory, and CMake parallelism to lower CI costs. These incremental adjustments complement recent optimizations.
371 lines
14 KiB
YAML
371 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: carrot_bbs/frontend-web
|
|
OTA_PUBLISH_URL: https://updates.littlelan.cn/admin/publish
|
|
OTA_MANIFEST_URL: https://updates.littlelan.cn/api/manifest
|
|
|
|
jobs:
|
|
ota:
|
|
runs-on: ubuntu-latest
|
|
if: github.event_name != 'pull_request' && (github.event_name != 'workflow_dispatch' || github.event.inputs.publish_ota == 'true')
|
|
permissions:
|
|
contents: read
|
|
strategy:
|
|
matrix:
|
|
platform: [android, ios]
|
|
steps:
|
|
- name: Checkout code
|
|
uses: actions/checkout@v4
|
|
|
|
- name: Set up Node
|
|
uses: actions/setup-node@v4
|
|
with:
|
|
node-version: '25.6.1'
|
|
|
|
- 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 update bundle
|
|
run: |
|
|
rm -rf dist-${{ matrix.platform }} dist-${{ matrix.platform }}-update.zip
|
|
npx expo export --platform ${{ matrix.platform }} --output-dir dist-${{ matrix.platform }}
|
|
npx expo config --type public --json > dist-${{ matrix.platform }}/expoConfig.json
|
|
|
|
- name: Archive update bundle
|
|
run: |
|
|
python -c "
|
|
import os, zipfile
|
|
p = '${{ matrix.platform }}'
|
|
dist = f'dist-{p}'
|
|
with zipfile.ZipFile(f'dist-{p}-update.zip', 'w', zipfile.ZIP_DEFLATED) as zf:
|
|
for root, _, files in os.walk(dist):
|
|
for name in files:
|
|
src = os.path.join(root, name)
|
|
arc = os.path.relpath(src, dist)
|
|
zf.write(src, arc)
|
|
"
|
|
|
|
- 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=${{ matrix.platform }}" \
|
|
-H "Authorization: Bearer ${OTA_AUTH_TOKEN}" \
|
|
-H "Content-Type: application/zip" \
|
|
--data-binary @"dist-${{ matrix.platform }}-update.zip"
|
|
|
|
- name: Verify OTA manifest
|
|
run: |
|
|
REMOTE="$(curl -sS "${OTA_MANIFEST_URL}" \
|
|
-H "expo-platform: ${{ matrix.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-${{ matrix.platform }}/metadata.json')); print(m['fileMetadata']['${{ matrix.platform }}']['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=false -Dorg.gradle.workers.max=1 -Dorg.gradle.jvmargs=-Xmx3g -XX:MaxMetaspaceSize=512m -XX:+UseG1GC -XX:ReservedCodeCacheSize=256m"
|
|
_JAVA_OPTIONS: "-Xmx2g -XX:MaxMetaspaceSize=384m -XX:+UseG1GC -XX:ReservedCodeCacheSize=128m"
|
|
NODE_OPTIONS: "--max-old-space-size=2048"
|
|
NODE_ENV: "production"
|
|
CMAKE_BUILD_PARALLEL_LEVEL: "3"
|
|
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'
|
|
|
|
- name: Install dependencies
|
|
run: npm ci
|
|
|
|
- name: Generate Android native project
|
|
run: npx expo prebuild --platform android
|
|
|
|
- name: Configure Gradle with signing
|
|
run: |
|
|
cd android
|
|
|
|
# Decode Android signing keystore
|
|
echo "${{ secrets.ANDROID_KEYSTORE_BASE64 }}" | base64 -d > app/withyou-release-key.keystore
|
|
|
|
# Update settings.gradle
|
|
cat > settings.gradle << 'SETTINGS_EOF'
|
|
pluginManagement {
|
|
repositories {
|
|
maven { url 'https://developer.huawei.com/repo/' }
|
|
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"
|
|
}
|
|
|
|
dependencyResolutionManagement {
|
|
repositories {
|
|
maven { url 'https://developer.huawei.com/repo/' }
|
|
google()
|
|
mavenCentral()
|
|
maven { url 'https://www.jitpack.io' }
|
|
}
|
|
}
|
|
|
|
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
|
|
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://developer.huawei.com/repo/' }
|
|
google()
|
|
mavenCentral()
|
|
}
|
|
dependencies {
|
|
classpath('com.huawei.agconnect:agcp:1.9.1.301') {
|
|
exclude group: 'com.huawei.agconnect', module: 'agconnect-apms-plugin'
|
|
}
|
|
classpath('com.google.gms:google-services:4.4.4')
|
|
classpath('com.android.tools.build:gradle:8.12.0')
|
|
classpath('com.facebook.react:react-native-gradle-plugin')
|
|
classpath('org.jetbrains.kotlin:kotlin-gradle-plugin')
|
|
}
|
|
}
|
|
|
|
allprojects {
|
|
repositories {
|
|
maven { url 'https://developer.huawei.com/repo/' }
|
|
google()
|
|
mavenCentral()
|
|
maven { url 'https://www.jitpack.io' }
|
|
}
|
|
}
|
|
|
|
apply plugin: "expo-root-project"
|
|
apply plugin: "com.facebook.react.rootproject"
|
|
BUILD_EOF
|
|
|
|
# Update gradle.properties (without secrets for better cache hit rate)
|
|
cat > gradle.properties << 'PROPS_EOF'
|
|
org.gradle.daemon=false
|
|
org.gradle.parallel=false
|
|
org.gradle.configureondemand=false
|
|
org.gradle.workers.max=1
|
|
org.gradle.jvmargs=-Xmx3g -XX:MaxMetaspaceSize=512m -XX:+UseG1GC -XX:ReservedCodeCacheSize=256m -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
|
|
ndkVersion=27.1.12297006
|
|
expo.useLegacyPackaging=false
|
|
systemProp.org.gradle.internal.http.connectionTimeout=30000
|
|
systemProp.org.gradle.internal.http.socketTimeout=30000
|
|
PROPS_EOF
|
|
|
|
# Append signing properties (secrets appended, not cached)
|
|
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
|
|
|
|
# Verify signing config in app/build.gradle
|
|
echo "=== app/build.gradle signing section ==="
|
|
grep -A 20 'signingConfigs' app/build.gradle || echo "signingConfigs not found"
|
|
|
|
- name: Build Android release APK (arm64 only)
|
|
run: |
|
|
cd android
|
|
chmod +x gradlew
|
|
./gradlew :app:assembleRelease -PreactNativeArchitectures=arm64-v8a --max-workers=1
|
|
|
|
- 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
|
|
|
|
- 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"
|