How `cargo xtask ci` Reproduces GitHub Actions CI Jobs Locally in OpenLogi

cargo xtask ci is a development helper that mimics the CI pipeline defined in .github/workflows/ci.yml by mapping each GitHub Actions job to a Rust Job enum variant and executing an identical sequence of shell steps on your local machine.

OpenLogi uses the xtask pattern to automate development workflows, and the cargo xtask ci command specifically allows developers to replicate complex GitHub Actions workflows locally without pushing code. This tool bridges the gap between local development and CI environments by parsing the repository's workflow definition and translating it into executable Rust code, ensuring that builds, tests, and linting steps behave identically on your workstation as they do in the cloud.

Detecting the Host Platform at Runtime

Unlike traditional Rust compilation that relies on compile-time cfg! checks, the implementation in xtask/src/commands/ci.rs uses Host::current to detect the operating system at runtime. This design choice allows a single compiled binary to adapt its behavior across Linux, macOS, and Windows, skipping jobs that cannot run on the current platform rather than failing to compile.

Modeling CI Jobs as Rust Enums

The core abstraction lives in xtask/src/commands/ci/jobs.rs, where the Job enum defines variants such as Rustfmt, Clippy, and TestsLinux. Each variant implements a spec() method that returns a Spec struct containing:

  • The display name matching the CI workflow (e.g., "tests (macos)")
  • Supported aliases and matrix prefixes
  • Host constraints defining which operating systems can execute the job
  • A participation flag for the default run set
  • A human-readable caveat explaining any deviations from the true CI behavior

This declarative approach makes it trivial to add new jobs or modify existing ones while keeping the Rust source code in sync with .github/workflows/ci.yml.

Resolving Job Selection from CLI Arguments

The command-line interface supports three modes of operation controlled by the Args struct in ci.rs:

  • --list: Prints a formatted table of every Job variant and its compatible hosts
  • Default run: Invokes Job::default_run() to select all jobs marked with in_default_run: true
  • Explicit selection: Parses job names or groups (like tests) through Job::resolve, which supports matrix prefixes for targeting specific configurations

The resolution logic handles fuzzy matching and group expansion, ensuring that cargo xtask ci tests correctly maps to all test-related variants regardless of the host operating system.

Building the Execution Plan

For each selected job, the system constructs a Plan consisting of reusable Step structs defined in xtask/src/commands/ci/jobs/steps.rs. The Job::plan method first verifies host compatibility; if the current machine does not satisfy the job's constraints, the job is immediately marked as SKIP with a clear explanation such as "CI runs it on linux and macos, this host is windows".

If the host is permitted, the plan generation proceeds:

  1. Each Step struct captures the executable program, command-line arguments, and required environment variables
  2. Steps are collected into a Plan object that maintains execution order
  3. Additional metadata like working directories and conditional logic are preserved from the original YAML workflow

Executing Steps with CI-Equivalent Environment

The Summary object orchestrates actual execution in ci.rs. When --dry-run is passed, it prints the formatted command via Step::fmt and reports PASS without invoking the system. In live mode, each Step executes through xshell::cmd! with carefully curated environment variables:

// Environment injected before any step runs
CARGO_TERM_COLOR=always
CARGO_INCREMENTAL=0
RUSTFLAGS="-D warnings"

These three variables mirror the defaults set by the GitHub Actions runner, ensuring that terminal output formatting, incremental compilation behavior, and compiler warning levels match the cloud CI exactly.

Reporting Results and Exit Codes

As the Summary iterates over plans, it categorizes each outcome as PASS, FAIL, or SKIP. Failed commands propagate their exit codes, and the final report displays counts for each category. This deterministic feedback loop allows developers to fix issues locally that would otherwise fail the remote CI pipeline.

Practical Usage Examples

To inspect available jobs without running them:

devenv shell -- cargo xtask ci --list

To execute all jobs compatible with your current host:

devenv shell -- cargo xtask ci

To preview what the test suite would do without side effects:

devenv shell -- cargo xtask ci --dry-run tests

For programmatic integration in other Rust tools:

use xtask::commands::ci::{Args, run};

let args = Args {
    list: false,
    dry_run: true,
    jobs: vec!["rustfmt".into()],
};
run(&args).unwrap();   // prints the rustfmt command without executing it

Summary

  • cargo xtask ci replicates the .github/workflows/ci.yml pipeline by modeling jobs as a Rust Job enum in xtask/src/commands/ci/jobs.rs
  • Runtime host detection via Host::current allows cross-platform usage without recompilation, automatically skipping incompatible jobs
  • The CLI supports --list for discovery, --dry-run for safe previews, and explicit job selection through Job::resolve
  • Each job generates a Plan of Step structs that capture exact shell commands and environment variables from the CI definition
  • Execution via xshell::cmd! pre-injects GitHub Actions environment variables (CARGO_TERM_COLOR, CARGO_INCREMENTAL, RUSTFLAGS) to ensure behavior parity
  • Entry point dispatch occurs in xtask/src/main.rs, with core logic residing in ci.rs and step definitions in jobs/steps.rs

Frequently Asked Questions

What is the xtask pattern and why does OpenLogi use it?

The xtask pattern is a Rust convention where development automation scripts are compiled as part of the workspace rather than relying on external build tools like Make or shell scripts. OpenLogi implements this in the xtask crate to provide type-safe, version-controlled automation that can leverage the full Rust ecosystem, ensuring that CI replication logic remains maintainable and testable alongside the main application code.

How does cargo xtask ci handle jobs intended for a different operating system?

When Job::plan detects that the current host does not match the job's constraints, it immediately marks the job as SKIP and prints a descriptive message explaining the platform mismatch. This allows developers on Windows to run the tool locally while gracefully ignoring macOS- or Linux-specific test suites without throwing errors.

What is the difference between --dry-run and normal execution?

The --dry-run flag causes the Summary object to print the exact command line for each Step via Step::fmt and report a simulated PASS status without ever calling xshell::cmd!. This mode is useful for verifying plan construction logic and reviewing which commands would execute given the current host and selection criteria.

How are environment variables kept in sync with the real GitHub Actions runner?

Before executing any steps, the run function in xtask/src/commands/ci.rs injects three specific environment variables—CARGO_TERM_COLOR, CARGO_INCREMENTAL, and RUSTFLAGS—that mirror the defaults provided by GitHub Actions. This ensures that color output, incremental compilation settings, and warning escalation levels match the cloud CI environment exactly, preventing "works on my machine" discrepancies.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →