Skip to main content

Summary

Struct Summary 

Source
pub struct Summary { /* private fields */ }
Expand description

Accumulating job-summary builder. Chain the builder methods, then write (append) or write_overwrite. Building is pure and inspectable via stringify; only the write* methods touch GITHUB_STEP_SUMMARY.

§Examples

use actions_rs::Summary;

let mut s = Summary::new();
s.heading("Build", 2)
    .code_block("cargo test", Some("sh"));

assert_eq!(
    s.stringify(),
    "<h2>Build</h2>\n<pre lang=\"sh\"><code>cargo test</code></pre>\n"
);

// In a real action you would then persist it:
// s.write()?;  // appends to $GITHUB_STEP_SUMMARY

Implementations§

Source§

impl Summary

Source

pub fn new() -> Self

Create an empty summary buffer.

§Examples
let s = actions_rs::Summary::new();
assert!(s.is_empty());
Examples found in repository?
examples/demo.rs (line 62)
13fn main() {
14    log::info(format!(
15        "in GitHub Actions: {} | CI: {} | step-debug: {}",
16        env::is_github_actions(),
17        env::is_ci(),
18        log::is_debug()
19    ));
20
21    let ctx = actions_rs::Context::new();
22    log::info(format!(
23        "repo={:?} ref={:?} sha={:?}",
24        ctx.repository(),
25        ctx.ref_name(),
26        ctx.sha()
27    ));
28
29    // Located annotation with a line range — should print:
30    // ::warning title=demo,file=src/lib.rs,line=10,endLine=12::heads up
31    Annotation::new()
32        .file("src/lib.rs")
33        .line(10)
34        .end_line(12)
35        .title("demo")
36        .warning("heads up: this span looks suspicious");
37
38    // Escaping check: newline in data, colon/comma in a property.
39    Annotation::new()
40        .title("type: mismatch, really")
41        .error("line one\nline two");
42
43    let total = log::group("expensive step", || {
44        log::info("...working...");
45        2 + 2
46    });
47    log::info(format!("group returned {total}"));
48
49    actions_rs::warning!("formatted macro: {} items left", 7);
50
51    output::set_output("answer", 42).expect("set_output");
52    match output::export_var("DEMO_FLAG", true) {
53        Ok(()) => {}
54        Err(actions_rs::Error::UnavailableFileCommand {
55            var: "GITHUB_ENV", ..
56        }) => {
57            log::info("GITHUB_ENV unset; skipping export_var in local demo");
58        }
59        Err(err) => panic!("export_var: {err}"),
60    }
61
62    let mut summary = Summary::new();
63    summary
64        .heading("Demo Report", 2)
65        .raw("Built by the `demo` example.", true)
66        .table([
67            vec![Cell::header("Check"), Cell::header("Result")],
68            vec![Cell::new("clippy"), Cell::new("pass")],
69            vec![Cell::new("tests"), Cell::new("36 pass")],
70        ])
71        .code_block("cargo test", Some("sh"));
72    summary.write().expect("write summary");
73    log::info("summary written (if GITHUB_STEP_SUMMARY was set)");
74}
More examples
Hide additional examples
examples/ci_selfcheck.rs (line 96)
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}
Source

pub fn raw(&mut self, text: impl AsRef<str>, eol: bool) -> &mut Self

Append raw text without HTML escaping. When eol is true a newline is appended after it.

§Safety

This is the one builder method that does not escape & < > ". Passing untrusted input here is an HTML-injection vector. Use it only for trusted or already-escaped markup; for arbitrary text prefer Summary::code_block / Summary::heading etc., which escape.

