A Rust fordító már önmagában is egy remek biztonsági háló, de ez nem jelenti azt, hogy nincs szükség CI/CD pipeline-ra. Egy jól megépített GitHub Actions workflow nemcsak azt garantálja, hogy a kódod fordul és a tesztek zöldek, hanem azt is, hogy a függőségeidben nincs ismert biztonsági rés, és hogy mindez néhány perc alatt lezajlik minden pull requestnél.

Ebben a cikkben végigmegyünk egy teljes pipeline felépítésén: a cargo check-től a cargo-audit-ig, közben pedig megnézzük, hogyan spórolhatunk build időt cache-eléssel, és hogyan tudjuk kihasználni a Rust 1.72-ben megjelent apróságokat is.

Miért van szükség CI/CD pipeline-ra Rust projektekben?

Elsőre furcsának tűnhet, hogy egy erősen típusos, memóriabiztos nyelvnél miért kell ennyit foglalkozni a CI-val. A válasz egyszerű: a fordító a típushibákat és a memóriabiztonsági problémákat kiszűri, de nem tud semmit a logikai hibákról, a regresszókról, vagy arról, hogy egy frissített dependency-ben van-e ismert CVE.

Egy jó pipeline tipikusan a következőket végzi el minden push vagy pull request esetén:

  • gyors szintaktikai és típusellenőrzés (cargo check)
  • teljes build (cargo build)
  • unit és integrációs tesztek futtatása (cargo test vagy cargo-nextest)
  • statikus analízis (cargo clippy)
  • formázás ellenőrzése (cargo fmt --check)
  • biztonsági audit (cargo-audit)
Tipp

Érdemes a cargo check-et külön, gyors job-ként futtatni, mielőtt a teljes build és teszt lépés elindulna — így egy egyszerű szintaktikai hiba miatt nem kell percekig várni a visszajelzésre.

Az alap GitHub Actions workflow felépítése

A GitHub Actions workflow-k YAML fájlokban élnek a .github/workflows/ mappában. Kezdjük egy minimális, de működő alap workflow-val:

name: CI

on:
  push:
    branches: [main]
  pull_request:

env:
  CARGO_TERM_COLOR: always

jobs:
  check:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: dtolnay/rust-toolchain@stable
      - run: cargo check --all-targets

Ez a workflow minden push és pull request esetén lefut, és egy Ubuntu runneren telepíti a legfrissebb stabil Rust toolchaint (jelen esetben az 1.72-t), majd lefuttatja a cargo check-et. Ez a lépés jellemzően 10-30 másodperc alatt lezajlik egy közepes méretű projektnél, így gyors visszajelzést ad.

Megjegyzés

A dtolnay/rust-toolchain action egy közösségi, de nagyon elterjedt és megbízható megoldás a Rust toolchain telepítésére GitHub Actions-ben. Alternatívaként a actions-rs/toolchain-t is használhatod, de az utóbbi karbantartása lassabb.

cargo check, cargo build és cargo test integrálása

A cargo check után jöhet a teljes build és a tesztek. Bővítsük a workflow-t egy test job-bal:

jobs:
  check:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: dtolnay/rust-toolchain@stable
      - run: cargo check --all-targets

  test:
    runs-on: ubuntu-latest
    needs: check
    steps:
      - uses: actions/checkout@v4
      - uses: dtolnay/rust-toolchain@stable
      - run: cargo build --verbose
      - run: cargo test --verbose

A needs: check sor biztosítja, hogy a test job csak azután induljon el, ha a check job sikeresen lefutott — így nem húzzuk fel a runnert egy olyan build-re, ami eleve el fog bukni.

Nézzünk egy egyszerű Rust modult, amit ez a pipeline tesztelne. Használjunk benne pár frissebb, de már stabil nyelvi elemet, mint a let-else és az OnceLock:

use std::sync::OnceLock;

static CONFIG: OnceLock<String> = OnceLock::new();

fn get_config() -> &'static str {
    CONFIG.get_or_init(|| "production".to_string())
}

fn parse_port(input: &str) -> u16 {
    let Ok(port) = input.parse::<u16>() else {
        return 8080;
    };
    port
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_get_config() {
        assert_eq!(get_config(), "production");
    }

    #[test]
    fn test_parse_port_valid() {
        assert_eq!(parse_port("3000"), 3000);
    }

    #[test]
    fn test_parse_port_invalid_fallback() {
        assert_eq!(parse_port("nem_szam"), 8080);
    }
}

A let-else szintaxis nagyon jól illik olyan helyzetekhez, ahol egy érték kinyerése sikertelen esetén korai visszatérést akarunk, anélkül hogy a teljes match-et ki kellene írnunk. Az OnceLock pedig szálbiztos, lazy inicializálású globális állapotot ad, ami CI szempontból is fontos: determinisztikus és nem okoz race condition-t párhuzamos tesztfuttatás közben.

Gyorsabb teszt futtatás cargo-nextest segítségével

A beépített cargo test runner sok esetben lassabb, mint amennyire szeretnénk, különösen nagyobb projekteknél, ahol több száz teszt van. Itt jön képbe a cargo-nextest, amely egy alternatív test runner: párhuzamosabban futtatja a teszteket, jobb kimenetet ad, és képes izolálni az egyes teszteket saját processzben, így egy panic! nem dönti le a teljes futtatást.

  nextest:
    runs-on: ubuntu-latest
    needs: check
    steps:
      - uses: actions/checkout@v4
      - uses: dtolnay/rust-toolchain@stable
      - uses: taiki-e/install-action@nextest
      - run: cargo build --tests
      - run: cargo nextest run

