actions-rs
A zero-dependency, #![forbid(unsafe_code)] Rust toolkit for talking to
the GitHub Actions runner from a binary/Docker action or any CI step.
It speaks the workflow-command and environment-file protocols — an
independent, unofficial Rust port of @actions/core (faithful API and
semantics, with deliberate safety-first departures).
Not affiliated with or endorsed by GitHub or the @actions/toolkit project.
What it's for (and the one thing it does best)
The flagship is a precise annotation builder: notice/warning/error
with file + line/column ranges and a title, using the exact
data-vs-property percent-encoding @actions/core uses (%, \r, \n
everywhere; :, , additionally in properties). Getting that encoding wrong
silently corrupts annotations in the GitHub UI — this crate gets it right and
unit-tests the encoding tables directly.
use ;
new
.file
.span
.title
.warning;
// emits this single line to stdout:
// ::warning title=clippy%3A%3Aneedless_clone,file=src/parser.rs,line=42,col=5,endColumn=7::redundant clone of `cfg`
Around that it provides the rest of the toolkit surface:
- logging + panic-safe
group,mask/set_secret, RAIIstop_commands,echo,set_failed/fail_now; - env files (
GITHUB_ENV/OUTPUT/STATE/PATH) with a collision-safe, std-only random heredoc delimiter (the CVE-2020-15228 injection class) and deprecated stdout fallback only for output/state; reserved-name guard; safe same-process overlay helpers for env/PATH; - typed inputs: strict YAML 1.2
bool_input,multiline_input,input_as::<T: FromStr>,mask_input; - a fluent
Summarybuilder (1 MiB guarded, escaped by default, raw HTML opt-in viaSummaryText::html); - runtime detection + a typed
Context; - crate-root
warning!/group!/ …format!-style macros.
Why
- Zero dependencies. Std only. Nothing to audit, fast to build.
- Correct by construction. Percent-encoding for command data vs. properties, collision-checked random heredoc delimiters for multiline environment-file values (the CVE-2020-15228 class of bug), strict YAML 1.2 boolean inputs.
- Honest errors. Filesystem/parse operations return
Result; pure stdout commands are infallible — no fake error channel. - Modern + compatible. Uses
GITHUB_ENV/GITHUB_OUTPUT/… directly; onlyset_output/save_statekeep deprecated stdout fallbacks where GitHub still supports them.
Differences from @actions/core
Faithful to the protocol, but deliberately divergent where Node semantics would be unsafe or dishonest in Rust. None of these change the bytes the runner sees; they change what the calling process can rely on:
- No
std::envmutation.@actions/core'sexportVariable/addPathalso writeprocess.envso the current process sees the change. That write isunsafein Rust edition 2024 (it is unsound when another thread may be running) and this crate is#![forbid(unsafe_code)], soexport_var/add_pathonly write the env file. For same-process visibility use the safe overlay:overlay_var,overlay_path, orapply_overlay(&mut Command)for child processes. - No retired-command fallback for env/PATH.
set_output/save_statekeep the merely-deprecated::set-output::/::save-state::stdout fallback.export_var/add_pathdo not fall back to::set-env::/::add-path::— GitHub disabled those (CVE-2020-15228, changelog) — so they returnError::UnavailableFileCommandoff-runner instead of emitting a command the runner ignores. - Injection guard. A
\r/\nin an output/state/env key or aPATHentry returnsError::InvalidNamerather than silently injecting extra env-file entries (the CVE-2020-15228 class). The heredoc delimiter is collision-checked →Error::DelimiterCollision. - Honest errors. Filesystem/parse operations return
Result; pure stdout commands stay infallible. No swallowed errors, no fake channel. - Summary escaped by default.
SummaryHTML-escapes text and attributes; raw HTML is explicit opt-in viaSummaryText::html.@actions/coreconcatenates raw HTML. - Deferred failure, not
process.exit.set_failedsets a flag instead of exiting;is_failed()/exit_code() -> std::process::ExitCodeletmaindecide when to exit, so destructors and cleanup still run.
Honest comparison with the alternatives
This is not the only crate in this space. Pick the right tool:
| Crate | Deps | Annotation builder (file+range) | Env files | Typed inputs | Summary | API client / derive | Notes |
|---|---|---|---|---|---|---|---|
| actions-rs (this) | zero, forbid(unsafe) |
yes, exact encoding, range | yes | yes (FromStr) |
yes | no | annotation-first; smallest, strictest |
gha |
zero | not as a builder | yes | basic | yes | no | closest overlap; mature, macro-style |
ghactions |
octocrab/serde/derive (opt.) | no | yes | yes (derive) | no | yes (#[derive(Actions)], octocrab) |
biggest, batteries-included |
github-actions |
uuid (opt.) |
no | yes | yes | yes | no | similar, tiny |
actions-core |
uuid 0.8 |
no (loose file/line/col) |
no[^ac] | basic | no | no | v0.0.2 (2020-04-01) |
[^ac]: From its published core.rs (latest release 0.0.2, 2020-04-01, per crates.io):
set_env / add_path emit the stdout ::set-env:: / ::add-path:: workflow commands.
GitHub announced the disabling of that command pair in October 2020 (changelog, CVE-2020-15228)
— distinct from the ::set-output:: / ::save-state:: pair, which was later only deprecated.
When to use this one: you want zero dependencies and #![forbid(unsafe_code)]
in a CI/security context, and you care about correct, ranged annotations.
When not to: you want a GitHub API client, action.yml generation, or
derive-driven input structs — use ghactions.
If you just want zero-dep logging/env helpers and don't need the annotation
builder, gha is mature and fine.
Not provided here: REST/GraphQL client, OIDC, action.yml
derive/codegen, tool cache, glob, command exec. These are deliberately a
different crate's job — pair this with one that covers them.
Quick start
The asserts below are real: this block is compiled and run as a doctest
(see tests/ and the cfg(doctest) README harness), so the documented
behaviour is verified on every cargo test.
use ;
Module map
| Module | Purpose |
|---|---|
log |
annotations, group, mask/set_secret, stop_commands, echo, set_failed/fail_now, exit_code/is_failed |
annotation |
Annotation builder (AnnotationSpan, file + line/column range + title) |
input |
input, input_required, bool_input, multiline_input, multiline_input_with, input_as::<T>, mask_input |
output |
set_output, save_state, get_state, export_var, add_path, overlay_var, overlay_path, apply_overlay |
summary |
fluent Summary builder (SummaryText::html for raw HTML, 1 MiB guarded) |
env |
is_github_actions/is_ci/is_debug, RunnerOs, RunnerArch, Context, vars constants |
command |
low-level WorkflowCommand for anything not covered above |
See examples/demo.rs for a runnable tour.
Compatibility
- Rust 1.95+ (edition 2024),
#![forbid(unsafe_code)], zero dependencies. - Dual-licensed MIT OR Apache-2.0.
- Unrelated to the archived
actions-rsGitHub org (setup-rustactions); this is an independent crate of the same (previously unregistered) name.