§Examples
let mut s = actions_rs::Summary::new();
s.raw("<div class=\"trusted\">ok</div>", true);
assert_eq!(s.stringify(), "<div class=\"trusted\">ok</div>\n");
Examples found in repository?
examples/demo.rs (line 65)
13fn main() {
14    log::info(format!(
15        "in GitHub Actions: {} | CI: {} | step-debug: {}",
16        env::is_github_actions(),
17        env::is_ci(),
18        log::is_debug()
19    ));
20
21    let ctx = actions_rs::Context::new();
22    log::info(format!(
23        "repo={:?} ref={:?} sha={:?}",
24        ctx.repository(),
25        ctx.ref_name(),
26        ctx.sha()
27    ));
28
29    // Located annotation with a line range — should print:
30    // ::warning title=demo,file=src/lib.rs,line=10,endLine=12::heads up
31    Annotation::new()
32        .file("src/lib.rs")
33        .line(10)
34        .end_line(12)
35        .title("demo")
36        .warning("heads up: this span looks suspicious");
37
38    // Escaping check: newline in data, colon/comma in a property.
39    Annotation::new()
40        .title("type: mismatch, really")
41        .error("line one\nline two");
42
43    let total = log::group("expensive step", || {
44        log::info("...working...");
45        2 + 2
46    });
47    log::info(format!("group returned {total}"));
48
49    actions_rs::warning!("formatted macro: {} items left", 7);
50
51    output::set_output("answer", 42).expect("set_output");
52    match output::export_var("DEMO_FLAG", true) {
53        Ok(()) => {}
54        Err(actions_rs::Error::UnavailableFileCommand {
55            var: "GITHUB_ENV", ..
56        }) => {
57            log::info("GITHUB_ENV unset; skipping export_var in local demo");
58        }
59        Err(err) => panic!("export_var: {err}"),
60    }
61
62    let mut summary = Summary::new();
63    summary
64        .heading("Demo Report", 2)
65        .raw("Built by the `demo` example.", true)
66        .table([
67            vec![Cell::header("Check"), Cell::header("Result")],
68            vec![Cell::new("clippy"), Cell::new("pass")],
69            vec![Cell::new("tests"), Cell::new("36 pass")],
70        ])
71        .code_block("cargo test", Some("sh"));
72    summary.write().expect("write summary");
73    log::info("summary written (if GITHUB_STEP_SUMMARY was set)");
74}
Source

pub fn eol(&mut self) -> &mut Self

Append a newline.

§Examples
let mut s = actions_rs::Summary::new();
s.raw("a", false).eol().raw("b", false);
assert_eq!(s.stringify(), "a\nb");
Source

pub fn heading(&mut self, text: impl Into<SummaryText>, level: u8) -> &mut Self

Append an <h1><h6> heading (level clamped to 1..=6). Text is escaped unless you pass SummaryText::html.

§Examples
let mut s = actions_rs::Summary::new();
s.heading("Result", 9); // level clamps to 6
assert_eq!(s.stringify(), "<h6>Result</h6>\n");
Examples found in repository?
examples/demo.rs (line 64)
13fn main() {
14    log::info(format!(
15        "in GitHub Actions: {} | CI: {} | step-debug: {}",
16        env::is_github_actions(),
17        env::is_ci(),
18        log::is_debug()
19    ));
20
21    let ctx = actions_rs::Context::new();
22    log::info(format!(
23        "repo={:?} ref={:?} sha={:?}",
24        ctx.repository(),
25        ctx.ref_name(),
26        ctx.sha()
27    ));
28
29    // Located annotation with a line range — should print:
30    // ::warning title=demo,file=src/lib.rs,line=10,endLine=12::heads up
31    Annotation::new()
32        .file("src/lib.rs")
33        .line(10)
34        .end_line(12)
35        .title("demo")
36        .warning("heads up: this span looks suspicious");
37
38    // Escaping check: newline in data, colon/comma in a property.
39    Annotation::new()
40        .title("type: mismatch, really")
41        .error("line one\nline two");
42
43    let total = log::group("expensive step", || {
44        log::info("...working...");
45        2 + 2
46    });
47    log::info(format!("group returned {total}"));
48
49    actions_rs::warning!("formatted macro: {} items left", 7);
50
51    output::set_output("answer", 42).expect("set_output");
52    match output::export_var("DEMO_FLAG", true) {
53        Ok(()) => {}
54        Err(actions_rs::Error::UnavailableFileCommand {
55            var: "GITHUB_ENV", ..
56        }) => {
57            log::info("GITHUB_ENV unset; skipping export_var in local demo");
58        }
59        Err(err) => panic!("export_var: {err}"),
60    }
61
62    let mut summary = Summary::new();
63    summary
64        .heading("Demo Report", 2)
65        .raw("Built by the `demo` example.", true)
66        .table([
67            vec![Cell::header("Check"), Cell::header("Result")],
68            vec![Cell::new("clippy"), Cell::new("pass")],
69            vec![Cell::new("tests"), Cell::new("36 pass")],
70        ])
71        .code_block("cargo test", Some("sh"));
72    summary.write().expect("write summary");
73    log::info("summary written (if GITHUB_STEP_SUMMARY was set)");
74}
More examples
Hide additional examples
examples/ci_selfcheck.rs (line 98)
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}
Source

pub fn code_block( &mut self, code: impl AsRef<str>, lang: Option<&str>, ) -> &mut Self

Append a fenced <pre><code> block with an optional language hint.

