Docs

action

Source: crates/lvcs-action/src/config.rs.

/// An action's name (decision 245): `[a-z0-9._-]+`, holding no `..`. A
/// gated action's name is also its channel's.
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
pub struct ActionName(String);

/// Where an action was declared: `lvcs.toml`, or a GitHub workflow file
/// (stage C), whose actions are named `<file stem>/<job id>`.
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum ActionSource {
    /// An `[[actions]]` entry of `lvcs.toml`.
    LvcsToml,
    /// A job of a `.github/workflows/` file.
    Workflow,
}

/// What runs an action (its trigger), and, for a gated action, the
/// channels that must already hold a frontier it takes (decision 141's
/// `from`, its eligibility gate). `after` is sorted, deduplicated, and
/// never empty (decision 297).
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum Trigger {
    /// Every merge into main, and main going quiet (decision 161); the
    /// action moves its own channel at each frontier it passes.
    Change,
    /// `channel` moving to a new frontier: the action runs once against
    /// that frontier, with no run request, when it is eligible.
    Channel {
        /// The channel it follows: another action's or a combination.
        channel: ChannelName,
        /// `channel` or channels upstream of it.
        after: Vec<ChannelName>,
    },
    /// A run request of the action's own channel (`lvcs run`).
    Manual {
        /// Action channels or combinations; the change set by default.
        after: Vec<ChannelName>,
    },
}

/// The paths an action reads: `paths`, which it covers, and `uses`, which
/// it reads besides, both gitignore-syntax patterns.
#[derive(Debug, Clone, Default, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
pub struct Inputs {
    /// The paths it covers; for the generator, empty means every synced
    /// file that isn't derived.
    pub paths: Vec<String>,
    /// The paths it reads besides.
    pub uses: Vec<String>,
}

/// The canonical action (decision 245): one sandboxed run over one
/// revision, keyed by its inputs and the runner's toolchain, signed by its
/// runner, and reused while its inputs are unchanged.
///
/// `channel` is the action's name for an `lvcs.toml` action, and `<file
/// stem>/<job id>` lowercased for a workflow change job (every matrix cell
/// of the job moves it) or the job's `environment` for a workflow gated
/// job (decision 297). `writes` is non-empty only for the generator. `secrets`
/// is non-empty only for a gated action. `timeout_secs` is resolved: the
/// class bound or the lower value asked for. `quorum` is as written; the
/// count it means is [`Action::quorum`]. Its id is [`Action::id`].
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
pub struct Action {
    /// Its name.
    pub name: ActionName,
    /// Where it was declared.
    pub source: ActionSource,
    /// What runs it.
    pub on: Trigger,
    /// The channel it moves.
    pub channel: ChannelName,
    /// The paths it reads.
    pub inputs: Inputs,
    /// The derived paths it generates.
    pub writes: Vec<String>,
    /// Its steps, in order.
    pub steps: Vec<Step>,
    /// The variables every step runs with; a gated action's holds each
    /// of its secrets under the secret's own name.
    pub env: BTreeMap<String, Template>,
    /// The credential variable names its run receives.
    pub secrets: Vec<String>,
    /// The toolchains it needs (`rust` is always probed besides).
    #[serde(with = "toolchains")]
    pub toolchain: BTreeSet<Toolchain>,
    /// Its wall clock and CPU limit, in seconds.
    pub timeout_secs: u32,
    /// The attestations or outcomes it asks for, as written.
    pub quorum: Option<u32>,
    /// The actions it runs after (a workflow job's `needs`, every cell of
    /// each needed job), by name.
    #[serde(default, skip_serializing_if = "Vec::is_empty")]
    pub after_actions: Vec<ActionName>,
}

/// A change action's channel under the actions in force (decision 297):
/// the ids of the change actions that move it (several for a workflow
/// job's matrix cells) and the attestations it counts a frontier at.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct ChangeChannel {
    /// The ids of the change actions that move it.
    pub actions: BTreeSet<ActionId>,
    /// The attestations it counts a frontier at: the largest `quorum` of
    /// its actions, at least `QUORUM_DEFAULT`.
    pub threshold: u32,
}

