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_"];