The Problem
Developers who need to test or validate GitHub Actions workflows locally often must spin up a full runner environment or rely on remote execution, adding latency and operational overhead. The repository provides a self‑contained Rust implementation that can parse, lint, plan, and execute actions on a developer machine, eliminating the need for a hosted runner for basic workflows.
What This Does
Canopy is split into a SDK of crates and a binary that composes them. The SDK crates live under crates/:
| Crate | Responsibility |
|---|---|
gh-actions-spec | Types for workflow and action.yml formats |
gh-actions-expr | Lexer, parser, evaluator for ${{ }} expressions |
gh-actions-context | Contexts a run exposes (e.g., github, runner) |
gh-actions-plan | Converts a workflow into a valid job graph |
gh-actions-lint | Linting rules for workflows (crates/gh-actions-lint/src/rules/) |
gh-actions-runner | Executes a planned job (crates/gh-actions-runner/src/executor.rs) |
gh-actions-services | Artifact and cache services a job talks to |
gh-actions-listener | Protocol a self‑hosted runner speaks: register, poll, acquire, report (crates/gh-actions-listener/src/lib.rs) |
The binary canopy is built from crates/canopy/src/main.rs and wires these crates together. The examples/ directory shows two compositions: gh-runner-local pulls jobs from GitHub and runs them locally, while gh-runner-fargate-ondemand provisions an AWS Fargate task per job.
How It Is Wired
Execution starts at crates/canopy/src/main.rs, which parses the CLI and dispatches to the appropriate crate. A typical run flow:
- Parse workflow –
gh-actions-specreads the YAML file. - Plan job graph –
gh-actions-plan(crates/gh-actions-plan/src/lib.rs) topologically sorts jobs, resolvesneeds, and produces a validated job list. - Linter –
gh-actions-lintchecks for common problems (crates/gh-actions-lint/src/rules/) before execution. - Runner –
gh-actions-runner(crates/gh-actions-runner/src/executor.rs) iterates the planned jobs, invoking the executor for each step. The executor (crates/gh-actions-runner/src/executor.rs) triggers actions, handlesruncommands, and manages I/O. - Services –
gh-actions-services(crates/gh-actions-services/src/lib.rs) provides artifact upload and caching during job execution. - Listener – If the workflow is run via the self‑hosted runner protocol,
gh-actions-listener(crates/gh-actions-listener/src/listener.rs) handles registration, polling, and report payloads.
The call graph is shallow: main → plan → runner → executor, with services and listener as side‑effects. The widest blast radius is in crates/gh-actions-runner/src/executor.rs, which touches the filesystem, spawns subprocesses, and interacts with the cache/artifact store.
How To Use It
Setup – The repo uses Cargo. To build the binary:
cargo build --release -p canopy
or install via cargo install --path crates/canopy if a published crate is available.
Configuration – No external keys are required for local runs. Environment variables that affect runner behavior are documented in crates/gh-actions-runner/src/ but are optional for basic workflow execution.
Running it – The README provides the exact commands (quoted verbatim):
canopy run .github/workflows/ci.yml # run it here
canopy run ci.yml --job build -n # plan what would run, and in what order
canopy lint ci.yml # lint the workflow
canopy lsp # language server over stdio
These invoke crates/canopy/src/main.rs with the appropriate subcommand.
Real‑World Use
A team can drop canopy lint ci.yml into their pre‑commit hook to catch malformed expressions or missing needs dependencies before pushing. For CI‑free development, canopy run ci.yml --job test -n shows the planned job order without executing anything, letting developers reason about matrix expansions and job ordering locally.
Code Health & Issues
Static analysis of the repository reveals:
- 60 test files across
tests/and per‑crate test modules. - 6 documentation files (README, LICENSE, etc.).
- No structural red flags: CI workflows (
.github/workflows/test.yml,release.yml,publish-extension.yml) are present, aCargo.lockis committed, and aLICENSEfile exists. - The test suite exercises the expression evaluator, planner validation, and runner execution paths.
Beyond the measured findings, the repo’s modular crate layout makes it straightforward to add new lint rules or extend the runner, but the listener crate’s protocol handling is tightly coupled to GitHub’s self‑hosted runner API, which may require upstream changes if the protocol evolves.
The Bottom Line
Canopy delivers a lightweight, Rust‑based runtime for GitHub Actions that can parse, lint, and locally execute workflows without a remote runner. The crate separation is clean, the test coverage is solid, and the binary commands are well‑documented. It is well‑suited for teams that need fast local feedback on workflow changes or want to prototype self‑hosted runner behavior. Organizations already invested in the GitHub Actions protocol will find the SDK useful for building custom tooling, while the binary provides an immediate “run here” experience.