diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..7ffff64 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,10 @@ +node_modules +.git +.expo +dist +dist-web +dist-android +dist-android-update.zip +android +screenshots +*.log diff --git a/.gitea/workflows/build.yml b/.gitea/workflows/build.yml new file mode 100644 index 0000000..98ea252 --- /dev/null +++ b/.gitea/workflows/build.yml @@ -0,0 +1,196 @@ +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 + 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: Set up Android SDK + uses: android-actions/setup-android@v3 + + - name: Install Android SDK packages + run: | + yes | sdkmanager --licenses + sdkmanager "platform-tools" "platforms;android-35" "build-tools;35.0.0" + + - name: Install dependencies + run: npm ci + + - 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.REGISTRY_USERNAME }} + password: ${{ secrets.REGISTRY_PASSWORD }} + + - 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" diff --git a/Dockerfile.web b/Dockerfile.web new file mode 100644 index 0000000..51984ef --- /dev/null +++ b/Dockerfile.web @@ -0,0 +1,15 @@ +FROM node:20-alpine AS builder + +WORKDIR /app + +COPY package.json package-lock.json ./ +RUN npm ci + +COPY . . +RUN npx expo export --platform web --output-dir dist-web + +FROM nginx:1.27-alpine + +COPY --from=builder /app/dist-web /usr/share/nginx/html + +EXPOSE 80