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_SUMMARYImplementations§
Source§impl Summary
impl Summary
Sourcepub fn new() -> Self
pub fn new() -> Self
Create an empty summary buffer.
§Examples
let s = actions_rs::Summary::new();
assert!(s.is_empty());Examples found in repository?
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
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}Sourcepub fn raw(&mut self, text: impl AsRef<str>, eol: bool) -> &mut Self
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?
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}Sourcepub fn eol(&mut self) -> &mut Self
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");Sourcepub fn heading(&mut self, text: impl Into<SummaryText>, level: u8) -> &mut Self
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?
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
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}Sourcepub fn code_block(
&mut self,
code: impl AsRef<str>,
lang: Option<&str>,
) -> &mut Self
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?
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
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}Sourcepub fn list<I, S>(&mut self, items: I, ordered: bool) -> &mut Self
pub fn list<I, S>(&mut self, items: I, ordered: bool) -> &mut Self
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");Sourcepub fn table(&mut self, rows: impl IntoIterator<Item = Vec<Cell>>) -> &mut Self
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?
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}Sourcepub fn details(
&mut self,
label: impl Into<SummaryText>,
content: impl Into<SummaryText>,
) -> &mut Self
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");Sourcepub fn image(
&mut self,
src: impl AsRef<str>,
alt: impl AsRef<str>,
size: Option<(u32, u32)>,
) -> &mut Self
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"
);Sourcepub fn link(
&mut self,
text: impl Into<SummaryText>,
href: impl AsRef<str>,
) -> &mut Self
pub fn link( &mut self, text: impl Into<SummaryText>, href: impl AsRef<str>, ) -> &mut Self
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");Sourcepub fn quote(
&mut self,
text: impl Into<SummaryText>,
cite: Option<&str>,
) -> &mut Self
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");Sourcepub fn separator(&mut self) -> &mut Self
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");Sourcepub fn break_(&mut self) -> &mut Self
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");Sourcepub fn stringify(&self) -> &str
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");Sourcepub fn is_empty(&self) -> bool
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());Sourcepub fn clear(&mut self) -> &mut Self
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());Sourcepub fn write(&mut self) -> Result<()>
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_SUMMARYExamples found in repository?
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}Sourcepub fn write_overwrite(&mut self) -> Result<()>
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?
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}