Skip to main content

Annotation

Struct Annotation 

Source
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

Source

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?
examples/demo.rs (line 31)
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 19)
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 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?
examples/demo.rs (line 35)
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 25)
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 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?
examples/demo.rs (line 32)
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 20)
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 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?
examples/demo.rs (line 33)
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 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?
examples/demo.rs (line 34)
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 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");
Source

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");
Source

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?
examples/ci_selfcheck.rs (lines 21-24)
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 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?
examples/ci_selfcheck.rs (lines 26-29)
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 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");
Source

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?
examples/demo.rs (line 36)
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 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?
examples/demo.rs (line 41)
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

Source§

fn clone(&self) -> Annotation

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 Annotation

Source§

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

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

impl Default for Annotation

Source§

fn default() -> Annotation

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

impl PartialEq for Annotation

Source§

fn eq(&self, other: &Annotation) -> bool

Tests for self and other values to be equal, and is used by ==.
1.0.0 (const: unstable) · Source§

fn ne(&self, other: &Rhs) -> bool

Tests for !=. The default implementation is almost always sufficient, and should not be overridden without very good reason.
Source§

impl Eq for Annotation

Source§

impl StructuralPartialEq for Annotation

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.