Remove the LiveKit audio switch patch plugin as it's no longer needed after the JitPack repository was restricted to GitHub packages only. Simplify CI by removing npm caching and Android NDK caching/installation steps. Update privacy policy and terms of service to reflect JPush SDK naming changes.
383 lines
14 KiB
YAML
383 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=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: "8"
|
|
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: Cache Gradle
|
|
uses: actions/cache@v4
|
|
with:
|
|
path: |
|
|
~/.gradle/caches
|
|
~/.gradle/wrapper
|
|
~/.android/build-cache
|
|
key: ${{ runner.os }}-gradle-${{ hashFiles('package-lock.json') }}
|
|
restore-keys: |
|
|
${{ runner.os }}-gradle-
|
|
|
|
- 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=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
|
|
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=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
|
|
|
|
- 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"
|