Files
Unlucky-Launcher/.github/workflows/guards.yml
T
AskaEth e9034a40c8 fix(guards): design 零依赖门禁覆盖 dev/build 依赖表(M89 复审 p2-2)
cargo tree --edges normal 看不到 dev-dependencies / build-dependencies,而 design
对外承诺的是「零第三方依赖」,不分 normal/dev/build。新增一步「依赖表必须为空」:
按表头切出 dependencies / dev-dependencies / build-dependencies / target.*.dependencies
(含 [dependencies.serde] 子表写法,表头携带包名),有任何一个键即失败;原「依赖树只允许
出现自身」那步保留,两道互补。

实测:基线绿;[dev-dependencies] serde 红(同一时刻 tree step 仍绿,正是原来的盲区);
[build-dependencies] digest 红。老 job(core-no-ui / file-size / licenses)解析后仍与 main 相等。
2026-11-02 11:19:16 +08:00

190 lines
10 KiB
YAML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
name: Guards
# 架构与合规守门(与 fmt/clippy 同级,不允许 continue-on-error):
# 1. core-no-ui —— crates/core 零 UI 依赖(D3 铁律),依赖表 + cargo tree 双查
# 2. presenter-no-ui —— crates/presenter 零 UI 依赖(D26 / M85):Slint 版与 iced 版共用的宿主层
# 3. design-zero-dep —— crates/design 零第三方依赖(M61 / M87):依赖表(含 dev/build)必须为空 + 依赖树只允许出现自身
# 4. file-size —— 全仓代码/文档单文件 ≤400 行(用户硬约束)
# 5. licenses —— cargo-deny:许可证白名单 + GPL 依赖拦截 + 供应链来源校验
on:
push:
branches: [main]
pull_request:
workflow_dispatch:
env:
CARGO_TERM_COLOR: always
jobs:
core-no-ui:
name: core 零 UI 守门
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable
- name: 依赖表直查(crates/core/Cargo.toml 不得出现 UI 框架)
run: |
set -uo pipefail
forbidden='slint|egui|eframe|iced|winit|gtk|javafx|tauri|fltk|druid|relm'
# 只看非注释行:manifest 里说明「不许出现 slint」的注释本身不该触发守门
if grep -vE '^[[:space:]]*#' crates/core/Cargo.toml | grep -nEi "$forbidden"; then
echo "::error file=crates/core/Cargo.toml::core 不得依赖 UI 框架(见 AGENTS.md 硬约束)"
exit 1
fi
echo "OK: crates/core/Cargo.toml 无 UI 依赖"
- name: 依赖树泄漏检查(cargo tree -p unlucky-core,排除自身行)
run: |
set -uo pipefail
forbidden='slint|egui|eframe|iced|winit|gtk|javafx|tauri|fltk|druid|relm'
tree=$(cargo tree -p unlucky-core --edges normal --prefix none)
echo "$tree"
if echo "$tree" | grep -vE '^unlucky-core v' | grep -qiE "$forbidden"; then
echo "::error::core 的依赖树出现 UI 框架 —— 传递依赖也不允许"
exit 1
fi
echo "OK: unlucky-core 依赖树零 UI"
# presenter 是 Slint 版(crates/app)与 iced 版(crates/app-iced)共用的唯一业务层(D26)。
# 一旦 slint/iced/winit 被拉进来,分层就废了,而编译与单测仍会全绿 —— 所以必须由 CI 守。
# 与 core-no-ui 同样是「依赖表 + cargo tree」双查,名单保持一致;脚本刻意复制而非抽公共脚本,
# 理由是 CI 守门脚本自身挂掉的排查成本远高于这几行重复。
presenter-no-ui:
name: presenter 零 UI 守门
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable
- name: 依赖表直查(crates/presenter/Cargo.toml 的依赖表不得出现 UI 框架)
run: |
set -uo pipefail
forbidden='slint|egui|eframe|iced|winit|gtk|javafx|tauri|fltk|druid|relm'
# 与 core-no-ui 的整文件 grep 不同:本 crate 的 description 合法地写着
# 「Slint 版与 iced 版共用」,整文件 grep 会把它当越线。所以先按表头切出依赖表
# (含 target.*.dependencies 与 dev/build 表),再去掉注释行,只查键名。
# 子表写法 `[dependencies.slint]` / `[target.'cfg(windows)'.dependencies.slint]`
# 是合法的等长写法,且表头本身带着包名,所以表头也要一并落进检查范围——
# 否则「子表 + optional + 非默认 feature」这种组合能同时骗过依赖表与 cargo tree。
awk '
/^[[:space:]]*\[/ {
in_deps = ($0 ~ /^[[:space:]]*\[(target\..*\.)?(dependencies|dev-dependencies|build-dependencies)(\.[^]]*)?\][[:space:]]*$/)
if (in_deps && $0 ~ /\.[^]]*\]/) printf "%d:%s\n", NR, $0
next
}
in_deps && !/^[[:space:]]*#/ && !/^[[:space:]]*$/ { printf "%d:%s\n", NR, $0 }
' crates/presenter/Cargo.toml > /tmp/presenter-deps.txt
cat /tmp/presenter-deps.txt
if grep -iE "$forbidden" /tmp/presenter-deps.txt; then
echo "::error file=crates/presenter/Cargo.toml::presenter 不得依赖 UI 框架(D26)。窗口创建 / 渲染 / 图片解码属于 app 与 app-iced 侧;需要跨前端共用的类型请放进 presenter 的 dto。"
exit 1
fi
echo "OK: crates/presenter/Cargo.toml 依赖表无 UI 依赖"
- name: 依赖树泄漏检查(cargo tree -p unlucky-presenter,排除自身行)
run: |
set -uo pipefail
forbidden='slint|egui|eframe|iced|winit|gtk|javafx|tauri|fltk|druid|relm'
# 注意:cargo tree 命中 UI 框架时**退出码仍是 0**,所以只能抓输出再判断。
if ! tree=$(cargo tree -p unlucky-presenter --edges normal --prefix none); then
echo "::error::cargo tree -p unlucky-presenter 执行失败 —— crate 是否仍叫 unlucky-presenter?"
exit 1
fi
echo "$tree"
hits=$(printf '%s\n' "$tree" | grep -vE '^unlucky-presenter v' | grep -iE "$forbidden")
if [ -n "$hits" ]; then
echo "::error::presenter 的依赖树出现 UI 框架(传递依赖也不允许),本次命中:"
printf '%s\n' "$hits" | sed 's/^/ - /'
echo "::error::presenter 是 Slint 版与 iced 版共用的宿主层:把窗口 / 渲染 / 图片解码留在 app 侧,presenter 只持有与框架无关的 DTO、状态 reducer 与 host 装配。"
exit 1
fi
echo "OK: unlucky-presenter 依赖树零 UI"
# unlucky-design 是「纯数据 + 纯函数、零第三方依赖」的设计令牌层,这是 README 明写的卖点
# (连 serde 都不加),但目前只靠人肉守。零依赖的判据比 UI 名单更硬:依赖树只允许有自己一行。
# 注意 `cargo tree --edges normal` 看不到 dev/build 依赖,所以另设一步按表头切依赖表逐个查空。
design-zero-dep:
name: design 零第三方依赖守门
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable
- name: 依赖表必须为空(dependencies / dev / build 表一律不许有键,含子表写法)
run: |
set -uo pipefail
# design 承诺的是「零第三方依赖」,不分 normal / dev / build —— 而 cargo tree
# --edges normal 对 dev/build 是瞎的。所以先按表头切出全部依赖表(含
# `[dependencies.serde]` 这类子表写法,表头本身携带包名),再看是否为空。
awk '
/^[[:space:]]*\[/ {
in_deps = ($0 ~ /^[[:space:]]*\[(target\..*\.)?(dependencies|dev-dependencies|build-dependencies)(\.[^]]*)?\][[:space:]]*$/)
if (in_deps && $0 ~ /\.[^]]*\]/) printf "%d:%s\n", NR, $0
next
}
in_deps && !/^[[:space:]]*#/ && !/^[[:space:]]*$/ { printf "%d:%s\n", NR, $0 }
' crates/design/Cargo.toml > /tmp/design-deps.txt
if [ -s /tmp/design-deps.txt ]; then
cat /tmp/design-deps.txt
echo "::error file=crates/design/Cargo.toml::unlucky-design 的依赖表必须为空(normal / dev / build 都算),本次命中:"
sed 's/^/ - /' /tmp/design-deps.txt
echo "::error::design 是纯数据 + 纯函数的设计令牌,README 对外承诺零第三方依赖(连 serde 都不加):色彩 / 排版 / 形状 / 高程 / 动效请不要引入任何 crate,测试与构建辅助依赖同样不许加。序列化与运行时能力放到消费方(app / app-iced / presenter)。"
exit 1
fi
echo "OK: crates/design/Cargo.toml 依赖表全空(含 dev / build)"
- name: 依赖树只允许出现自身(cargo tree -p unlucky-design --edges normal)
run: |
set -uo pipefail
if ! tree=$(cargo tree -p unlucky-design --edges normal --prefix none); then
echo "::error::cargo tree -p unlucky-design 执行失败 —— crate 是否仍叫 unlucky-design?"
exit 1
fi
echo "$tree"
# 空行不算,剩下的每一行都必须是自己:正常时整个输出只有一行
# `unlucky-design v0.1.0 (/path/to/crates/design)`。
lines=$(printf '%s\n' "$tree" | grep -cE '[^[:space:]]' || true)
others=$(printf '%s\n' "$tree" | grep -vE '^[[:space:]]*$' | grep -vE '^unlucky-design v' || true)
if [ -n "$others" ] || [ "$lines" -ne 1 ]; then
echo "::error file=crates/design/Cargo.toml::unlucky-design 必须零第三方依赖,本应只有自身一行,实际 $lines 行;越线的包:"
printf '%s\n' "$others" | sed 's/^/ - /'
echo "::error::design 是纯数据 + 纯函数的设计令牌:色彩 / 排版 / 形状 / 高程 / 动效请不要引入 serde、rand、once_cell 等任何 crate。序列化与运行时能力放到消费方(app / app-iced / presenter)。"
exit 1
fi
echo "OK: unlucky-design 依赖树只有自身,零第三方依赖"
file-size:
name: 文件行数 ≤400 守门
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: 检查单文件行数
run: |
set -uo pipefail
LIMIT=400
failed=0
while IFS= read -r f; do
case "$f" in
LICENSE|*/LICENSE|LICENSE-*|*/LICENSE-*) continue ;;
CHANGELOG*|*/CHANGELOG*) continue ;;
esac
n=$(wc -l < "$f")
if [ "$n" -gt "$LIMIT" ]; then
echo "::error file=$f::$f 有 $n 行,超过 $LIMIT 行上限,请按模块拆分"
failed=$((failed + 1))
fi
# 覆盖范围含脚本(*.py/*.sh):packaging/ 下的构建辅助脚本同样受 400 行约束;
# Cargo.lock 等机器生成文件刻意不在列(否则一生成就超限)
done < <(git ls-files '*.rs' '*.md' '*.slint' '*.yml' '*.yaml' '*.toml' '*.json' '*.py' '*.sh')
if [ "$failed" -ne 0 ]; then
echo "共 $failed 个文件超限"
exit 1
fi
echo "OK: 全部受检文件 ≤$LIMIT 行"
licenses:
name: 依赖许可与来源(cargo-deny)
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: EmbarkStudios/cargo-deny-action@v2
with:
# 只跑许可/禁用/来源三类,advisories 由后续安全轮次单独接管
command: check licenses bans sources