Docs

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"),
];