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 testvagycargo-nextest) - statikus analízis (
cargo clippy) - formázás ellenőrzése (
cargo fmt --check) - biztonsági audit (
cargo-audit)
É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.
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.
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'
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.
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.