Build an Action
Write a reusable maintenance action with a --fix flag and add it to your harness.
Build check-readme, an action that reports a missing README.md and creates
one with --fix. You will write a Rust library crate and mount it in your
harness. Mounting adds a command with the action’s name.
You need a working harness. If you do not have one, follow Get Started first.
Create the Crate
An action is an ordinary library crate that depends on the contract crate,
rakko:
cargo new --lib otter-check-readme
cd otter-check-readme
cargo add rakko
The contract defines the interfaces that actions and harnesses share. The
action does not need rakko-cli, which builds the command line.
This keeps the action independent of a command-line framework.
Everything that this action needs comes from rakko. Put these imports at the
top of src/lib.rs:
use std::fs;
use rakko::{
Access, Action, Args, ArgsSchema, ArgsValues, Argument, ArgumentShape, Context, Documented,
Finding, Location, Name, Outcome, ReadArgsError, Summary, action_name, argument_name,
};Add an Argument
Arguments let callers control a run, for example with a --fix flag.
Define them in a type that implements Args. Add this type to src/lib.rs:
/// The arguments that a run of check-readme reads
#[derive(Copy, Clone, Debug, Default)]
pub struct CheckReadmeArgs {
/// Whether the run writes a README when the project has none
fix: bool,
}
impl Args for CheckReadmeArgs {
fn schema() -> ArgsSchema {
ArgsSchema::new([Argument::builder()
.name(argument_name!("fix"))
.shape(ArgumentShape::Boolean)
.documentation("Write a README when the project has none")
.build()])
}
fn from_values(values: &ArgsValues) -> Result<Self, ReadArgsError> {
let name = argument_name!("fix");
let fix = match values.get(&name) {
Some(value) => value
.get()
.parse()
.map_err(|_| ReadArgsError::UnreadableValue {
name,
value: value.clone(),
})?,
None => false,
};
Ok(Self { fix })
}
}
schema describes each argument’s name, shape, and help text. The command
line turns a boolean argument into a switch, such as --fix. Other shapes
become flags that take a value.
from_values converts the values for a run into your argument type. Your
type decides what an omitted value means. If conversion fails, it returns
ReadArgsError before the action runs.
The command line reserves names such as json, quiet, and verbose.
If an action uses a reserved argument name, the harness stops at startup
and identifies the action and argument.
An action that reads no arguments uses the unit type, type Args = ();.
Write the Action
An action is a type that implements Action. Add it to src/lib.rs:
/// The text that a repair writes into a new README
const README: &str = "# Project\n";
/// Checks that the project has a README
///
/// A README is the first page that a new contributor reads. The action looks
/// for `README.md` in the root of the project. With `--fix`, it writes one
/// when the project has none.
#[derive(Copy, Clone, Debug, Default, Documented)]
pub struct CheckReadme;
impl Action for CheckReadme {
type Args = CheckReadmeArgs;
fn name(&self) -> Name {
action_name!("check-readme")
}
fn access(&self, _context: &Context, args: &Self::Args) -> Access {
if args.fix {
Access::Write
} else {
Access::Read
}
}
async fn run(&self, context: &Context, args: &Self::Args) -> Outcome {
let readme = context.root().get().join("README.md");
match readme.try_exists() {
Ok(true) => Outcome::Passed {
summary: Some(Summary::new("found README.md")),
},
Ok(false) if args.fix => match fs::write(&readme, README) {
Ok(()) => Outcome::Changed {
repairs: vec![missing_readme()],
},
Err(error) => Outcome::Errored {
source: Box::new(error),
},
},
Ok(false) => Outcome::Failed {
findings: vec![missing_readme()],
repairs: Vec::new(),
},
Err(error) => Outcome::Errored {
source: Box::new(error),
},
}
}
}
/// Returns the finding that reports a project without a README
fn missing_readme() -> Finding {
Finding::builder()
.message("the project has no README.md")
.location(Location::Project)
.build()
}
The code has these parts:
- The
Documentedderive uses the first paragraph of the doc comment as command help. Write one sentence without Markdown or links. A type without a doc comment does not compile. - The action name is also its command name. It starts with a lowercase ASCII
letter and contains lowercase ASCII letters, digits, and single hyphens.
action_name!rejects invalid names at compile time. The namesdoctorandhelpare reserved. accessdeclares whether a run reads or writes the project. In this action,--fixrequires exclusive access. Without it, the run can share the project with other readers. This method must not read files or start tools. If the arguments cannot determine access, returnAccess::Write, which is also the default.runexamines the project and returns anOutcome.Contextsupplies the project root. Every result, including an error, belongs in the outcome.
Choose the Outcome
Choose an outcome that describes the run:
Outcome::Passedmeans that the action found no problems. ASummary, such asfound README.md, describes what it examined.Outcome::Failedcarries aFindingfor each remaining problem. A finding describes a problem and its location.Locationidentifies the project, a directory, a file, or a position or range within a file. Include any completed repairs alongside the remaining findings.Outcome::Changedmeans that the action repaired every problem it found. Each repair has the same structure as a finding.Outcome::Skippedmeans that the action had nothing to examine. Include aSkipReasonfor the user.Outcome::Erroredmeans that an error prevented the action from reaching a result.
Actions and Groups explains how the command line turns each outcome into output and an exit code.
Mount It in a Harness
Add your crate to the harness dependencies. Then mount the action in the
harness’s main function:
use otter_check_readme::CheckReadme;
/// Builds the command line of the project and runs it
fn main() -> std::process::ExitCode {
rakko_cli::builder().mount(CheckReadme).run()
}
The harness now has a check-readme command. Its help includes the doc
comment and the --fix flag:
mise run rakko -- check-readme --help
Run the action, and let it repair the project:
mise run rakko -- check-readme
mise run rakko -- check-readme --fixNext Steps
- Run a Tool From an Action to use an external linter or formatter.
- Test an Action against a temporary project, including its access declaration.
- Form a Group to run several actions under one command.
- Publish an Action so that others can find and use it.