name: Publish Mini Program Dev Version on: workflow_dispatch: push: branches: - main paths: - "mini/**" - ".gitea/workflows/miniapp-preview.yml" jobs: publish: # 标准标签:hk / bt-runner 均带 ubuntu-latest,任一空闲 runner 接单 runs-on: ubuntu-latest steps: # 精简 runner(Alpine)无 node,无法跑 action-checkout;直接用 git 浅克隆 - name: Checkout env: REPO_URL: ${{ gitea.server_url }}/${{ gitea.repository }} REF_NAME: ${{ gitea.ref_name }} run: | git init -q . git remote add origin "$REPO_URL" git fetch -q --depth 20 origin "$REF_NAME" git checkout -q FETCH_HEAD git log --oneline -3 - name: Prepare Node toolchain run: | # 优先复用 runner 宿主机缓存的工具链(工作目录持久化) TOOLDIR="$RUNNER_WORKSPACE/.lunar-toolchain" mkdir -p "$TOOLDIR" if [ -x "$TOOLDIR/node/bin/node" ] && "$TOOLDIR/node/bin/node" --version >/dev/null 2>&1; then echo "复用缓存 Node 工具链" else # 清理旧缓存:Alpine busybox mv 遇已存在目录会合并而非替换, # 残留的旧版 node 会污染环境导致 symbol not found rm -rf "$TOOLDIR" mkdir -p "$TOOLDIR" # 探测 libc:Alpine(musl) 用 unofficial musl 构建,其余用官方 glibc 构建(tar.gz,busybox tar 不支持 xz) # 关键教训:unofficial-builds 在亚洲带宽极低,wget --timeout 只限单次读取, # 慢速断流仍返回成功,产生截断的 gzip(解压出损坏二进制报 symbol not found)。 # 必须用 sha256 硬校验,不匹配则换源重试。 if ldd --version 2>&1 | grep -qi musl || [ ! -e /lib/x86_64-linux-gnu/ld-linux-x86-64.so.2 ]; then NODE_DIST="node-v22.12.0-linux-x64-musl" NODE_SHA="8d96de0293fc3978d09e2a3fee68c66bca4efcca90f52105e6f7463636e97308" # 优先 npmmirror(国内/亚洲快),unofficial-builds 作为后备 URLS="https://registry.npmmirror.com/-/binary/node-unofficial-builds/v22.12.0/$NODE_DIST.tar.gz https://unofficial-builds.nodejs.org/download/release/v22.12.0/$NODE_DIST.tar.gz" else NODE_DIST="node-v22.12.0-linux-x64" NODE_SHA="e05a4d65232ae2b27b3d77da2e368522fb46b923335b8e0d5f77624c32484044" URLS="https://nodejs.org/dist/v22.12.0/$NODE_DIST.tar.gz https://mirrors.aliyun.com/nodejs-release/v22.12.0/$NODE_DIST.tar.gz" fi DL_OK="" for url in $URLS; do echo "尝试下载: $url" if wget -t 2 -T 120 -q -O "$TOOLDIR/node.tgz" "$url"; then # sha256 硬校验(busybox 有 sha256sum):拦截截断/错误页等假文件 if [ "$(sha256sum "$TOOLDIR/node.tgz" | cut -d' ' -f1)" = "$NODE_SHA" ]; then echo "下载成功且校验通过" DL_OK=1 break fi echo "sha256 校验失败,文件可能截断,尝试下一源" else echo "wget 下载失败,尝试下一源" fi rm -f "$TOOLDIR/node.tgz" done test -n "$DL_OK" || { echo "::error::所有下载源均失败或校验不通过"; exit 1; } tar -C "$TOOLDIR" -xzf "$TOOLDIR/node.tgz" mv "$TOOLDIR/$NODE_DIST" "$TOOLDIR/node" rm -f "$TOOLDIR/node.tgz" # musl 版 node 动态链接 libstdc++/libgcc_s,Alpine 精简容器缺失时补装 if [ "$NODE_DIST" = "node-v22.12.0-linux-x64-musl" ] && [ ! -e /usr/lib/libstdc++.so.6 ] && [ ! -e /lib/libstdc++.so.6 ]; then if command -v apk >/dev/null 2>&1; then apk add --no-cache libstdc++ libgcc || echo "::warning::libstdc++/libgcc 安装失败,node 可能无法启动" fi fi fi # 验证二进制可执行,失败则清缓存并报错(避免坏缓存被后续 run 复用) if ! "$TOOLDIR/node/bin/node" --version; then echo "::error::Node 二进制无法执行,请检查下载源与 libc 匹配" rm -rf "$TOOLDIR" exit 1 fi echo "PATH=$TOOLDIR/node/bin:$PATH" >> "$GITEA_ENV" - name: Verify Node runtime run: node --version - name: Install dependencies working-directory: mini run: npm ci - name: Run tests working-directory: mini run: npm test - name: Configure production API and app version working-directory: mini env: MINIAPP_BUILD_NUMBER: ${{ gitea.run_number }} GITEA_SHA: ${{ gitea.sha }} run: | test -n "$MINIAPP_BUILD_NUMBER" VERSION="$(node -e 'const {resolveVersion}=require("./ci/resolve-version.cjs"); process.stdout.write(resolveVersion(process.cwd()))')" test -n "$VERSION" mkdir -p ci-artifacts printf '%s\n' "$VERSION" > ci-artifacts/resolved-version.txt echo "Resolved Mini Program version for build+upload: $VERSION" # 注入版本号到 utils/version.js BUILD_TIME="$(date -u +%Y-%m-%dT%H:%M:%SZ)" COMMIT_SHORT="${GITEA_SHA:0:7}" cat > utils/version.js < "$RUNNER_TEMP/wechat-ci.key" - name: Upload WeChat development version working-directory: mini env: MINIAPP_APPID: ${{ vars.MINIAPP_APPID }} MINIAPP_ROBOT: ${{ vars.MINIAPP_ROBOT }} # Prefer the version resolved before build so UI label matches WeChat upload. MINIAPP_BUILD_NUMBER: ${{ gitea.run_number }} WECHAT_CI_PRIVATE_KEY_PATH: ${{ runner.temp }}/wechat-ci.key MINIAPP_UPLOAD_RESULT: ${{ gitea.workspace }}/mini/ci-artifacts/upload-result.json MINIAPP_VERSION_LABEL: ${{ gitea.sha }} MINIAPP_UPLOAD_DESC_ITEMS: "3" MINIAPP_UPLOAD_DESC_MAX: "100" run: | test -n "$MINIAPP_APPID" test -n "$MINIAPP_BUILD_NUMBER" test -s ci-artifacts/resolved-version.txt RESOLVED_VERSION="$(tr -d '\n' < ci-artifacts/resolved-version.txt)" # Force upload to the same version that was baked into the JS bundle. export MINIAPP_VERSION_OVERRIDE="$RESOLVED_VERSION" mkdir -p ci-artifacts node -e 'const c=require("./project.config.json"); if(c.appid!==process.env.MINIAPP_APPID){console.error("MINIAPP_APPID mismatch with project.config.json", process.env.MINIAPP_APPID, c.appid); process.exit(1)}' node -e 'const {resolveVersion}=require("./ci/resolve-version.cjs"); console.log("Resolved Mini Program version:", resolveVersion(process.cwd()))' node -e 'const {resolveUploadDesc}=require("./ci/resolve-upload-desc.cjs"); console.log("Resolved upload remark:", resolveUploadDesc({repoRoot:require("path").resolve(".."), versionLabel:process.env.MINIAPP_VERSION_LABEL}))' # Normalize PEM newlines that may be flattened when stored in Secrets. # 用 node 处理(部分 runner 无 python3) node - <<'JS' const fs = require("fs"); const p = process.env.WECHAT_CI_PRIVATE_KEY_PATH; let t = fs.readFileSync(p, "utf8"); if (t.includes("\\n") && t.includes("BEGIN")) t = t.split("\\n").join("\n"); t = t.split("\r\n").join("\n").trim() + "\n"; fs.writeFileSync(p, t); if (!/PRIVATE KEY/.test(t)) { console.error("WECHAT_CI_PRIVATE_KEY is not a PEM private key"); process.exit(1); } console.log("private_key_lines=", t.split("\n").length); JS node ci/upload-ci.cjs test -s ci-artifacts/upload-result.json cat ci-artifacts/upload-result.json - name: Send success notification if: success() env: TELEGRAM_BOT_TOKEN: ${{ secrets.TELEGRAM_BOT_TOKEN }} TELEGRAM_CHAT_ID: ${{ secrets.TELEGRAM_CHAT_ID }} DISCORD_WEBHOOK_URL: ${{ secrets.DISCORD_WEBHOOK_URL }} run: | VERSION="$(tr -d '\n' < mini/ci-artifacts/resolved-version.txt)" bash .gitea/scripts/notify.sh success "小程序开发版上传成功" "版本: ${VERSION}" || echo "通知发送失败,不影响主流程" - name: Send failure notification if: failure() env: TELEGRAM_BOT_TOKEN: ${{ secrets.TELEGRAM_BOT_TOKEN }} TELEGRAM_CHAT_ID: ${{ secrets.TELEGRAM_CHAT_ID }} DISCORD_WEBHOOK_URL: ${{ secrets.DISCORD_WEBHOOK_URL }} run: | bash .gitea/scripts/notify.sh failure \ "小程序开发版上传失败" \ "请检查 Actions 日志了解失败原因" || echo "通知发送失败" - name: Remove temporary credentials if: always() run: rm -f "$RUNNER_TEMP/wechat-ci.key"