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_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: ubuntu-latest container: image: reactnativecommunity/react-native-android:latest 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: Install dependencies run: npm ci - name: Generate Android native project run: npx expo prebuild --platform android --non-interactive - name: Build Android release APK run: | cd android chmod +x gradlew ./gradlew clean :app:assembleRelease --no-daemon - name: Upload APK artifact uses: actions/upload-artifact@v3 with: name: carrot-bbs-android-release-apk path: android/app/build/outputs/apk/release/app-release.apk 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"