actions_rs/error.rs
1//! Error type for fallible operations (environment-file writes, typed input parsing, oversized job summaries).
2//!
3//! Pure stdout workflow commands (annotations, groups, masking) never return an error — see the [`crate::log`] module.
4
5use std::fmt;
6use std::path::PathBuf;
7
8/// Errors produced by fallible `actions-rs` operations.
9///
10/// `#[non_exhaustive]` so new variants can be added without a breaking change.
11///
12/// # Examples
13///
14/// ```
15/// use actions_rs::Error;
16///
17/// // Reserved names are rejected before any write happens.
18/// let err = actions_rs::output::export_var("GITHUB_TOKEN", "x").unwrap_err();
19/// assert!(matches!(err, Error::ReservedName(_)));
20/// // `Display` is human-readable.
21/// assert!(err.to_string().contains("reserved"));
22/// ```
23#[derive(Debug)]
24#[non_exhaustive]
25pub enum Error {
26 /// An underlying I/O error while reading or appending an environment file.
27 Io(std::io::Error),
28 /// The runner did not provide the required environment-file path for an
29 /// operation whose stdout command fallback has been retired.
30 UnavailableFileCommand {
31 /// The environment variable that should point at the file.
32 var: &'static str,
33 /// The attempted operation (for diagnostics).
34 operation: &'static str,
35 },
36 /// The environment-file variable pointed at a path that does not exist.
37 ///
38 /// GitHub sets these (`GITHUB_ENV`, `GITHUB_OUTPUT`, ...) to a real file;
39 /// if the variable is present but the file is missing the runner state is
40 /// broken and we surface it rather than silently dropping the write.
41 MissingEnvFile {
42 /// The environment variable name (e.g. `GITHUB_OUTPUT`).
43 var: &'static str,
44 /// The path the variable pointed at.
45 path: PathBuf,
46 },
47 /// The randomly generated heredoc delimiter collided with the key or value
48 /// being written. Astronomically unlikely; retrying will pick a fresh
49 /// delimiter. Mirrors `@actions/core`, which also errors in this case.
50 DelimiterCollision,
51 /// Attempted to export a reserved variable via [`crate::output::export_var`]
52 /// (`GITHUB_*`, `RUNNER_*`, or `NODE_OPTIONS`). The runner forbids this.
53 ReservedName(String),
54 /// A boolean input did not match the strict YAML 1.2 core schema
55 /// (`true|True|TRUE|false|False|FALSE`).
56 InvalidBool {
57 /// The input name that was queried.
58 name: String,
59 /// The offending raw value.
60 value: String,
61 },
62 /// A required input was absent or empty.
63 MissingRequiredInput(String),
64 /// A typed input could not be parsed into the requested type.
65 ParseInput {
66 /// The input name that was queried.
67 name: String,
68 /// A human-readable reason from the type's `FromStr` implementation.
69 reason: String,
70 },
71 /// The job summary buffer exceeded GitHub's 1 MiB per-step limit.
72 SummaryTooLarge {
73 /// The size of the buffer that was rejected, in bytes.
74 bytes: usize,
75 },
76 /// A key or path contained a carriage return or line feed, which would
77 /// corrupt the line-oriented environment-file syntax (and could inject
78 /// extra entries). Rejected before anything is written.
79 InvalidName {
80 /// The offending value.
81 name: String,
82 /// Why it was rejected.
83 reason: &'static str,
84 },
85}
86
87impl fmt::Display for Error {
88 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
89 match self {
90 Error::Io(e) => write!(f, "i/o error: {e}"),
91 Error::UnavailableFileCommand { var, operation } => write!(
92 f,
93 "`{operation}` requires `{var}`; GitHub retired the stdout fallback for this operation"
94 ),
95 Error::MissingEnvFile { var, path } => {
96 write!(f, "{var} points at missing file: {}", path.display())
97 }
98 Error::DelimiterCollision => {
99 f.write_str("generated heredoc delimiter collided with content")
100 }
101 Error::ReservedName(name) => {
102 write!(f, "`{name}` is a reserved variable and cannot be exported")
103 }
104 Error::InvalidBool { name, value } => write!(
105 f,
106 "input `{name}` is not a valid boolean (got {value:?}); \
107 expected one of true|True|TRUE|false|False|FALSE"
108 ),
109 Error::MissingRequiredInput(name) => {
110 write!(f, "required input `{name}` was not supplied")
111 }
112 Error::ParseInput { name, reason } => {
113 write!(f, "could not parse input `{name}`: {reason}")
114 }
115 Error::SummaryTooLarge { bytes } => write!(
116 f,
117 "job summary is {bytes} bytes, exceeding the 1 MiB per-step limit"
118 ),
119 Error::InvalidName { name, reason } => {
120 write!(f, "invalid name {name:?}: {reason}")
121 }
122 }
123 }
124}
125
126impl std::error::Error for Error {
127 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
128 match self {
129 Error::Io(e) => Some(e),
130 _ => None,
131 }
132 }
133}
134
135impl From<std::io::Error> for Error {
136 fn from(e: std::io::Error) -> Self {
137 Error::Io(e)
138 }
139}
140
141/// Convenience alias for results returned by fallible `actions-rs` operations.
142pub type Result<T> = std::result::Result<T, Error>;