/// Why [`map`] refused an `[[actions]]` list.
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ActionConfigError {
    /// More than `GATED_ACTIONS_MAX` gated actions.
    TooManyGated,
    /// More than `CHANNELS_MAX` `[channels]` combinations.
    TooManyCombinations,
    /// A `[channels]` combination's name isn't a channel name, or it has
    /// no members (`bad_channel`).
    BadCombination {
        /// The combination.
        name: String,
    },
    /// A `[channels]` combination is named like an action's channel
    /// (`duplicate`).
    CombinationNamed {
        /// The combination.
        name: String,
    },
    /// A `[channels]` member names no action's channel and no other
    /// combination (`unknown_trigger`).
    UnknownMember {
        /// The combination.
        name: String,
        /// The member named.
        member: String,
    },
    /// A manual action leaves out `after` and no change action is
    /// declared, so the change set it defaults to is empty
    /// (`unknown_trigger`).
    NoChangeSet {
        /// The entry's name.
        name: String,
    },
    /// A name outside `[a-z0-9._-]+`, or holding `..`.
    Name {
        /// The name refused.
        name: String,
    },
    /// A name `lvcs run` keeps for a subcommand ([`RESERVED_ACTION_NAMES`],
    /// decision 332).
    ReservedName {
        /// The name refused.
        name: String,
    },
    /// Two entries have one name.
    Duplicate {
        /// The name given twice.
        name: String,
    },
    /// An entry's `run` is empty, or it gives both or neither of `run`
    /// and `steps`.
    EmptyRun {
        /// The entry's name.
        name: String,
    },
    /// A step is refused (decision 248).
    Step {
        /// The entry's name.
        name: String,
        /// The step's place, from 1.
        step: usize,
        /// Why.
        reason: String,
    },
    /// The action's `env`, `toolchain`, or `steps` is refused as a whole
    /// (decision 248).
    Action {
        /// The entry's name.
        name: String,
        /// Why.
        reason: String,
    },
    /// An entry other than the generator leaves out `paths`.
    NoPaths {
        /// The entry's name.
        name: String,
    },
    /// A change action sets `after` or `secrets`, or a gated action sets
    /// `writes`.
    NotFor {
        /// The entry's name.
        name: String,
        /// The key set.
        key: &'static str,
    },
    /// More than one action sets `writes`.
    TwoGenerators {
        /// The second one's name.
        name: String,
    },
    /// A gated action's `after` is empty, names itself, or names a
    /// channel no action moves and no combination (`unknown_trigger`).
    After {
        /// The entry's name.
        name: String,
    },
    /// A channel-triggered action's `after` channel is neither its trigger
    /// channel nor a channel upstream of it along `after` links and
    /// combination members.
    AfterNotUpstream {
        /// The entry's name.
        name: String,
        /// Its `after`.
        after: String,
        /// Its trigger channel.
        channel: String,
    },
    /// A gated action's `after` or channel trigger, or a `[channels]`
    /// member, names the generator's channel, which takes generation
    /// records, not attestations, so it can't gate an action
    /// (`bad_channel`, decision 297).
    GeneratorChannel {
        /// The entry or combination naming it.
        name: String,
        /// Where it names it: `after`, `on`, or `[channels]`.
        key: &'static str,
        /// The generator's channel.
        channel: String,
    },
    /// A channel trigger names its own channel, or a channel no action
    /// moves and no combination (`unknown_trigger`).
    UnknownTrigger {
        /// The entry's name.
        name: String,
        /// The channel named.
        channel: String,
    },
    /// Channel triggers, `after` links, combination members, and
    /// `after_actions` form a cycle (`trigger_cycle`): the actions and
    /// combinations on it, in order, the first repeated at the end.
    TriggerCycle {
        /// The cycle's actions.
        cycle: Vec<String>,
    },
    /// `paths`, `uses`, or `writes` holds more than `ACTION_PATTERNS_MAX`
    /// patterns.
    TooManyPatterns {
        /// The entry's name.
        name: String,
        /// The key.
        key: &'static str,
    },
    /// A pattern lvcs-doc's `paths` matcher refuses.
    Pattern {
        /// The entry's name.
        name: String,
        /// The key holding it.
        key: &'static str,
        /// The matcher's refusal, which names the pattern.
        error: PathSetError,
    },
    /// `writes` matches a file at the root that is never derived
    /// (`lvcs.toml`, `.gitignore`, `.lvcsignore`, or `.gitattributes`, row 179's
    /// `NEVER_DERIVED`).
    NeverDerived {
        /// The entry's name.
        name: String,
        /// The root file matched.
        file: &'static str,
    },
    /// `secrets` lists more than `ACTION_SECRETS_MAX` names.
    TooManySecrets {
        /// The entry's name.
        name: String,
    },
    /// A `secrets` name [`secret_name_allowed`] refuses.
    SecretName {
        /// The entry's name.
        name: String,
        /// The secret name refused.
        secret: String,
    },
    /// `timeout_secs` is 0.
    ZeroTimeout {
        /// The entry's name.
        name: String,
    },
}

/// The variables the sandbox sets itself, which a secret name may not be
/// (decision 141).
pub const ENV_RESERVED: [&str; 5] = ["PATH", "HOME", "CARGO_HOME", "TMPDIR", "LANG"];

/// The prefixes a secret name may not have: the dynamic loader's and
/// LVCS's own (decision 141).
pub const ENV_RESERVED_PREFIXES: [&str; 2] = ["LD_", "LVCS_"];