CI/CD 故障排查手册
通用 GitHub Actions 发布工作流的常见问题和解决方案。
1. 跨平台原生绑定缺失
症状:macOS 构建成功,Windows 构建安装依赖时报找不到平台特定的原生模块。
原因:npm/bun 的 optional dependencies bug(npm/cli#4828)。在一个平台生成的 lock file 不包含其他平台的可选依赖。
解决方案:
- 改用 pnpm(推荐):pnpm 在每个 CI runner 上独立解析 optional dependencies
- 或在 Windows 步骤中显式安装缺失的包:
npm install @package/win32-x64-msvc
2. rm -f 在 PowerShell 报错
症状:Windows runner 上 rm -f file 报 -f 参数歧义。
原因:PowerShell 的 rm 是 Remove-Item 别名,-f 被解析为 -Filter。
解决方案:使用跨平台兼容命令,或用条件判断 if: runner.os == 'Windows' 分开处理。
3. tag 触发的重复构建
症状:移动 tag 后同时触发新旧两个构建。
解决方案:添加 concurrency 配置:
concurrency:
group: release-${{ github.ref_name }}
cancel-in-progress: true4. GitHub token 缺少 workflow 权限
症状:推送 .github/workflows/ 文件被拒绝。
解决方案:
gh auth refresh -h github.com -s workflow5. 构建超时
症状:Rust 编译或 npm install 超过 GitHub Actions 默认 6 小时限制。
解决方案:
- 启用依赖缓存(
Swatinem/rust-cache、actions/cache) - 使用
CARGO_INCREMENTAL: 0和CARGO_TERM_COLOR: always优化 Rust 编译 - 拆分构建矩阵为多个独立 job
6. Tauri TAURI_SIGNING_PRIVATE_KEY Secret 解析失败(minisign 解不出)
症状:Build Tauri bundle 阶段(cargo tauri build 触发 tauri-plugin-updater 自动签名)报:
Error failed to decode secret key: incorrect updater private key password:
Missing encoded key in secret key原因:TAURI_SIGNING_PRIVATE_KEY env var 被多层 base64 编码了。cargo tauri signer generate 输出的 .key 文件本身已经是一层 base64(348 字节单行),如果 Secret 灌之前再 cat | base64 -w0 又包一层(double-base64),minisign 解码就拿到 base64 字符流,不是合法 minisign 私钥 blob。
解决方案:
- Secret 直接灌文件原文(一层 base64):
gh secret set TAURI_SIGNING_PRIVATE_KEY < ~/.tauri/faropdf.key - 验证:本地试签一遍能跑通即正确:
TAURI_PRIVATE_KEY="$(cat ~/.tauri/faropdf.key)" \ TAURI_PRIVATE_KEY_PASSWORD="..." \ cargo tauri signer sign /tmp/test.txt # 应该输出 "Your file was signed successfully" - 相关:
tauri.conf.json的pubkey字段也要求base64(2 行 minisign 公钥文件内容)(含untrusted comment: minisign public key: <KEYNUM>header 行),不是.pub文件第二行原文RWS8...。详见references/tauri-release.md§3 密钥链
7. Tauri pubkey 字段格式错
症状:Build Tauri bundle 阶段报:
Error failed to decode pubkey: failed to decode base64 pubkey:
failed to convert base64 to utf8: invalid utf-8 sequence of 1 bytes from index 2或后续报:
failed to convert updater pubkey: Missing encoded key in public key原因:Tauri CLI 的 decode_key 函数(crates/tauri-cli/src/helpers/updater_signature.rs)对 pubkey 字段值先 base64-decode 再 UTF-8 转换,然后 PublicKeyBox::from_string 解析。
- 填原文
RWS8WkTIW8ht2pmQPiablJPY8vRrsXleS6NxLsalJ/Tyn+1tKpHGxREc→ base64-decode 得到二进制 minisign 公钥 bytes(不是 UTF-8)→str::from_utf8失败 - 填
base64(RWS8...)单行(缺 minisign 2 行 header)→ base64-decode 通过但from_string拿到单行不合法 box →into_public_key报 "Missing encoded key in public key"
解决方案:字段值 = base64(2 行 minisign 公钥文件内容):
PUBKEY_B64=$(cat ~/.tauri/faropdf.key.pub | base64 -d | base64 -w0)
# 验:echo -n "$PUBKEY_B64" | base64 -d 应回显两行(含 untrusted comment + RWS8...)写入 tauri.conf.json 的 plugins.updater.pubkey。
8. Windows runner 多行 cargo tauri build \ 反斜杠被 PowerShell 吃掉
症状:Windows runner 的 Build Tauri bundle step 报:
ParserError: ...ps1:3
Line | 3 | --target x86_64-pc-windows-msvc \
| ~
| Missing expression after unary operator '--'.原因:Windows runner 默认 shell 是 PowerShell 7 (pwsh.EXE),\ 反斜杠在 PowerShell 里**不是**行续字符。下一行 --target 被解析成 PowerShell 表达式 -- unary operator。macOS / Linux 默认 bash 续行正常,没暴露。
解决方案:在 Build Tauri bundle step 显式 shell: bash(GitHub Actions Windows runner 自带 Git Bash):
- name: Build Tauri bundle
env: ...
shell: bash # ← 关键:Windows 也走 Git Bash,跟 macOS / Linux 一致
run: |
cargo tauri build \
--target ${{ matrix.target }} \
--bundles ${{ matrix.bundles }}⚠️
shell:只适用于run:step,不能用在uses:step。给uses: tauri-apps/tauri-action@v0这种 action step 加shell: bash会让整个 workflow file 语法违规,所有 run 0s failure 报 "workflow file issue"。这条修法只针对自写
run: cargo tauri buildstep(v0.1.x 风格)。改用tauri-apps/tauri-action@v0后这个 step 不存在,tauri-action 内部处理跨平台 shell,不要再补shell: bash。详见tauri-release.md§3。
9. Tauri updater manifest URL 子目录错(create-updater-manifest.mjs 之类自写脚本)
症状:release assets 都在 release 根目录(如 FaroPDF_0.1.0_amd64.AppImage),但 latest.json 的 url 指向子目录:
"url": "https://github.com/.../releases/download/0.1.0/faropdf-linux-x64/appimage/FaroPDF_0.1.0_amd64.AppImage"updater 客户端按这个 url 拉会 404(releases/download/.../<name> 不带子目录)。
原因:
softprops/action-gh-release@v2用files: artifacts/**/*.dmgglob 上传时只用 basename(actions/download-artifact@v4拉到本地时按 artifact 名分子目录,但上传时软化)- 自写 manifest 脚本用
relative(releaseDir, file)算 url,把 artifact 名子目录带进去了
解决方案:自写 manifest 脚本里 url 用 basename(file),不要用 relative(releaseDir, file):
// 错的
const url = buildAssetUrl(args.repo, args.tag, relative(releaseDir, file));
// 对的
import { basename, ... } from "node:path";
const url = buildAssetUrl(args.repo, args.tag, basename(file));10. 重新发布后 CDN 同步延迟(公共 URL 5-15 分钟 404)
症状:gh release view / gh release download 能正常列出 / 下载 assets(走 GitHub API),但 curl https://github.com/.../releases/latest/download/latest.json 公共 CDN URL 一直 404。
原因:GitHub release asset 的 CDN 同步到公共 releases/download/... URL 需要 5-15 分钟(gh CLI/API 用的是另一个 endpoint,先于公共 CDN 生效)。
解决方案:
- 临时验证用
gh release download或gh api repos/<owner>/<repo>/releases/tags/<tag>走 API 路径 - 等 15 分钟后再用 curl 测公共 URL
- 客户端(tauri-plugin-updater)第一次检查更新失败时会有 fallback 重试机制,不影响最终升级
- 不要因为 curl 404 就立刻删 release 重发——asset 已经在 release 上了,重发反而引入新 asset hash 不一致
11. Tauri updater manifest URL 缺 v 前缀(tag 写 0.2.0 而非 v0.2.0)
症状:release build 全绿、assets 都在、latest.json 已生成,应用内检查更新能"检测到新版本",但点下载后静默失败 / 404。gh release download 正常,但 curl .../releases/download/0.2.0/<asset> 公共 URL 永久 404(不是 §10 的 CDN 延迟——等再久也 404)。
原因:create-updater-manifest.mjs 生成 url 时把 tag 的 v 前缀去掉了:
const tagNoV = tag.replace(/^v/, "");
return `${repo}/releases/download/${tagNoV}/${asset}`; // → /releases/download/0.2.0/...GitHub Releases 的 download 路径要求真实 git tag。git tag 是 v0.2.0(带 v),0.2.0 不是 tag → 404。注意这跟 §9(basename 子目录)是不同维度的 URL bug:§9 是路径多了 artifact 子目录,本条是 tag 少了 v 前缀,两者都会让 updater 404 但根因不同。
解决方案:直接用完整 tag,不要去 v:
return `${repo}/releases/download/${tag}/${asset}`; // → /releases/download/v0.2.0/... ✅发版后验证(每次发版必查最后一道):
gh release download vX.Y.Z --pattern latest.json --dir /tmp/check
grep -o 'releases/download/[^/]*/' /tmp/check/latest.json # 必须输出 releases/download/vX.Y.Z/线上已发错的 latest.json 不必重跑整个 build:本地下 *.sig + 重跑 manifest 脚本生成新 latest.json + gh release upload vX.Y.Z latest.json --clobber 覆盖线上坏的(asset 本身不用动)。详见 tauri-release.md §8。
12. 本机 gh api 大请求体 PUT 被 EOF 掐断(GET 正常)
症状:本地用 gh api -X PUT contents/<file> 提交较大文件(base64 后几十 KB 以上,如整份 README)时连续 EOF / 连接重置;同会话的 gh api GET 正常,git push 也正常。CI 上无此问题。
原因:本机常驻代理(系统代理或 *_proxy 环境变量)对长连接大请求体不稳定,掐断上传方向;GET 响应体小不受影响。
解决方案:清空代理变量直连重试,不需要关掉代理本身:
env -u HTTP_PROXY -u HTTPS_PROXY -u http_proxy -u https_proxy -u ALL_PROXY -u all_proxy \
gh api -X PUT repos/<owner>/<repo>/contents/<file> ...若直连仍失败再考虑网络本身。同根因的其他表现:常驻代理掐断大文件分片上传(见各技能 CHANGELOG 中的 OSS 上传代理隔离条目)。