step
Source: crates/lvcs-action/src/step.rs.
/// A step's id (decision 248): `[A-Za-z_][A-Za-z0-9_-]*`, unique within its
/// action; `${{ steps.<id>.outputs.<name> }}` names a step by it.
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
pub struct StepId(String);
/// A status function (decision 245): what a step's `if` asks of the run
/// so far.
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum Status {
/// No earlier step failed and the run was not cancelled: the default.
Success,
/// Always, unless the run was cancelled.
Always,
/// An earlier step failed.
Failure,
/// The run was cancelled.
Cancelled,
}
/// An expression (decisions 245 and 248): a status function, an earlier
/// step's output, or any other GitHub expression, folded at mapping time.
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum Expr {
/// `success()`, `always()`, `failure()`, or `cancelled()`.
Status(Status),
/// `steps.<step>.outputs.<name>`: what the step wrote to
/// `GITHUB_OUTPUT` under `name`, empty when it wrote none.
StepOutput {
/// The step.
step: StepId,
/// The output's name.
name: String,
},
/// Any other expression (stage C): what is left of it once what the
/// action fixes is folded in. As an `if` that calls no status function
/// it is `success() && (<expr>)` already.
Eval(Ast),
}
/// A typed hole of a [`Template`].
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum Hole {
/// An expression, which the `run` machine renders.
Expr(Expr),
/// A secret by name (`secrets.<NAME>`), which only the shell fills,
/// from the runner daemon's process environment, as it builds the
/// sandbox spec.
Secret(String),
}
/// A piece of a [`Template`].
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum Part {
/// Literal text.
Text(String),
/// A hole.
Hole(Hole),
}
/// Literal text with typed holes, each written `${{ <expression> }}`
/// (decision 245).
#[derive(Debug, Clone, Default, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
pub struct Template(Vec<Part>);
/// The program a script step runs its file with (decision 245's shell
/// templates on Linux, `{0}` being the script's path).
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum Shell {
/// None given: `bash -e {0}`, or `sh -e {0}` when the sandbox has no
/// `bash`.
Default,
/// `bash --noprofile --norc -eo pipefail {0}`.
Bash,
/// `sh -e {0}`.
Sh,
/// `python {0}`.
Python,
/// A custom argument list holding `{0}`.
Custom {
/// The argument list.
argv: Vec<String>,
},
}
/// A node step (decision 245): `node <tree>/<main>` with its inputs as
/// `INPUT_<NAME>`, after `pre` in the run's pre phase and before `post` in
/// its post phase. In stage B2 `tree` is a directory of the action's own
/// inputs, relative to `/work`.
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
pub struct NodeStep {
/// The program's directory, relative to `/work`.
pub tree: String,
/// The main program, relative to `tree`.
pub main: String,
/// The program run in the pre phase, relative to `tree`.
pub pre: Option<String>,
/// The program run in the post phase, relative to `tree`.
pub post: Option<String>,
/// Its inputs, by name.
pub inputs: BTreeMap<String, Template>,
}
/// A built-in action a workflow step's `uses:` names (decision 245, stage
/// C), matched by `owner/repo` at any ref; it needs no vendored tree.
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum Builtin {
/// `actions/checkout`: nothing, since `/work` holds the revision
/// already (it has no `.git`).
Checkout,
/// `actions/cache` (and its `restore` and `save`): nothing, its
/// `cache-hit` output `false`; reuse is LVCS's cache.
Cache,
/// `actions/setup-node`: the step fails unless the node on the fixed
/// `PATH`, which the `node` toolchain probe ran, has this major
/// version; with none asked for, nothing.
SetupNode {
/// The major version `node-version` asks for.
major: Option<u32>,
},
/// `actions/upload-artifact`: nothing; the files stay in the run.
UploadArtifact,
}
impl Builtin {
/// The action it stands for, as `uses:` names it.
#[must_use]
pub const fn uses(&self) -> &'static str {
match self {
Self::Checkout => "actions/checkout",
Self::Cache => "actions/cache",
Self::SetupNode { .. } => "actions/setup-node",
Self::UploadArtifact => "actions/upload-artifact",
}
}
}
/// What a step runs.
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum StepKind {
/// An argument list, with no shell.
Exec {
/// The argument list.
argv: Vec<String>,
},
/// A script, written to a file and run by `shell`.
Script {
/// Its shell.
shell: Shell,
/// Its text.
text: Template,
},
/// A node program.
Node(NodeStep),
/// A built-in action.
Builtin(Builtin),
}
/// One step of an action (decision 245).
///
/// `cond` is its `if`, `success()` when unset; `env` is set after the
/// action's own `env` and the `GITHUB_ENV` of earlier steps;
/// `working_directory` is relative to `/work`; a step whose
/// `continue_on_error` is set fails without failing the run; and
/// `timeout_secs` can only lower what is left of the run's limit.
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
pub struct Step {
/// Its id.
pub id: Option<StepId>,
/// Its `if`.
pub cond: Option<Expr>,
/// Its environment.
pub env: BTreeMap<String, Template>,
/// Its working directory, relative to `/work`.
pub working_directory: Option<String>,
/// Whether its failure leaves the run passing.
pub continue_on_error: bool,
/// Its own limit, in seconds.
pub timeout_secs: Option<u32>,
/// What it runs.
pub kind: StepKind,
}
/// What one step wrote to its step files (decision 245), parsed: each
/// `GITHUB_ENV` variable and `GITHUB_OUTPUT` and `GITHUB_STATE` value in
/// the order written (a later one of a name wins), and each `GITHUB_PATH`
/// entry in the order written.
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
pub struct StepFiles {
/// `GITHUB_ENV`.
pub env: Vec<(String, String)>,
/// `GITHUB_PATH`.
pub path: Vec<String>,
/// `GITHUB_OUTPUT`.
pub output: Vec<(String, String)>,
/// `GITHUB_STATE`.
pub state: Vec<(String, String)>,
}
/// How the shell read a step's step files.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum FilesRead {
/// What they hold.
Read(StepFiles),
/// Not handed to the machine: a gated action's run, whose step files
/// a secret could have reached, is never journaled with them.
Withheld,
/// A file is malformed, past `STEP_FILE_BYTES`, or not a regular file,
/// which fails the step.
Bad(String),
}
/// The directory of the step files, under a run directory's `tmp/`, which
/// the sandbox sees at `/tmp`.
pub const STEP_FILES_DIR: &str = "lvcs-step";
/// Each step file's name in [`STEP_FILES_DIR`], with the variable naming
/// its path in the sandbox.
pub const STEP_FILES: [(&str, &str); 5] = [
("GITHUB_ENV", "env"),
("GITHUB_PATH", "path"),
("GITHUB_OUTPUT", "output"),
("GITHUB_STATE", "state"),
("GITHUB_STEP_SUMMARY", "summary"),
];