pub struct Annotation { /* private fields */ }Expand description
Fluent builder for a located annotation.
All fields are optional — an empty Annotation simply produces a plain annotation with no location.
Build it, then emit with Annotation::notice, Annotation::warning or Annotation::error.
use actions_rs::Annotation;
let cmd = Annotation::new()
.file("src/lib.rs")
.line(10)
.end_line(12)
.title("clippy")
.command(actions_rs::AnnotationKind::Warning, "unused variable");
assert_eq!(
cmd.to_string(),
"::warning title=clippy,file=src/lib.rs,line=10,endLine=12::unused variable"
);Implementations§
Source§impl Annotation
impl Annotation
Sourcepub fn new() -> Self
pub fn new() -> Self
Create an empty annotation (no location, no title).
§Examples
use actions_rs::{Annotation, AnnotationKind};
let c = Annotation::new().command(AnnotationKind::Notice, "hi");
assert_eq!(c.to_string(), "::notice::hi");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 title(self, title: impl Into<String>) -> Self
pub fn title(self, title: impl Into<String>) -> Self
Set the annotation title shown in the GitHub UI.
§Examples
use actions_rs::{Annotation, AnnotationKind};
let c = Annotation::new().title("clippy").command(AnnotationKind::Warning, "w");
assert_eq!(c.to_string(), "::warning title=clippy::w");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 file(self, file: impl Into<String>) -> Self
pub fn file(self, file: impl Into<String>) -> Self
Set the file path the annotation refers to (relative to the workspace).
§Examples
use actions_rs::{Annotation, AnnotationKind};
let c = Annotation::new().file("src/lib.rs").command(AnnotationKind::Error, "e");
assert_eq!(c.to_string(), "::error file=src/lib.rs::e");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 line(self, line: u32) -> Self
pub fn line(self, line: u32) -> Self
Set the (1-based) start line.
§Examples
use actions_rs::{Annotation, AnnotationKind};
let c = Annotation::new().file("x").line(42).command(AnnotationKind::Warning, "w");
assert_eq!(c.to_string(), "::warning file=x,line=42::w");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 end_line(self, end_line: u32) -> Self
pub fn end_line(self, end_line: u32) -> Self
Set the (1-based) end line of a multi-line span.
§Examples
use actions_rs::{Annotation, AnnotationKind};
let c = Annotation::new().file("x").line(10).end_line(12)
.command(AnnotationKind::Warning, "w");
assert_eq!(c.to_string(), "::warning file=x,line=10,endLine=12::w");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 col(self, col: u32) -> Self
pub fn col(self, col: u32) -> Self
Set the (1-based) start column.
§Examples
use actions_rs::{Annotation, AnnotationKind};
let c = Annotation::new().file("x").line(7).col(3)
.command(AnnotationKind::Warning, "w");
assert_eq!(c.to_string(), "::warning file=x,line=7,col=3,endColumn=3::w");Sourcepub fn end_column(self, end_column: u32) -> Self
pub fn end_column(self, end_column: u32) -> Self
Set the (1-based) end column.
§Examples
use actions_rs::{Annotation, AnnotationKind};
let c = Annotation::new().file("x").line(7).col(3).end_column(9)
.command(AnnotationKind::Warning, "w");
assert_eq!(c.to_string(), "::warning file=x,line=7,col=3,endColumn=9::w");Sourcepub fn span(self, span: AnnotationSpan) -> Self
pub fn span(self, span: AnnotationSpan) -> Self
Replace the current location fields with a span that is valid by construction.
§Examples
use actions_rs::{Annotation, AnnotationKind, AnnotationSpan};
let c = Annotation::new()
.span(AnnotationSpan::Line { start: 4, end: Some(6) })
.command(AnnotationKind::Notice, "block");
assert_eq!(c.to_string(), "::notice line=4,endLine=6::block");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}Sourcepub fn command(
&self,
kind: AnnotationKind,
message: impl Into<String>,
) -> WorkflowCommand
pub fn command( &self, kind: AnnotationKind, message: impl Into<String>, ) -> WorkflowCommand
Build the WorkflowCommand for this annotation and message without emitting it.
Useful for testing or custom sinks.
Property order matches @actions/core: title, file, line, endLine, col, endColumn.
§Examples
use actions_rs::{Annotation, AnnotationKind};
// Inspect the wire form without writing to stdout.
let cmd = Annotation::new().file("a.rs").line(1).command(AnnotationKind::Error, "e");
assert_eq!(cmd.to_string(), "::error file=a.rs,line=1::e");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}Sourcepub fn notice(&self, message: impl Into<String>)
pub fn notice(&self, message: impl Into<String>)
Emit a ::notice:: annotation to stdout.
§Examples
use actions_rs::Annotation;
Annotation::new().file("README.md").line(1).notice("looks good");Sourcepub fn warning(&self, message: impl Into<String>)
pub fn warning(&self, message: impl Into<String>)
Emit a ::warning:: annotation to stdout.
§Examples
use actions_rs::Annotation;
Annotation::new().file("src/lib.rs").line(42).title("lint").warning("unused import");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 error(&self, message: impl Into<String>)
pub fn error(&self, message: impl Into<String>)
Emit an ::error:: annotation to stdout.
§Examples
use actions_rs::Annotation;
Annotation::new().file("src/main.rs").line(7).error("type mismatch");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}Trait Implementations§
Source§impl Clone for Annotation
impl Clone for Annotation
Source§fn clone(&self) -> Annotation
fn clone(&self) -> Annotation
1.0.0 (const: unstable) · Source§fn clone_from(&mut self, source: &Self)
fn clone_from(&mut self, source: &Self)
source. Read moreSource§impl Debug for Annotation
impl Debug for Annotation
Source§impl Default for Annotation
impl Default for Annotation
Source§fn default() -> Annotation
fn default() -> Annotation
Source§impl PartialEq for Annotation
impl PartialEq for Annotation
Source§fn eq(&self, other: &Annotation) -> bool
fn eq(&self, other: &Annotation) -> bool
self and other values to be equal, and is used by ==.