Files
frontend/.gitea/workflows/build.yml
lafay c1f8acd4fc
Some checks failed
Frontend CI / build-and-push-web (push) Has been cancelled
Frontend CI / ota (android) (push) Has been cancelled
Frontend CI / ota (ios) (push) Has been cancelled
Frontend CI / build-android-apk (push) Successful in 45m59s
fix(ci): use explicit echo blocks instead of heredocs to prevent leading whitespace in gradle.properties
The heredoc approach (`cat >> file << EOF`) was introducing leading whitespace to each line,
causing Gradle to treat whitespace as part of property keys. This made hasProperty() return
false for signingConfig values, breaking release builds. The fix uses explicit echo blocks
that write column-0 strings without indentation artifacts.
2026-06-18 15:37:56 +08:00

286 lines
11 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.runtimeVersion")"
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"
NODE_OPTIONS: "--max-old-space-size=2048"
NODE_ENV: "production"
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
# Append signing properties to gradle.properties (secrets appended after prebuild for better cache hit rate).
# IMPORTANT: each echo line writes a column-0 string to gradle.properties — gradle.properties keys
# cannot have leading whitespace (Gradle treats leading whitespace as part of the key name and
# hasProperty() then returns false, breaking release signing — see app/build.gradle signingConfigs.release).
# NB: this block runs with `set -e` by default; if any echo fails the step aborts.
{
echo "MYAPP_UPLOAD_STORE_FILE=withyou-release-key.keystore"
echo "MYAPP_UPLOAD_STORE_PASSWORD=${{ secrets.ANDROID_KEYSTORE_PASSWORD }}"
echo "MYAPP_UPLOAD_KEY_ALIAS=withyou-key"
echo "MYAPP_UPLOAD_KEY_PASSWORD=${{ secrets.ANDROID_KEY_PASSWORD }}"
} >> gradle.properties
# CI build tuning (low-memory container): force single-worker, reduce JVM heap,
# plus restore properties that prebuild does NOT generate but the build needs.
{
echo "org.gradle.daemon=false"
echo "org.gradle.parallel=false"
echo "org.gradle.configureondemand=false"
echo "org.gradle.workers.max=1"
echo "org.gradle.jvmargs=-Xmx4g -XX:MaxMetaspaceSize=1g -XX:+UseG1GC -XX:ReservedCodeCacheSize=256m -XX:+HeapDumpOnOutOfMemoryError"
echo "kotlin.daemon.jvmargs=-Xmx4g -XX:MaxMetaspaceSize=1g -XX:+UseG1GC -XX:ReservedCodeCacheSize=256m"
echo "ndkVersion=27.1.12297006"
echo "systemProp.org.gradle.internal.http.connectionTimeout=30000"
echo "systemProp.org.gradle.internal.http.socketTimeout=30000"
# ExpoAutolinkingPlugin requires this; without it Gradle calls
# `node ... mirror-kotlin-inline-modules --watched-directories-serialized ''`
# and node JSON.parse('') throws SyntaxError, exiting 1.
# See ExpoAutolinkingPlugin.kt:51 (findProperty fallback to emptyList)
# and ExpoAutolinkingPlugin.kt:67 (passes value to node as-is).
echo "expo.inlineModules.watchedDirectories=[]"
} >> gradle.properties
# Verify signing config in app/build.gradle
echo "=== app/build.gradle signing section ==="
grep -A 20 'signingConfigs' app/build.gradle || echo "signingConfigs not found"
# Diagnostic: dump full gradle.properties + verify the signing property is readable
echo "=== gradle.properties (final) ==="
cat gradle.properties
echo "=== MYAPP_UPLOAD_STORE_FILE check ==="
grep -c '^MYAPP_UPLOAD_STORE_FILE=' gradle.properties || echo "MISSING MYAPP_UPLOAD_STORE_FILE (column 0)"
- name: Build Android release APK (arm64 only)
run: |
cd android
chmod +x gradlew
taskset -c 0-7 ./gradlew :app:assembleRelease -PreactNativeArchitectures=arm64-v8a --max-workers=1 --stacktrace --info -Dkotlin.daemon.jvm.options="-Xmx2g,XX:MaxMetaspaceSize=1g" 2>&1 | tee gradle-build.log
echo "===== Last 200 lines of build log ====="
tail -n 200 gradle-build.log
- 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.runtimeVersion")"
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"