bigywater.tech
Rust · 170

Rust 项目持续集成

CI 就是把本地的检查命令放到一个每次提交都会跑的地方。这篇写了一个完整的 ci.yml,然后诚实地区分:哪些是真正验证过的(语法检查、命令本身、门禁行为),哪些只能在 GitHub 上运行才知道(这篇没有做)。

0 / 1
actionlint 对正确 / 笔误的 ci.yml 的退出码
7
一次触发的 job 数(2×2 矩阵展开,自己算的)
4 / 4
注入的 4 种缺陷各只让对应的 1 个 job 失败

工作流文件

实测 · 本机执行输出
      1 | name: ci
      2 | on:
      3 |   push:
      4 |     branches: [main]
      5 |   pull_request:
      6 | env:
      7 |   CARGO_TERM_COLOR: always
      8 | jobs:
      9 |   fmt:
     10 |     runs-on: ubuntu-latest
     11 |     steps:
     12 |       - uses: actions/checkout@v4
     13 |       - run: cargo fmt --all -- --check
     14 |   clippy:
     15 |     runs-on: ubuntu-latest
     16 |     steps:
     17 |       - uses: actions/checkout@v4
     18 |       - run: cargo clippy --locked --all-targets -- -D warnings
     19 |   test:
     20 |     runs-on: ${{ matrix.os }}
     21 |     strategy:
     22 |       fail-fast: false
     23 |       matrix:
     24 |         os: [ubuntu-latest, macos-latest]
     25 |         toolchain: [stable, beta]
     26 |     steps:
     27 |       - uses: actions/checkout@v4
     28 |       - uses: dtolnay/rust-toolchain@master
     29 |         with:
     30 |           toolchain: ${{ matrix.toolchain }}
     31 |       - uses: actions/cache@v4
     32 |         with:
     33 |           path: |
     34 |             ~/.cargo/registry
     35 |             target
     36 |           key: ${{ runner.os }}-${{ matrix.toolchain }}-${{ hashFiles('**/Cargo.lock') }}
     37 |       - run: cargo build --locked
     38 |       - run: cargo test --locked
     39 |   audit:
     40 |     runs-on: ubuntu-latest
     41 |     needs: [fmt, clippy, test]
     42 |     steps:
     43 |       - uses: actions/checkout@v4
     44 |       - run: cargo install cargo-audit --locked
     45 |       - run: cargo audit

本篇串起前面几篇的检查:rustfmt 格式化、Clippy 静态检查、单元测试与集成测试、依赖安全审计、Cargo.lock。

actionlint

实测 · 本机执行输出
  actionlint ci.yml:退出码 0;输出:(无)
  needs 写错 job 名:退出码 1
    rsdata/u170_bad.yml:39:3: job "audit" needs job "tset" which does not exist in this workflow [job-needs]
       |
    39 |   audit:
       |   ^~~~~~
  matrix 属性写错:退出码 1
    rsdata/u170_bad.yml:36:37: property "toolchian" is not defined in object type {os: string; toolchain: string} [expression]
       |
    36 |           key: ${{ runner.os }}-${{ matrix.toolchian }}-${{ hashFiles('**/Cargo.lock') }}
       |                                     ^~~~~~~~~~~~~~~~
  缺少 runs-on:退出码 1
    rsdata/u170_bad.yml:9:3: "runs-on" section is missing in job "fmt" [syntax-check]
      |
    9 |   fmt:
      |   ^~~~

矩阵展开

实测 · 本机执行输出
  触发条件: {'push': {'branches': ['main']}, 'pull_request': None}
  test 矩阵:os × toolchain = 2 × 2 = 4 个 job: ubuntu-latest/stable, ubuntu-latest/beta, macos-latest/stable, macos-latest/beta
  整个工作流一次触发的 job 数:fmt 1 + clippy 1 + test 4 + audit 1 = 7;audit 声明 needs = ['fmt', 'clippy', 'test'](要等前面的 job 成功);test 里 fail-fast = False
  每个 job 的 run 步骤:
    fmt: ['cargo fmt --all -- --check']
    clippy: ['cargo clippy --locked --all-targets -- -D warnings']
    test: ['cargo build --locked', 'cargo test --locked']
    audit: ['cargo install cargo-audit --locked', 'cargo audit']

