Skip to main content

actions_rs/
log.rs

1//! Logging, grouping, masking and command-flow control.
2//!
3//! Everything here writes to stdout (the runner's command channel).\
4//! A failed stdout write inside an action is unrecoverable, so these functions are intentionally
5//! **infallible** — mirroring `@actions/core`.\
6//! Fallible operations live in [`crate::output`] and [`crate::summary`].
7
8use std::io::{self, Write};
9use std::sync::atomic::{AtomicBool, Ordering};
10
11use crate::command::WorkflowCommand;
12use crate::env;
13
14/// Process-global failure flag, the Rust analogue of `@actions/core`'s `process.exitCode = ExitCode.Failure`.
15/// Set by [`set_failed`], read by [`exit_code`] / [`is_failed`].
16static FAILED: AtomicBool = AtomicBool::new(false);
17
18fn emit(cmd: &WorkflowCommand) {
19    cmd.issue();
20}
21
22/// Write a plain line to the log (no annotation).
23/// Equivalent to `println!`, provided for symmetry with the other log functions.
24///
25/// # Examples
26///
27/// ```
28/// actions_rs::log::info("starting build");
29/// ```
30pub fn info(message: impl AsRef<str>) {
31    let _ = writeln!(io::stdout().lock(), "{}", message.as_ref());
32}
33
34/// Emit a `::debug::` message.
35/// Only visible when step-debug logging is enabled (the `ACTIONS_STEP_DEBUG` secret, surfaced as `RUNNER_DEBUG=1`).
36///
37/// # Examples
38///
39/// ```
40/// actions_rs::log::debug("cache key = v2-linux");
41/// ```
42pub fn debug(message: impl Into<String>) {
43    emit(&WorkflowCommand::new("debug").message(message));
44}
45
46/// Emit a `::notice::` annotation with no location.
47/// For located annotations use [`crate::Annotation`].
48///
49/// # Examples
50///
51/// ```
52/// actions_rs::log::notice("published 3 artifacts");
53/// ```
54pub fn notice(message: impl Into<String>) {
55    emit(&WorkflowCommand::new("notice").message(message));
56}
57
58/// Emit a `::warning::` annotation with no location.
59///
60/// # Examples
61///
62/// ```
63/// actions_rs::log::warning("deprecated input `path`; use `dir`");
64/// ```
65pub fn warning(message: impl Into<String>) {
66    emit(&WorkflowCommand::new("warning").message(message));
67}
68
69/// Emit an `::error::` annotation with no location.
70///
71/// # Examples
72///
73/// ```
74/// actions_rs::log::error("manifest checksum mismatch");
75/// ```
76pub fn error(message: impl Into<String>) {
77    emit(&WorkflowCommand::new("error").message(message));
78}
79
80/// Whether step-debug logging is enabled (`RUNNER_DEBUG == "1"`).
81///
82/// # Examples
83///
84/// ```
85/// if actions_rs::log::is_debug() {
86///     actions_rs::log::debug("verbose diagnostics enabled");
87/// }
88/// ```
89#[must_use]
90pub fn is_debug() -> bool {
91    env::is_debug()
92}
93
94/// Mask `value` in all subsequent log output (`::add-mask::`).
95///
96/// Note this only affects output produced *after* the call;
97/// anything already logged is not retroactively masked.
98///
99/// # Examples
100///
101/// ```
102/// let token = "ghp_example";
103/// actions_rs::log::mask(token);
104/// // Any later log line containing `ghp_example` is shown as `***`.
105/// ```
106pub fn mask(value: impl Into<String>) {
107    emit(&WorkflowCommand::new("add-mask").message(value));
108}
109
110/// Alias for [`mask`], named after `@actions/core`'s `setSecret`.
111///
112/// # Examples
113///
114/// ```
115/// actions_rs::log::set_secret(std::env::var("API_KEY").unwrap_or_default());
116/// ```
117pub fn set_secret(value: impl Into<String>) {
118    mask(value);
119}
120
121/// Mark the action as failed: emit `message` as an `::error::` annotation and set the process-global failure flag.
122///
123/// This mirrors `@actions/core`'s `setFailed`, which sets `process.exitCode = 1` *without* exiting — the step runs to completion (allowing cleanup) and then fails.
124/// Rust has no settable deferred process exit code, so the deferred part is realised by returning [`exit_code`] from `main`:
125///
126/// ```no_run
127/// use std::process::ExitCode;
128/// fn main() -> ExitCode {
129///     ghactions_doctest();
130///     actions_rs::log::exit_code() // Failure iff set_failed was called
131/// }
132/// # fn ghactions_doctest() {}
133/// ```
134///
135/// For immediate termination instead, use [`fail_now`].
136pub fn set_failed(message: impl Into<String>) {
137    error(message);
138    FAILED.store(true, Ordering::SeqCst);
139}
140
141/// Whether [`set_failed`] has been called in this process.
142///
143/// # Examples
144///
145/// ```
146/// assert!(!actions_rs::log::is_failed());
147/// actions_rs::log::set_failed("step failed");
148/// assert!(actions_rs::log::is_failed());
149/// ```
150#[must_use]
151pub fn is_failed() -> bool {
152    FAILED.load(Ordering::SeqCst)
153}
154
155/// The process exit code to return from `main`: [`ExitCode::FAILURE`] if [`set_failed`] was called, otherwise [`ExitCode::SUCCESS`].
156/// This is the faithful analogue of `@actions/core`'s deferred `process.exitCode`.
157///
158/// [`ExitCode::FAILURE`]: std::process::ExitCode::FAILURE
159/// [`ExitCode::SUCCESS`]: std::process::ExitCode::SUCCESS
160///
161/// # Examples
162///
163/// ```no_run
164/// use std::process::ExitCode;
165/// fn main() -> ExitCode {
166///     // ... action body; call `set_failed` on any recoverable failure ...
167///     actions_rs::log::exit_code()
168/// }
169/// ```
170#[must_use]
171pub fn exit_code() -> std::process::ExitCode {
172    if is_failed() {
173        std::process::ExitCode::FAILURE
174    } else {
175        std::process::ExitCode::SUCCESS
176    }
177}
178
179/// Emit `message` as an error annotation and immediately exit the process with code `1`.
180/// Convenience wrapper around [`set_failed`] that does not wait for `main` to return [`exit_code`].
181///
182/// # Examples
183///
184/// ```no_run
185/// let Some(input) = std::env::var_os("INPUT_TARGET") else {
186///     actions_rs::log::fail_now("required input `target` missing");
187/// };
188/// ```
189pub fn fail_now(message: impl Into<String>) -> ! {
190    set_failed(message);
191    std::process::exit(1)
192}
193
194/// Toggle command echoing (`::echo::on` / `::echo::off`).
195///
196/// # Examples
197///
198/// ```
199/// actions_rs::log::echo(true);  // runner echoes subsequent workflow commands
200/// actions_rs::log::echo(false);
201/// ```
202pub fn echo(on: bool) {
203    emit(&WorkflowCommand::new("echo").message(if on { "on" } else { "off" }));
204}
205
206/// Begin a collapsible log group.
207/// Prefer [`group`], which closes the group automatically even on panic.
208///
209/// # Examples
210///
211/// ```
212/// actions_rs::log::start_group("install");
213/// actions_rs::log::info("downloading toolchain");
214/// actions_rs::log::end_group();
215/// ```
216pub fn start_group(name: impl Into<String>) {
217    emit(&WorkflowCommand::new("group").message(name));
218}
219
220/// End the current collapsible log group.
221///
222/// # Examples
223///
224/// ```
225/// actions_rs::log::start_group("tests");
226/// actions_rs::log::info("running");
227/// actions_rs::log::end_group();
228/// ```
229pub fn end_group() {
230    emit(&WorkflowCommand::new("endgroup"));
231}
232
233/// RAII guard returned by [`group_guard`]; emits `::endgroup::` on drop.
234///
235/// # Examples
236///
237/// ```
238/// {
239///     let _g = actions_rs::log::group_guard("lint");
240///     actions_rs::log::info("clippy clean");
241/// } // `::endgroup::` emitted here
242/// ```
243#[must_use = "the group ends when this guard is dropped"]
244pub struct GroupGuard(());
245
246impl Drop for GroupGuard {
247    fn drop(&mut self) {
248        end_group();
249    }
250}
251
252/// Start a group and return a guard that closes it when dropped (including on panic / early return).
253///
254/// # Examples
255///
256/// ```
257/// fn step() -> Result<(), &'static str> {
258///     let _g = actions_rs::log::group_guard("deploy");
259///     // early return still closes the group via the guard's Drop
260///     Err("boom")
261/// }
262/// assert!(step().is_err());
263/// ```
264pub fn group_guard(name: impl Into<String>) -> GroupGuard {
265    start_group(name);
266    GroupGuard(())
267}
268
269/// Run `f` inside a collapsible group, closing the group afterwards even if `f` panics.
270/// Returns whatever `f` returns.
271///
272/// # Examples
273///
274/// ```no_run
275/// let built = actions_rs::log::group("build", || {
276///     actions_rs::log::info("compiling...");
277///     6 * 7
278/// });
279/// assert_eq!(built, 42);
280/// ```
281pub fn group<R>(name: impl Into<String>, f: impl FnOnce() -> R) -> R {
282    let _guard = group_guard(name);
283    f()
284}
285
286/// RAII guard returned by [`stop_commands`];
287/// emits the resume token on drop, re-enabling workflow-command processing.
288///
289/// # Examples
290///
291/// ```
292/// {
293///     let _g = actions_rs::log::stop_commands();
294///     println!("::not-a-command:: this line is not interpreted");
295/// } // command processing resumes here
296/// ```
297#[must_use = "command processing resumes when this guard is dropped"]
298pub struct StopGuard {
299    token: String,
300}
301
302impl Drop for StopGuard {
303    fn drop(&mut self) {
304        // Resume: the command name *is* the token and carries no message. The
305        // token is a hex-suffixed identifier so it needs no escaping, and it
306        // is not `&'static`, so write it directly rather than via
307        // [`WorkflowCommand`].
308        let _ = writeln!(io::stdout().lock(), "::{}::", self.token);
309    }
310}
311
312/// Stop the runner from interpreting workflow commands until the returned guard is dropped.
313/// Useful when logging untrusted text that might otherwise be parsed as a `::command::`.
314///
315/// The stop/resume token is randomly generated so untrusted content cannot guess it and resume command processing early.
316///
317/// # Examples
318///
319/// ```
320/// let untrusted = "::error::spoofed";
321/// {
322///     let _g = actions_rs::log::stop_commands();
323///     actions_rs::log::info(untrusted); // logged literally, not interpreted
324/// }
325/// ```
326pub fn stop_commands() -> StopGuard {
327    let token = crate::file_command::random_token();
328    emit(&WorkflowCommand::new("stop-commands").message(token.clone()));
329    StopGuard { token }
330}
331
332#[cfg(test)]
333mod tests {
334    use super::*;
335
336    #[test]
337    fn group_runs_and_returns() {
338        let v = group("build", || 21 * 2);
339        assert_eq!(v, 42);
340    }
341
342    #[test]
343    fn group_closes_on_panic() {
344        let r = std::panic::catch_unwind(|| {
345            group("boom", || panic!("inside"));
346        });
347        assert!(r.is_err(), "panic should propagate after group closes");
348    }
349}