Skip to main content

ci_selfcheck/
ci_selfcheck.rs

1//! `cargo run --example ci_selfcheck`
2//!
3//! Build the report once, write it BOTH to stdout (the job log) AND to a
4//! tmpfile on the runner, then read that tmpfile back and drop it into the
5//! job summary as a single code block. No shell, no prose. Exits 1 only on a
6//! real round-trip mismatch.
7
8use std::fmt::Write as _;
9use std::path::PathBuf;
10use std::process::ExitCode;
11
12use actions_rs::{Annotation, AnnotationKind, AnnotationSpan, Summary, output};
13
14fn read_env_file(var: &str) -> Option<String> {
15    std::fs::read_to_string(PathBuf::from(std::env::var_os(var)?)).ok()
16}
17
18fn main() -> ExitCode {
19    let notice = Annotation::new()
20        .file("examples/ci_selfcheck.rs")
21        .span(AnnotationSpan::Line {
22            start: 18,
23            end: Some(24),
24        })
25        .title("ci_selfcheck")
26        .command(
27            AnnotationKind::Notice,
28            "actions-rs self-check ran in this job",
29        );
30    let warning = Annotation::new()
31        .file("src/summary.rs")
32        .span(AnnotationSpan::Column {
33            line: 112,
34            start: 5,
35            end: None,
36        })
37        .title("example warning")
38        .command(
39            AnnotationKind::Warning,
40            "ranged warning annotation covering Summary::code_block",
41        );
42    notice.issue();
43    warning.issue();
44
45    if let Err(e) = output::set_output("answer", "42\nwith newline") {
46        eprintln!("::error::set_output: {e}");
47        return ExitCode::FAILURE;
48    }
49    if let Err(e) = output::export_var("DEMO_FLAG", true)
50        && !matches!(
51            e,
52            actions_rs::Error::UnavailableFileCommand {
53                var: "GITHUB_ENV",
54                ..
55            }
56        )
57    {
58        eprintln!("::error::export_var: {e}");
59        return ExitCode::FAILURE;
60    }
61
62    let gh_output = read_env_file("GITHUB_OUTPUT").unwrap_or_else(|| "<local: unset>".into());
63    let gh_env = read_env_file("GITHUB_ENV").unwrap_or_else(|| "<local: unset>".into());
64
65    // Build the whole report once.
66    let mut report = String::new();
67    for (label, body) in [
68        ("workflow commands (stdout)", format!("{notice}\n{warning}")),
69        ("GITHUB_OUTPUT", gh_output.clone()),
70        ("GITHUB_ENV", gh_env.clone()),
71    ] {
72        let _ = write!(report, "===== {label} =====\n{body}\n");
73    }
74
75    // 1. normal out (the job log).
76    print!("{report}");
77
78    // 2. a tmpfile on the runner.
79    let tmp = std::env::var_os("RUNNER_TEMP")
80        .map(PathBuf::from)
81        .unwrap_or_else(std::env::temp_dir)
82        .join("ci_selfcheck.report.txt");
83    if let Err(e) = std::fs::write(&tmp, &report) {
84        eprintln!("::error::tmpfile write: {e}");
85        return ExitCode::FAILURE;
86    }
87
88    // 3. read the tmpfile back, drop it into the summary as one code block.
89    let captured = match std::fs::read_to_string(&tmp) {
90        Ok(c) => c,
91        Err(e) => {
92            eprintln!("::error::tmpfile read: {e}");
93            return ExitCode::FAILURE;
94        }
95    };
96    let mut summary = Summary::new();
97    summary
98        .heading("actions-rs ci_selfcheck", 2)
99        .code_block(&captured, None);
100    if let Err(e) = summary.write_overwrite() {
101        eprintln!("::error::summary.write_overwrite: {e}");
102        return ExitCode::FAILURE;
103    }
104
105    // round-trip assertions (only meaningful when the runner set the files).
106    let mut exit = ExitCode::SUCCESS;
107    if std::env::var_os("GITHUB_OUTPUT").is_some() {
108        for (var, needle, hay) in [
109            ("GITHUB_OUTPUT", "answer<<", &gh_output),
110            ("GITHUB_ENV", "DEMO_FLAG<<", &gh_env),
111        ] {
112            if !hay.contains(needle) {
113                eprintln!("::error::{var} missing {needle:?}");
114                exit = ExitCode::FAILURE;
115            }
116        }
117    }
118    exit
119}