Repository navigation
Release PWA (Mobile) #7
Workflow file for this run
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: Release PWA (Mobile) | |
| # 由 semantic-release 创建的 v* tag 触发:构建 PWA 移动端产物并部署到 | |
| # ECS 服务器(https://entropydecrease.com/pwa/)。与 release.yml(Electron | |
| # 桌面端)共用同一 v* tag——双端同版本发布、互不干扰;PR 质量验证复用 | |
| # pr-check.yml(client job 已含 vite build 产出 PWA)。 | |
| on: | |
| push: | |
| tags: ['v*'] | |
| # 兜底触发:semantic-release 以内置 GITHUB_TOKEN 推 tag 时 GitHub 防递归 | |
| # 机制不派发 push 事件(同 release.yml 注释),Release 发布事件补跑 | |
| release: | |
| types: [published] | |
| # 手动兜底:在 UI 选择对应 tag 运行,用于补发历史版本 | |
| workflow_dispatch: | |
| permissions: | |
| contents: read | |
| # 同一 tag 的多个触发源串行执行,避免重复部署竞争 | |
| concurrency: | |
| group: release-pwa-${{ github.ref_name }} | |
| cancel-in-progress: false | |
| jobs: | |
| build: | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 30 | |
| steps: | |
| # lfs: true 必需:pwa-192x192.png 等由 Git LFS 托管,不拉取则为指针文件 | |
| - uses: actions/checkout@v4 | |
| with: | |
| lfs: true | |
| - uses: actions/setup-node@v4 | |
| with: | |
| node-version: 20 | |
| cache: 'npm' | |
| cache-dependency-path: client/package-lock.json | |
| - run: cd client && npm ci | |
| - run: cd client && npm run lint | |
| - run: cd client && npm run test -- --run | |
| # PWA 构建(子路径 /pwa/ 部署):.env.production 已入库(git 跟踪), | |
| # 生产环境变量自动注入;VITE_PWA_BASE 控制构建 base 与 manifest | |
| # start_url/scope(vite.config.ts),保证子路径安装入口正确 | |
| - name: Build PWA (base=/pwa/) | |
| env: | |
| VITE_PWA_BASE: /pwa | |
| run: cd client && npx vite build | |
| - name: Verify PWA artifacts | |
| run: | | |
| cd client | |
| test -f dist/index.html && echo "index.html OK" | |
| test -f dist/manifest.webmanifest && echo "manifest.webmanifest OK" | |
| test -f dist/sw.js && echo "sw.js OK" | |
| - uses: actions/upload-artifact@v4 | |
| with: | |
| name: pwa-dist | |
| path: client/dist/ | |
| retention-days: 14 | |
| deploy: | |
| needs: build | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 15 | |
| # secrets 不可用于 if 条件(同 release.yml),先注入 job 级 env 再在 step 用 env.* 引用 | |
| env: | |
| DOWNLOAD_BASE_URL: ${{ secrets.DOWNLOAD_BASE_URL }} | |
| HAS_ALIYUN_AK: ${{ secrets.ALIYUN_ACCESS_KEY_ID != '' }} | |
| steps: | |
| - uses: actions/download-artifact@v4 | |
| with: | |
| name: pwa-dist | |
| path: pwa-dist | |
| - name: Ensure PWA directory on server | |
| uses: appleboy/ssh-action@v1 | |
| with: | |
| host: ${{ secrets.SERVER_HOST }} | |
| username: ${{ secrets.SERVER_USER }} | |
| key: ${{ secrets.SSH_PRIVATE_KEY }} | |
| script: mkdir -p /opt/Entropydecrease/pwa | |
| - name: Upload PWA to server (clean replace) | |
| uses: appleboy/scp-action@v0.1.7 | |
| with: | |
| host: ${{ secrets.SERVER_HOST }} | |
| username: ${{ secrets.SERVER_USER }} | |
| key: ${{ secrets.SSH_PRIVATE_KEY }} | |
| source: pwa-dist/* | |
| target: /opt/Entropydecrease/pwa | |
| strip_components: 1 | |
| rm: true | |
| # scp rm:true 重建目录导致 bind mount 失效,须重启 nginx 容器恢复 | |
| # (同 deploy-website.yml 模式);顺带写入版本标记 + 验证部署。 | |
| # 自愈兜底:nginx 容器因故未运行(如服务器手动 compose 漏传 | |
| # --env-file .env.production 致上游解析失败)时,用正确 env-file 重建, | |
| # 避免 restart 报错且全站持续不可用(2026-08-16 生产事故教训) | |
| - name: Restart Nginx, write version and verify | |
| uses: appleboy/ssh-action@v1 | |
| with: | |
| host: ${{ secrets.SERVER_HOST }} | |
| username: ${{ secrets.SERVER_USER }} | |
| key: ${{ secrets.SSH_PRIVATE_KEY }} | |
| script: | | |
| echo "${{ github.ref_name }}" > /opt/Entropydecrease/pwa/version.txt | |
| if docker ps -a --format '{{.Names}}' | grep -q '^entropy-decrease-nginx$'; then | |
| docker restart entropy-decrease-nginx | |
| else | |
| cd /opt/Entropydecrease/server | |
| docker compose -f docker-compose.prod.yml --env-file .env.production up -d nginx | |
| fi | |
| sleep 5 | |
| curl -sf https://entropydecrease.com/pwa/ | grep -q "熵减" || exit 1 | |
| echo "PWA deployed: ${{ github.ref_name }}" | |
| # --------------------------------------------------------------- | |
| # CDN 预热:PWA 入口与元数据(manifest/sw.js)预热,移动端首开 | |
| # 免回源等待(入口文件虽小,CDN 冷缓存回源仍受源站出网带宽限制)。 | |
| # 与 release.yml 安装包预热共用同一套阿里云凭证;未配置时跳过。 | |
| # --------------------------------------------------------------- | |
| - name: Prefetch CDN cache (PWA) | |
| if: env.DOWNLOAD_BASE_URL != '' && env.HAS_ALIYUN_AK == 'true' | |
| continue-on-error: true | |
| env: | |
| AK_ID: ${{ secrets.ALIYUN_ACCESS_KEY_ID }} | |
| AK_SECRET: ${{ secrets.ALIYUN_ACCESS_KEY_SECRET }} | |
| run: | | |
| set -eo pipefail | |
| if [ ! -x /tmp/aliyun ]; then | |
| curl -sL https://aliyuncli.alicdn.com/aliyun-cli-linux-latest-amd64.tgz -o /tmp/aliyun.tgz | |
| tar -xzf /tmp/aliyun.tgz -C /tmp | |
| fi | |
| /tmp/aliyun configure set --profile cdnci --mode AK --region cn-hangzhou \ | |
| --access-key-id "$AK_ID" --access-key-secret "$AK_SECRET" | |
| OBJECTS="https://entropydecrease.com/pwa/"$'\n'"https://entropydecrease.com/pwa/manifest.webmanifest"$'\n'"https://entropydecrease.com/pwa/sw.js" | |
| echo "Prefetching:" | |
| echo "$OBJECTS" | |
| /tmp/aliyun cdn PushObjectCache \ | |
| --profile cdnci \ | |
| --ObjectPath "$OBJECTS" \ | |
| --Area domestic | |
| echo "PWA CDN prefetch done" | |
| # --------------------------------------------------------------------------- | |
| # Android APK(B 方案:TWA 打包):基于已入库的 android/twa 工程构建签名 APK, | |
| # 上传 GitHub Release + 服务器下载目录 | |
| # (官网「下载 Android APK」直链 = entropydecrease.com/downloads/entropydecrease.apk)。 | |
| # | |
| # ⚠️ 首次设置(2026-08-16 已完成:bubblewrap init 生成 android/twa/ + keystore): | |
| # keystore = android/twa/android.keystore(alias=android,packageId=com.entropydecrease.twa) | |
| # 1. 将 android/twa/android.keystore 转 base64 存入 Secret ANDROID_KEYSTORE_BASE64 | |
| # 2. Secret ANDROID_KEYSTORE_PASS / ANDROID_KEY_PASS = keystore 密码(init 时 keytool 输入值) | |
| # CI 直接 build(密码经 BUBBLEWRAP_* 环境变量传入,CLI 无密码参数); | |
| # android/twa/ 提交仓库(*.keystore 已 gitignore) | |
| # --------------------------------------------------------------------------- | |
| build-apk: | |
| needs: deploy | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 30 | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - uses: actions/setup-java@v4 | |
| with: | |
| distribution: temurin | |
| java-version: '17' | |
| - uses: actions/setup-node@v4 | |
| with: | |
| node-version: 20 | |
| - uses: android-actions/setup-android@v3 | |
| - name: Install bubblewrap | |
| run: npm i -g @bubblewrap/cli | |
| - name: Write signing keystore from secrets | |
| env: | |
| KEYSTORE_B64: ${{ secrets.ANDROID_KEYSTORE_BASE64 }} | |
| run: | | |
| if [ -z "$KEYSTORE_B64" ]; then | |
| echo "::error::ANDROID_KEYSTORE_BASE64 未配置(见 job 注释首次设置),APK 构建无法继续" | |
| exit 1 | |
| fi | |
| echo "$KEYSTORE_B64" | base64 -d > /tmp/entropydecrease.keystore | |
| # bubblewrap 每次命令都会加载 ~/.bubblewrap/config.json;CI 全新 runner | |
| # 无此文件时 jdkPath/androidSdkPath 为空,会弹交互安装提示导致非交互环境 | |
| # exit 130(v0.38~v0.40 发版 build-apk 连续失败根因)。预置配置指向 | |
| # setup-java/setup-android 提供的路径,消除交互。 | |
| - name: Configure bubblewrap (jdk/sdk paths) | |
| run: | | |
| mkdir -p ~/.bubblewrap | |
| cat > ~/.bubblewrap/config.json <<EOF | |
| {"jdkPath":"$JAVA_HOME","androidSdkPath":"$ANDROID_HOME"} | |
| EOF | |
| # bubblewrap 校验 SDK 根目录需存在 tools/ 或 bin/(新式 cmdline-tools | |
| # 布局没有),缺则建空占位目录通过校验 | |
| if [ ! -d "$ANDROID_HOME/tools" ] && [ ! -d "$ANDROID_HOME/bin" ]; then | |
| mkdir -p "$ANDROID_HOME/bin" | |
| fi | |
| # 缺 build-tools 36.1.0 时预装:bubblewrap 自动安装只找 tools/bin 或 bin | |
| # 下的 sdkmanager(新式布局找不到),故显式用 cmdline-tools 的 sdkmanager | |
| if [ ! -d "$ANDROID_HOME/build-tools/36.1.0" ]; then | |
| SDKMANAGER="$ANDROID_HOME/cmdline-tools/latest/bin/sdkmanager" | |
| [ -f "$SDKMANAGER" ] || SDKMANAGER=$(find "$ANDROID_HOME/cmdline-tools" -name sdkmanager -type f 2>/dev/null | head -n1) | |
| if [ -n "$SDKMANAGER" ]; then | |
| yes | "$SDKMANAGER" --sdk_root="$ANDROID_HOME" "build-tools;36.1.0" | |
| fi | |
| fi | |
| # TWA 工程已随仓库生成(8e2f68c),twa-manifest.json 与 manifest-checksum.txt | |
| # 一致,build 的变更检测不会触发交互;无需 update(其 --manifest 参数指向 | |
| # 本地 twa-manifest.json 而非 web manifest URL)。版本号由仓库文件维护。 | |
| - name: Build signed APK | |
| env: | |
| # bubblewrap build 的密码仅从这两个环境变量读取(CLI 无密码参数) | |
| BUBBLEWRAP_KEYSTORE_PASSWORD: ${{ secrets.ANDROID_KEYSTORE_PASS }} | |
| BUBBLEWRAP_KEY_PASSWORD: ${{ secrets.ANDROID_KEY_PASS }} | |
| run: | | |
| cd android/twa | |
| bubblewrap build \ | |
| --signingKeyPath /tmp/entropydecrease.keystore \ | |
| --signingKeyAlias android | |
| - name: Verify APK artifact | |
| run: test -f android/twa/app-release-signed.apk && echo "APK OK" | |
| - name: Upload APK to GitHub Release | |
| # 仅 tag 触发时存在对应 release;workflow_dispatch 补跑时跳过 | |
| if: startsWith(github.ref, 'refs/tags/') | |
| uses: softprops/action-gh-release@v2 | |
| with: | |
| files: android/twa/app-release-signed.apk | |
| - name: Upload APK to server downloads | |
| uses: appleboy/scp-action@v0.1.7 | |
| with: | |
| host: ${{ secrets.SERVER_HOST }} | |
| username: ${{ secrets.SERVER_USER }} | |
| key: ${{ secrets.SSH_PRIVATE_KEY }} | |
| source: android/twa/app-release-signed.apk | |
| target: /opt/Entropydecrease/downloads | |
| strip_components: 2 | |
| - name: Rename APK to canonical name | |
| uses: appleboy/ssh-action@v1 | |
| with: | |
| host: ${{ secrets.SERVER_HOST }} | |
| username: ${{ secrets.SERVER_USER }} | |
| key: ${{ secrets.SSH_PRIVATE_KEY }} | |
| script: | | |
| mv -f /opt/Entropydecrease/downloads/app-release-signed.apk \ | |
| /opt/Entropydecrease/downloads/entropydecrease.apk | |
| ls -la /opt/Entropydecrease/downloads/entropydecrease.apk |