A taiki-e/install-action@nextest egy praktikus action, ami előre lefordított binárist tölt le a cargo-nextest-hez, így nem kell a forrásból lefordítani minden futásnál — ez önmagában is jelentős időt spórol.

Tipp

A cargo nextest run alapból JSON-szerű, strukturált kimenetet is tud adni (--message-format libtest-json), amit később más eszközökkel (pl. teszt riportokkal) tudsz feldolgozni.

Biztonsági audit automatizálása cargo-audit-tal

A függőségek biztonsága sokszor elsikkad a napi rohanásban. A cargo-audit a RustSec advisory adatbázis alapján ellenőrzi a Cargo.lock fájlban szereplő crate-eket, és figyelmeztet, ha valamelyikben ismert biztonsági rés van.

  audit:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: dtolnay/rust-toolchain@stable
      - uses: taiki-e/install-action@cargo-audit
      - run: cargo audit

Ez a job önállóan is futtatható, nem szükséges hozzá a teljes build, mivel a cargo audit csak a Cargo.lock-ot olvassa. Érdemes ezt egy ütemezett (cron) futtatással is kiegészíteni, mert egy új CVE bármikor megjelenhet egy már meglévő dependency-ben, függetlenül attól, hogy te épp csinálsz-e commitot:

on:
  push:
    branches: [main]
  pull_request:
  schedule:
    - cron: '0 6 * * 1'
Figyelem

A cargo audit alapból hibával (nem-zero exit code-dal) tér vissza, ha talál sérülékenységet, ami elbukja a job-ot. Ha van olyan ismert, de általad kockázatmentesnek ítélt advisory, amit ideiglenesen ki akarsz zárni, azt a audit.toml fájlban az ignore listával teheted meg — de csak átgondoltan, dokumentált indokkal.

Cache-elés és build idő optimalizálás

A Rust build idő az egyik leggyakoribb panasz CI környezetben, főleg, ha minden futásnál nulláról fordul minden dependency. A Swatinem/rust-cache action pontosan erre a problémára ad megoldást: intelligensen cache-eli a target/ mappát és a ~/.cargo regisztert a Cargo.lock hash alapján.

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: dtolnay/rust-toolchain@stable
      - uses: Swatinem/rust-cache@v2
      - run: cargo build --verbose
      - run: cargo test --verbose

Ez a cache action jóval hatékonyabb, mint a natív actions/cache manuális beállítása, mert automatikusan kezeli a Cargo-specifikus mappastruktúrát, és külön cache kulcsot generál a különböző rust toolchain verziókhoz is.

Érdemes még a sccache-t is megfontolni nagyobb, több modulból álló workspace-eknél, mivel az objektum szintű cache-elést biztosít, nem csak a végleges build artifact-ok szintjén.

Jó tudni

A cache mindig csak addig segít, amíg a Cargo.lock nem változik jelentősen. Ha gyakran frissítesz major verziójú dependency-ket, számíts arra, hogy időnként újra kell épülnie a teljes cache-nek.

Rust 1.72 újdonságainak kihasználása a CI konfigurációban

Az 1.72-es kiadás egyik hasznos apró változása a cargo check és cargo build diagnosztikai kimenetének javítása, ami CI logokban is jobban olvasható hibaüzeneteket eredményez — ez különösen hasznos, ha a hibát csak a job logjából kell visszakövetni, anélkül hogy lokálisan reprodukálnád.

Érdemes emellett a workflow-ban explicit módon rögzíteni a toolchain verziót is, ha determinisztikus buildeket szeretnél, ne csak a stable csatornára hagyatkozz:

  - uses: dtolnay/[email protected]

Ez különösen fontos, ha a projekt egy adott MSRV-t (Minimum Supported Rust Version) céloz meg, és külön job-ban szeretnéd ellenőrizni, hogy a kód a régebbi, deklarált minimum verzióval is fordul-e:

  msrv:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: dtolnay/[email protected]
      - run: cargo check --all-targets

Így egyszerre két toolchainen futtatod a pipeline-t: az egyik a stable (jelenleg 1.72), a másik pedig a projekt hivatalos MSRV-je, elkerülve azt a kellemetlen helyzetet, amikor egy felhasználó régebbi Rust verzióval próbálja lefordítani a crate-edet, és kiderül, hogy már nem megy.

Összefoglalás

Egy jól megépített GitHub Actions pipeline Rust projektekhez nem bonyolult, de sok apró döntésből áll össze: gyors cargo check az első védelmi vonal, cargo build és cargo test (vagy inkább cargo-nextest) a funkcionális helyesség biztosítására, cargo-audit a függőségi biztonságra, és egy okos cache-elési stratégia, hogy mindez ne vegyen el feleslegesen sok időt minden egyes pull requestnél.

Ha ezeket a lépéseket egyesével, jobonként építed fel, és needs kulcsszóval logikusan összekapcsolod őket, egy olyan pipeline-t kapsz, ami gyorsan ad visszajelzést, de közben semmit nem hagy ki a fontos ellenőrzések közül. Innen már csak egy lépés a release automatizálása is — de az már egy másik cikk témája.