跳转到主内容

SeatFlow GitHub Actions 工作流详解:单元测试、工作流 YAML 校验、多平台构建、发布与 OSS 分发、delta 链与缓存、WASM 在线版发布、Worker 密钥轮换

CI/CD 工作流

SeatFlow 使用 GitHub Actions 实现持续集成与持续发布。仓库中共有六条工作流,覆盖代码校验、工作流 YAML 校验、多平台构建、桌面发布与 OSS 分发、WASM 在线版发布和 Worker 密钥轮换。

工作流总览

文件 触发条件 用途
unit-tests.yml push(.cs/.csproj/.slnx/.axaml 路径);pull_request(全量);workflow_call 全量构建 + 条件测试(含 WASM 目标构建)
release.yml push 到 main 且仅 version.json 变更;手动触发 版本预检 → 4 平台构建(稳定版:OSS 同步打包历史 + Action 缓存;预发布:拉取最新 full 基线)→ vpk 打包 → 暂存 artifacts
actionlint.yml push / pull_request(.github/workflows/** 变更) 校验工作流 YAML(钉版 actionlint 1.7.7 + SHA256 校验)
publish.yml workflow_run(Release SeatFlow 完成后);手动触发 下载产物 → 创建 GitHub Release(稳定版同步 OSS)
publish-web.yml workflow_run(Publish SeatFlow 完成后,main) 正式版 WASM 构建 → OSS 上传 → KV 切换 → 清理旧版本
worker-secret-sync.yml 每周一 03
UTC;手动触发
Cloudflare Worker OSS 密钥轮换,失败自动开 Issue
push version.json (main)
  → release.yml(preflight → build: win/linux/macos × vpk;稳定版 OSS 历史同步 + 缓存)
  → workflow_run → publish.yml(GitHub Release + OSS)
  → workflow_run → publish-web.yml(仅正式版 → online.seatflow.work)

手动:
  Actions UI → release.yml(workflow_dispatch,按 pre-release 构建)
  Actions UI → publish.yml(workflow_dispatch,选择构建 run,始终 pre-release)

1. 单元测试工作流 (unit-tests.yml)

触发条件

on:
  push:
    paths: ['**.cs', '**.csproj', '**.slnx', '**.axaml']
  pull_request:
  workflow_call:
  • push:任何 C# 源码(.cs)、项目文件(.csproj、.slnx)或 Avalonia 标记文件(.axaml)的变更都会触发;路径过滤避免 README/文档变更浪费 CI。
  • pull_request:不加路径过滤,所有 PR 全量触发。
  • workflow_call:作为可复用工作流供其他工作流调用。

并发控制

concurrency:
  group: ci-${{ github.ref }}
  cancel-in-progress: true

同一分支/PR 上如果有新提交,旧的运行中 job 会被自动取消,节省 runner 时间。

增量测试策略

工作流先安装 wasm-tools 并执行一次全量 dotnet build(SeatFlow.Browser 在解决方案内,构建需要该 workload),再用 dorny/paths-filter@v4 按变更路径条件运行测试:

变更路径 运行的测试
src/SeatFlow.Core/** Core.Tests
src/SeatFlow.Infrastructure/** Infrastructure.Tests
src/SeatFlow.Application/** Application.Tests
tests/SeatFlow.Core.Tests/** Core.Tests
tests/SeatFlow.Infrastructure.Tests/** Infrastructure.Tests
tests/SeatFlow.Application.Tests/** Application.Tests

依赖传递规则:修改 Core 层会同时触发 Infrastructure 和 Application 的测试(因为上层依赖 Core);修改 Infrastructure 层同理会触发 Application 测试。这体现在 job 的条件判断中:

# Core.Tests 运行条件
- if: steps.filter.outputs.core == 'true'
      || steps.filter.outputs.infra == 'true'
      || steps.filter.outputs.tests-core == 'true'

# Infrastructure.Tests 运行条件
- if: steps.filter.outputs.core == 'true'
      || steps.filter.outputs.infra == 'true'
      || steps.filter.outputs.tests-infra == 'true'

# Application.Tests 运行条件
- if: steps.filter.outputs.core == 'true'
      || steps.filter.outputs.infra == 'true'
      || steps.filter.outputs.app == 'true'
      || steps.filter.outputs.tests-app == 'true'

执行流程

checkout (fetch-depth: 0)
  → 路径过滤 (dorny/paths-filter)
  → setup-dotnet 10.0.x
  → 安装 wasm-tools workload
  → dotnet build (全量)
  → dotnet test --no-build (按条件逐项目运行)

Presentation.Tests 暂未纳入 CI:过滤器中有 pres(src/SeatFlow.Presentation.Avalonia/**)但工作流只运行 Core/Infrastructure/Application 三个测试项目,tests/SeatFlow.Presentation.Tests(Headless UI,67 用例)当前仅参与全量构建验证。因此改动 UI 层后必须在本地执行全量 dotnet test(4 个项目共 448 用例)。


2. 发布构建工作流 (release.yml)

触发条件

on:
  push:
    branches: ['main']
    paths: ['version.json']
  workflow_dispatch:
    inputs:
      version:  # 覆盖 version.json 的发布版本号
      suffix:   # 可选后缀(beta.1 / rc),自动按 pre-release 处理
  • 自动触发:向 main 分支推送仅 version.json 的变更(版本号 bump)——其他文件变更不会触发发布。
  • 手动触发:workflow_dispatch 指定版本号与可选后缀,作为预发布构建。

Job 0: preflight — 版本预检

  • 禁止发布已存在的 Release tag(v{version})。
  • 版本号必须大于当前最新正式 Release(packaging.version.Version 比较),否则中止且不触发构建。

Job 1: build — 多平台构建 + Velopack 打包

矩阵策略(4 个 RID,fail-fast: false):

RID 运行环境 入口/图标 产物
win-x64 ubuntu-latest SeatFlow.exe / installer.ico SeatFlow-{ver}-Setup.exe
linux-x64 ubuntu-latest SeatFlow / SF_icon_mini_background.png SeatFlow-{ver}-linux-x64.AppImage
osx-x64 macos-latest SeatFlow / 运行时由 PNG 生成 installer.icns .pkg / .dmg
osx-arm64 macos-latest 同上 .pkg / .dmg

执行步骤:

checkout → setup-python → setup-dotnet 10.0.x
  → 安装系统依赖 (Linux: squashfs-tools, zstd / macOS: zstd)
  → 安装 vpk(钉版 1.2.161,与客户端 Velopack 包一致)
  → 【稳定版】restore 打包历史缓存 → pip install oss2 → 从 OSS 同步历史到 publish/history
  → dotnet publish (自包含, -r {rid}, -o publish/tmp)
  → 【预发布】拉取上一版本做 delta 基线 (scripts/ci/fetch_previous.sh,读 UPDATE_FEED_URL)
  → (macOS) 由 PNG 生成 icns
  → vpk pack(稳定版输出 publish/history;预发布输出 publish/out)
  → 暂存本次产物到 publish/dist(稳定版随后 save 打包历史缓存)
  → 上传 artifacts (publish/dist/*)

vpk pack 关键参数:

参数 说明
--packId SeatFlow 包 ID
--packVersion 取 preflight outputs,保证整条链版本一致
--channel {rid} 每个 RID 独立更新通道
--delta BestSpeed Zstandard 增量;BestSize(bsdiff)与客户端 Update.exe 不兼容
--noPortable 仅 Windows:不生成便携版
--keyId/--keyFile/--keyPassword 可选签名参数,secrets 为空时跳过签名

产物暂存与上传:pack 完成后由 scripts/ci/stage_velopack_artifacts.sh 把本次版本的安装包、SeatFlow-{ver}-{rid}-full/delta.nupkg 与 channel 文件复制到 publish/dist/,artifacts 只上传 publish/dist/*(if-no-files-found: error 保证产物缺失时报错);稳定版随后从历史目录清理安装包、Portable.zip、assets.*.json,保持缓存精简。

增量链与打包历史(稳定版)

vpk pack 写出的 releases.{channel}.json 会列出打包目录中的全部 nupkg,客户端据此把「本机版本 → 目标版本」之间的 delta 按版本顺序串联应用(超过 10 跳或 delta 总大小超过 full 时退回全量)。因此要让落后多个版本的用户也能增量更新,打包目录必须保留历史 delta:

  • OSS 为唯一真源:稳定版构建在 pack 前运行 scripts/ci/sync_velopack_history.py,把 updates/ 下本通道的 nupkg 同步到 publish/history——保留全部历史 delta + 最新 2 个 full,并做双向对账(下载缺失/大小不符者并校验 size + MD5/ETag;删除本地不在保留集的 nupkg,防止未发布版本混入 feed)。
  • Action 缓存仅加速:publish/history 通过 actions/cache 的 restore/save 子动作跨运行缓存(key 含 run_id/run_attempt,restore-keys 按通道前缀滚动);缓存失效时自动从 OSS 全量回填;保存失败(如超限)不阻塞发布。
  • 失败策略:同步失败硬失败(本次不发版),避免发布断链的 feed。
  • delta 生成:每次 pack 只生成「上一版 full → 新版本」一个 delta;缺口(有 full 但无指向它的 delta)仅记录在同步日志中,不回填历史。

2026-09 之前 Worker 对 .nupkg 直链返回 403,fetch_previous 一直静默失败,因此 OSS 上 1.4.x / 2.0.0 没有任何历史 delta;增量链自 2.1.0 起积累。

该工作流不创建 GitHub Release、不上传 OSS——发布动作由 publish.yml 接力完成。


3. 发布工作流 (publish.yml)

触发条件

on:
  workflow_run:
    workflows: ['Release SeatFlow']
    types: [completed]
  workflow_dispatch:
    inputs:
      run_id:  # 使用哪个 release.yml 构建运行(默认最新成功运行)
      version: # 与构建一致的版本号
      suffix:  # 可选后缀
模式 条件 行为
自动 workflow_run.event == 'push' 且构建成功 读取 version.json(与构建同源),正式版发布 latest 并同步 OSS
手动 workflow_dispatch 必须指定构建运行,始终按 pre-release 处理,不上传 OSS

非 push 触发的构建(如手动构建)完成时也会唤醒本工作流,但会被 if 条件直接跳过,避免红色失败记录。

执行步骤

checkout → setup-python
  → 解析元数据 (run_id / version / is_pre)
  → Pre-flight:tag 不得存在、版本必须大于最新正式 Release
  → 下载 artifacts (pattern: release-*,merge-multiple)
  → 版本化安装程序文件名 (SeatFlow Setup.exe → SeatFlow Setup-{ver}.exe;已含版本号则跳过)
  → 校验 RELEASE.md 存在
  → 组装 Release 正文:RELEASE.md + SHA256 校验表
  → 上传 OSS(仅自动 push + 稳定版;见 scripts/ci/upload_oss.py)
  → gh release create v{version}(稳定版=latest,预发布=--prerelease)

OSS 上传:仅当自动链路且版本号不含 - 时执行;凭证缺失时跳过 OSS 仅创建 GitHub Release。

发布触发链路

修改 version.json → push main
  → release.yml 构建(preflight + vpk)
  → publish.yml 自动发布 latest + OSS
  → publish-web.yml 自动发布在线版(仅正式版)

或手动:
Actions UI → release.yml(workflow_dispatch)→ publish.yml(选 run,pre-release)

4. 在线版发布工作流 (publish-web.yml)

桌面正式版发布成功后自动接力,将 WASM 静态站发布到 https://online.seatflow.work。

门禁 gate

  • 仅当上游 Publish SeatFlow 是 push 触发的自动链路成功运行时放行(conclusion == 'success' && event == 'workflow_run')。
  • 读取 version.json,版本号含 -(预发布)则 stable=false,deploy-web 跳过。
  • 手动发布不进入本流水线,未发布的 wasm 由开发者本地自测。

部署 deploy-web

checkout (head_sha) → setup-python/dotnet
  → 安装 wasm-tools workload
  → dotnet publish src/SeatFlow.Browser -c Release -o web-out
  → 产物校验:index.html / dotnet.js 存在,
     dotnet.native.*.wasm > 5MB 且日志无 "won't be linked in"(否则白屏)
  → 上传 OSS online_worktable/<version>/(原文件 + .br,--no-switch)
  → 读取当前 KV current(记录回滚版本)
  → KV 原子切换到新版本(--switch-only)
  → 清理旧版本 --keep 5(current 指向的版本永不删除)
  → 失败时打印回滚命令

回滚(秒级,需 Cloudflare 凭证):

CF_ACCOUNT_ID=... CF_API_TOKEN=... \
python3 scripts/ci/upload_web_oss.py --version <旧版本号> --switch-only

5. Worker 密钥同步 (worker-secret-sync.yml)

  • 触发:每周一 03
    UTC(北京时间 11
    )定时 + 手动触发;concurrency 不取消进行中的运行。
  • 动作:执行 scripts/ci/rotate_worker_secrets.py,轮换 Cloudflare Worker 使用的 OSS 密钥。
  • 失败处理:if: failure() 时自动创建带 ops 标签的 Issue(含运行链接),便于追踪。

6. 工作流 YAML 校验 (actionlint.yml)

  • 触发:push / pull_request 且变更命中 .github/workflows/**。
  • 动作:下载钉版 actionlint 1.7.7(SHA256 校验)后运行 ./actionlint -shellcheck= -pyflakes= -color .github/workflows/*.yml。
  • 作用:拦截「YAML 合法但 Actions 拒绝」的写法(典型如表达式中未加引号的 # 被当成注释截断,导致整个运行以 workflow file issue 失败且不启动任何 job)。

发布门禁

  • release.yml 的 preflight job 保证:tag 不重复、版本单调递增。
  • publish.yml 在下载产物前重复同样的 tag/版本预检,避免旧构建被误发布。
  • 分支保护建议将 unit-tests 的 job 设为 required status check,保证 main 合并前构建与测试通过。

版本号管理

发布链路由 version.json 驱动,平时由 scripts/version.py 管理:

# 查看当前版本
python3 scripts/version.py show

# bump 补丁版本(同步 version.json / about.json 等 15+ 处)
python3 scripts/version.py bump-app patch --force

version.json 中的 commitId / buildDate 由构建/发布脚本生成,不要手动修改。本地 dotnet r 任务仅限开发使用,禁止在 CI/CD 中调用。


环境变量与密钥

GitHub Secrets

密钥 用途 工作流
OSS_KEY_ID 阿里云 OSS AccessKey ID release(稳定版历史同步)/ publish / publish-web / worker-secret-sync
OSS_KEY_SECRET 阿里云 OSS AccessKey Secret 同上
CF_ACCOUNT_ID Cloudflare 账号 ID(KV / Worker API) publish-web / worker-secret-sync
CF_API_TOKEN Cloudflare API Token publish-web / worker-secret-sync
VPK_KEY_ID / VPK_KEY_FILE / VPK_KEY_PASSWORD Velopack 代码签名(可选,缺省跳过签名) release

GitHub Variables

变量 用途 工作流
OSS_ENDPOINT OSS endpoint(如 oss-cn-hongkong.aliyuncs.com) release / publish / publish-web / worker-secret-sync
OSS_BUCKET OSS bucket 名称 release / publish / publish-web / worker-secret-sync
UPDATE_FEED_URL 预发布 delta 基线更新源(稳定版已由 OSS 同步取代) release
CF_WORKER_SCRIPT 需要轮换密钥的 Worker 脚本名 worker-secret-sync
CF_API_BASE Cloudflare API 基础地址(可选覆盖) worker-secret-sync

release.yml 的 build job 直接读取仓库级 OSS_KEY_ID/OSS_KEY_SECRET(secrets)与 OSS_ENDPOINT/OSS_BUCKET(vars)做历史同步;publish.yml 仍在 Environment OSS 中执行上传(该环境 branch policy 仅允许 main),两者互不影响。

工作流级环境变量

env:
  DOTNET_NOLOGO: 'true'               # 禁用 .NET 横幅
  DOTNET_CLI_TELEMETRY_OPTOUT: 'true' # 禁用遥测
  PROJECT_PATH: 'src/SeatFlow.Desktop'
  CONFIGURATION: 'Release'

手动触发发布

发布(publish.yml)

  1. 进入仓库 Actions 页面,选择 Publish SeatFlow workflow
  2. 点击 Run workflow,填写 run_id(可留空取最新成功构建)、version、可选 suffix
  3. 手动发布始终为 pre-release 且不上传 OSS

构建(release.yml)

手动触发 Release SeatFlow 并填写版本号/后缀,用于构建验证或预发布产物;构建成功后自动链路的 publish.yml 会跳过手动构建(避免误发 latest)。


故障排查

release.yml:vpk 命令不可用

错误信息:"dotnet tool restore" to make the "vpk" command available.

原因:仓库 dotnet-tools.json 中声明了 vpk 为本地工具,但 CI 未 restore。解决:workflow 中已使用 dotnet tool install -g vpk --version 1.2.161 全局安装钉版(与客户端 Velopack 包版本一致),并直接调用 vpk(而非 dotnet vpk)绕过本地 manifest 解析。

release.yml:稳定版同步打包历史失败

Sync Velopack history from OSS 步骤非零退出会硬失败、不发版。检查仓库级 OSS_KEY_ID/OSS_KEY_SECRET(secrets)与 OSS_ENDPOINT/OSS_BUCKET(vars)是否有效;可本地用主仓库 scripts/ci/sync_velopack_history.py --dry-run 预演。

Delta 未生成或链不完整

同步日志若列出「缺口(有 full 无 delta)」,说明对应历史版本没有 delta(不会自动回填)。仅当上一版 full 存在于打包目录时,vpk pack 才会生成 prev → 新版本 的 delta;客户端因链路超过 10 跳或缺口而退回全量下载属预期行为。

publish.yml:文件未附带版本号

OSS/GitHub Release 的安装程序文件名缺少版本号(如 SeatFlow Setup.exe 而非 SeatFlow Setup-2.1.0.exe)。

原因:曾缺失版本号补齐步骤。已通过 “Version installer filenames” 步骤修复——对不含版本号的文件名自动插入版本号,已含版本号的跳过。

publish-web.yml:白屏 / 部署异常

  • dotnet.native.*.wasm 小于 5MB 或日志出现 won't be linked in → 构建缺少 wasm-tools(workflow 已前置校验)。
  • 线上冒烟已移除:Cloudflare Bot 管理会对 GitHub Runner 的请求返回 403,轮询 X-Online-Version 无法自证版本且会空转超时;部署正确性改由上传完整性校验(key/大小全量比对)与 KV 写入成功保证。
  • 部署后站点仍显示旧版本 → 检查 KV current 是否写入成功、Worker 是否正常与传播延迟;失败时 workflow 会打印上一版本的回滚命令。
  • br 协商或 MIME 校验失败(人工验证时)→ 检查 Worker 回源与 OSS 对象是否完整。

worker-secret-sync.yml:密钥轮换失败

工作流会自动创建 ops 标签的 Issue 并附运行链接;检查 Cloudflare / OSS 凭证配置与 CF_WORKER_SCRIPT 变量是否覆盖目标 Worker。

UI 改动未触发测试

unit-tests.yml 当前不运行 Presentation.Tests(见第 1 节警告);若希望纳入,需要在 workflow 中新增 dotnet test tests/SeatFlow.Presentation.Tests --no-build 步骤并在路径过滤中加入对应条目。


相关文档