Skip to main content

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>;