§Examples
let mut s = actions_rs::Summary::new();
s.code_block("cargo test", Some("sh"));
assert_eq!(s.stringify(), "<pre lang=\"sh\"><code>cargo test</code></pre>\n");
Examples found in repository?
examples/demo.rs (line 71)
13fn main() {
14    log::info(format!(
15        "in GitHub Actions: {} | CI: {} | step-debug: {}",
16        env::is_github_actions(),
17        env::is_ci(),
18        log::is_debug()
19    ));
20
21    let ctx = actions_rs::Context::new();
22    log::info(format!(
23        "repo={:?} ref={:?} sha={:?}",
24        ctx.repository(),
25        ctx.ref_name(),
26        ctx.sha()
27    ));
28
29    // Located annotation with a line range — should print:
30    // ::warning title=demo,file=src/lib.rs,line=10,endLine=12::heads up
31    Annotation::new()
32        .file("src/lib.rs")
33        .line(10)
34        .end_line(12)
35        .title("demo")
36        .warning("heads up: this span looks suspicious");
37
38    // Escaping check: newline in data, colon/comma in a property.
39    Annotation::new()
40        .title("type: mismatch, really")
41        .error("line one\nline two");
42
43    let total = log::group("expensive step", || {
44        log::info("...working...");
45        2 + 2
46    });
47    log::info(format!("group returned {total}"));
48
49    actions_rs::warning!("formatted macro: {} items left", 7);
50
51    output::set_output("answer", 42).expect("set_output");
52    match output::export_var("DEMO_FLAG", true) {
53        Ok(()) => {}
54        Err(actions_rs::Error::UnavailableFileCommand {
55            var: "GITHUB_ENV", ..
56        }) => {
57            log::info("GITHUB_ENV unset; skipping export_var in local demo");
58        }
59        Err(err) => panic!("export_var: {err}"),
60    }
61
62    let mut summary = Summary::new();
63    summary
64        .heading("Demo Report", 2)
65        .raw("Built by the `demo` example.", true)
66        .table([
67            vec![Cell::header("Check"), Cell::header("Result")],
68            vec![Cell::new("clippy"), Cell::new("pass")],
69            vec![Cell::new("tests"), Cell::new("36 pass")],
70        ])
71        .code_block("cargo test", Some("sh"));
72    summary.write().expect("write summary");
73    log::info("summary written (if GITHUB_STEP_SUMMARY was set)");
74}
More examples
Hide additional examples
examples/ci_selfcheck.rs (line 99)
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}
Source

pub fn list<I, S>(&mut self, items: I, ordered: bool) -> &mut Self
where I: IntoIterator<Item = S>, S: Into<SummaryText>,

Append a <ul> (or <ol> when ordered) of items.

§Examples
let mut s = actions_rs::Summary::new();
s.list(["a", "b"], false);
assert_eq!(s.stringify(), "<ul><li>a</li><li>b</li></ul>\n");
Source

pub fn table(&mut self, rows: impl IntoIterator<Item = Vec<Cell>>) -> &mut Self

Append a <table>. Each row is a list of Cells.

§Examples
use actions_rs::{Cell, Summary};
let mut s = Summary::new();
s.table([vec![Cell::header("k"), Cell::new("v")]]);
assert_eq!(
    s.stringify(),
    "<table><tr><th colspan=\"1\" rowspan=\"1\">k</th>\
     <td colspan=\"1\" rowspan=\"1\">v</td></tr></table>\n"
);
Examples found in repository?
examples/demo.rs (lines 66-70)
13fn main() {
14    log::info(format!(
15        "in GitHub Actions: {} | CI: {} | step-debug: {}",
16        env::is_github_actions(),
17        env::is_ci(),
18        log::is_debug()
19    ));
20
21    let ctx = actions_rs::Context::new();
22    log::info(format!(
23        "repo={:?} ref={:?} sha={:?}",
24        ctx.repository(),
25        ctx.ref_name(),
26        ctx.sha()
27    ));
28
29    // Located annotation with a line range — should print:
30    // ::warning title=demo,file=src/lib.rs,line=10,endLine=12::heads up
31    Annotation::new()
32        .file("src/lib.rs")
33        .line(10)
34        .end_line(12)
35        .title("demo")
36        .warning("heads up: this span looks suspicious");
37
38    // Escaping check: newline in data, colon/comma in a property.
39    Annotation::new()
40        .title("type: mismatch, really")
41        .error("line one\nline two");
42
43    let total = log::group("expensive step", || {
44        log::info("...working...");
45        2 + 2
46    });
47    log::info(format!("group returned {total}"));
48
49    actions_rs::warning!("formatted macro: {} items left", 7);
50
51    output::set_output("answer", 42).expect("set_output");
52    match output::export_var("DEMO_FLAG", true) {
53        Ok(()) => {}
54        Err(actions_rs::Error::UnavailableFileCommand {
55            var: "GITHUB_ENV", ..
56        }) => {
57            log::info("GITHUB_ENV unset; skipping export_var in local demo");
58        }
59        Err(err) => panic!("export_var: {err}"),
60    }
61
62    let mut summary = Summary::new();
63    summary
64        .heading("Demo Report", 2)
65        .raw("Built by the `demo` example.", true)
66        .table([
67            vec![Cell::header("Check"), Cell::header("Result")],
68            vec![Cell::new("clippy"), Cell::new("pass")],
69            vec![Cell::new("tests"), Cell::new("36 pass")],
70        ])
71        .code_block("cargo test", Some("sh"));
72    summary.write().expect("write summary");
73    log::info("summary written (if GITHUB_STEP_SUMMARY was set)");
74}
Source