本机依次执行

实测 · 本机执行输出
  job      结果         耗时(秒)  执行的命令
  fmt      通过           0.1  cargo fmt --all -- --check
  clippy   通过           1.6  cargo clippy --locked --all-targets -- -D warnings
  test     通过           0.6  cargo build --locked; cargo test --locked
  audit    通过           2.3  cargo audit
  cargo test 的结果行:
    test tests::adds ... ok
    test tests::renders ... ok
    test result: ok. 2 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
    test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
    

门禁

实测 · 本机执行输出
  缺陷                                 fmt       clippy    test      audit    
  代码没格式化                             失败        通过        通过        通过       
  clippy 警告(needless_return)         通过        失败        通过        通过       
  测试断言错误                             通过        通过        失败        通过       
  依赖有已知漏洞(固定 smallvec 1.6.0)         通过        通过        通过        失败       
  各缺陷下失败 job 的第一条错误信息:
    [代码没格式化] fmt:退出码 1;cargo fmt --all -- --check
        Diff in <实验目录>/proj/u170/src/lib.rs:1:
        -pub fn  add(a:i32,b:i32)->i32{a+b}
        +pub fn add(a: i32, b: i32) -> i32 {
    [clippy 警告(needless_return)] clippy:退出码 101;cargo clippy --locked --all-targets -- -D warnings
        Checking ci_demo v0.1.0 (<实验目录>/proj/u170)
        error: unneeded `return` statement
         --> src/lib.rs:2:5
    [测试断言错误] test:退出码 101;cargo test --locked
        running 2 tests
        test tests::renders ... ok
        test tests::adds ... FAILED
    [依赖有已知漏洞(固定 smallvec 1.6.0)] audit:退出码 1;cargo audit
        Crate:     smallvec
        Version:   1.6.0
        Title:     Buffer overflow in SmallVec::insert_many

--locked

实测 · 本机执行输出
  cargo build --locked:退出码 101
        Updating crates.io index
    error: cannot update the lock file <实验目录>/proj/u170/Cargo.lock because --locked was passed to prevent this
    help: to generate the lock file without accessing the network, remove the --locked flag and use --offline instead.
    
  不加 --locked 的 cargo build:退出码 0;Cargo.lock 被悄悄改写,新增的包:['tinyvec']

缓存

实测 · 本机执行输出
  冷缓存:2.6, 2.7, 2.6 秒(中位数 2.6);恢复缓存后没改动:0.18, 0.17, 0.18 秒;恢复缓存后改了 lib.rs:0.51, 0.51, 0.48 秒
  缓存键(Cargo.lock 的 sha256 前 12 位):初始 beb799212c5f;改源码后 beb799212c5f(不变);新增依赖并重新生成锁文件后 e27d64fcd622(变化,缓存失效)

分步演示

CI
本步的对象

    本文用到的工具与边界

    • 本机 macOS 27 arm64 / rustc 1.98.1(brew)/ cargo 1.98.1;单个文件用 rustc 直接编译,需要依赖时用 cargo(经代理从 crates.io 下载);计时用 std::time::Instant,数字每次运行会有小幅抖动。actionlint 1.7.12(go install);PyYAML 6.0.2;cargo-audit 0.22.2(RustSec 漏洞库)
    • 没有验证:工作流在 GitHub Actions 上的真实运行(runner、权限、secrets、并发、超时)、uses: 的第三方 action(actions/checkout@v4、dtolnay/rust-toolchain、actions/cache@v4)的行为、beta 工具链与 ubuntu runner 上的结果、hashFiles 的实际算法、真实缓存的命中与大小限制、其它 CI 系统(GitLab CI 等)
    • 本机执行时跳过了 uses: 与 cargo install 步骤,test 只跑了一个矩阵项,所以「本机通过」不能代替 CI 的结果
    京ICP备2026067502号-1京公网安备11010502063487号