pub fn details( &mut self, label: impl Into<SummaryText>, content: impl Into<SummaryText>, ) -> &mut Self

Append a <details> block with a <summary> label. Both text nodes are escaped unless you pass SummaryText::html.

§Examples
let mut s = actions_rs::Summary::new();
s.details("logs", "all green");
assert_eq!(s.stringify(), "<details><summary>logs</summary>all green</details>\n");
Source

pub fn image( &mut self, src: impl AsRef<str>, alt: impl AsRef<str>, size: Option<(u32, u32)>, ) -> &mut Self

Append an <img>. size is an optional (width, height) in pixels.

§Examples
let mut s = actions_rs::Summary::new();
s.image("cov.svg", "coverage", Some((120, 20)));
assert_eq!(
    s.stringify(),
    "<img src=\"cov.svg\" alt=\"coverage\" width=\"120\" height=\"20\">\n"
);

Append an <a> link. The link text is escaped unless you pass SummaryText::html; href is always attribute-escaped.

§Examples
let mut s = actions_rs::Summary::new();
s.link("run", "https://example.com/run/1");
assert_eq!(s.stringify(), "<a href=\"https://example.com/run/1\">run</a>\n");
Source

pub fn quote( &mut self, text: impl Into<SummaryText>, cite: Option<&str>, ) -> &mut Self

Append a <blockquote> with an optional cite URL. Quote text is escaped unless you pass SummaryText::html.

§Examples
let mut s = actions_rs::Summary::new();
s.quote("ship it", None);
assert_eq!(s.stringify(), "<blockquote>ship it</blockquote>\n");
Source

pub fn separator(&mut self) -> &mut Self

Append an <hr>.

§Examples
let mut s = actions_rs::Summary::new();
s.separator();
assert_eq!(s.stringify(), "<hr>\n");
Source

pub fn break_(&mut self) -> &mut Self

Append a <br>.

§Examples
let mut s = actions_rs::Summary::new();
s.break_();
assert_eq!(s.stringify(), "<br>\n");
Source

pub fn stringify(&self) -> &str

The buffered summary content.

§Examples
let mut s = actions_rs::Summary::new();
s.heading("Hi", 1);
assert_eq!(s.stringify(), "<h1>Hi</h1>\n");
Source

pub fn is_empty(&self) -> bool

Whether nothing has been buffered yet.

§Examples
let mut s = actions_rs::Summary::new();
assert!(s.is_empty());
s.eol();
assert!(!s.is_empty());
Source

pub fn clear(&mut self) -> &mut Self

Clear the buffer (does not touch the file).

§Examples
let mut s = actions_rs::Summary::new();
s.heading("x", 1);
s.clear();
assert!(s.is_empty());
Source

pub fn write(&mut self) -> Result<()>

Append the buffer to the job summary file.

§Errors

Error::SummaryTooLarge if the buffer exceeds 1 MiB, or an I/O error.

§Examples
let mut s = actions_rs::Summary::new();
s.heading("Build passed", 2);
s.write()?; // appends to $GITHUB_STEP_SUMMARY
Examples found in repository?
examples/demo.rs (line 72)
13fn main() {
14    log::info(format!(
15        "in GitHub Actions: {} | CI: {} | step-debug: {}",
16        env::is_github_actions(),
17        env::is_ci(),
18        log::is_debug()
19    ));
20
21    let ctx = actions_rs::Context::new();
22    log::info(format!(
23        "repo={:?} ref={:?} sha={:?}",
24        ctx.repository(),
25        ctx.ref_name(),
26        ctx.sha()
27    ));
28
29    // Located annotation with a line range — should print:
30    // ::warning title=demo,file=src/lib.rs,line=10,endLine=12::heads up
31    Annotation::new()
32        .file("src/lib.rs")
33        .line(10)
34        .end_line(12)
35        .title("demo")
36        .warning("heads up: this span looks suspicious");
37
38    // Escaping check: newline in data, colon/comma in a property.
39    Annotation::new()
40        .title("type: mismatch, really")
41        .error("line one\nline two");
42
43    let total = log::group("expensive step", || {
44        log::info("...working...");
45        2 + 2
46    });
47    log::info(format!("group returned {total}"));
48
49    actions_rs::warning!("formatted macro: {} items left", 7);
50
51    output::set_output("answer", 42).expect("set_output");
52    match output::export_var("DEMO_FLAG", true) {
53        Ok(()) => {}
54        Err(actions_rs::Error::UnavailableFileCommand {
55            var: "GITHUB_ENV", ..
56        }) => {
57            log::info("GITHUB_ENV unset; skipping export_var in local demo");
58        }
59        Err(err) => panic!("export_var: {err}"),
60    }
61
62    let mut summary = Summary::new();
63    summary
64        .heading("Demo Report", 2)
65        .raw("Built by the `demo` example.", true)
66        .table([
67            vec![Cell::header("Check"), Cell::header("Result")],
68            vec![Cell::new("clippy"), Cell::new("pass")],
69            vec![Cell::new("tests"), Cell::new("36 pass")],
70        ])
71        .code_block("cargo test", Some("sh"));
72    summary.write().expect("write summary");
73    log::info("summary written (if GITHUB_STEP_SUMMARY was set)");
74}
Source

pub fn write_overwrite(&mut self) -> Result<()>

Overwrite the job summary file with the buffer.

§Errors

Error::SummaryTooLarge if the buffer exceeds 1 MiB, or an I/O error.

§Examples
let mut s = actions_rs::Summary::new();
s.heading("Replaces any prior summary", 2);
s.write_overwrite()?;
Examples found in repository?
examples/ci_selfcheck.rs (line 100)
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}

Trait Implementations§

Source§

impl Clone for Summary

Source§

fn clone(&self) -> Summary

Returns a duplicate of the value. Read more
1.0.0 (const: unstable) · Source§

fn clone_from(&mut self, source: &Self)

Performs copy-assignment from source. Read more
Source§

impl Debug for Summary

Source§

fn fmt(&self, f: &mut Formatter<'_>) -> Result

Formats the value using the given formatter. Read more
Source§

impl Default for Summary

Source§

fn default() -> Summary

Returns the “default value” for a type. Read more

Auto Trait Implementations§

Blanket Implementations§

Source§

impl<T> Any for T
where T: 'static + ?Sized,

Source§

fn type_id(&self) -> TypeId

Gets the TypeId of self. Read more
Source§

impl<T> Borrow<T> for T
where T: ?Sized,

Source§

fn borrow(&self) -> &T

Immutably borrows from an owned value. Read more
Source§

impl<T> BorrowMut<T> for T
where T: ?Sized,

Source§

fn borrow_mut(&mut self) -> &mut T

Mutably borrows from an owned value. Read more
Source§

impl<T> CloneToUninit for T
where T: Clone,

Source§

unsafe fn clone_to_uninit(&self, dest: *mut u8)

🔬This is a nightly-only experimental API. (clone_to_uninit)
Performs copy-assignment from self to dest. Read more
Source§

impl<T> From<T> for T

Source§

fn from(t: T) -> T

Returns the argument unchanged.

Source§

impl<T, U> Into<U> for T
where U: From<T>,

Source§

fn into(self) -> U

Calls U::from(self).

That is, this conversion is whatever the implementation of From<T> for U chooses to do.

Source§

impl<T> ToOwned for T
where T: Clone,

Source§

type Owned = T

The resulting type after obtaining ownership.
Source§

fn to_owned(&self) -> T

Creates owned data from borrowed data, usually by cloning. Read more
Source§

fn clone_into(&self, target: &mut T)

Uses borrowed data to replace owned data, usually by cloning. Read more
Source§

impl<T, U> TryFrom<U> for T
where U: Into<T>,

Source§

type Error = Infallible

The type returned in the event of a conversion error.
Source§

fn try_from(value: U) -> Result<T, <T as TryFrom<U>>::Error>

Performs the conversion.
Source§

impl<T, U> TryInto<U> for T
where U: TryFrom<T>,

Source§

type Error = <U as TryFrom<T>>::Error

The type returned in the event of a conversion error.
Source§

fn try_into(self) -> Result<U, <U as TryFrom<T>>::Error>

Performs the conversion.