Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
109 changes: 105 additions & 4 deletions src/logger.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,15 @@ pub const GROUP_TARGET: &str = "codspeed::group";
pub const OPENED_GROUP_TARGET: &str = "codspeed::group::opened";
pub const ANNOUNCEMENT_TARGET: &str = "codspeed::announcement";

/// Default title used by provider loggers when an announcement is logged
/// without an explicit title.
pub const DEFAULT_ANNOUNCEMENT_TITLE: &str = "New CodSpeed Feature";

/// Internal delimiter (ASCII Unit Separator) used to encode an announcement
/// title alongside its message in a single log record. Reserved control
/// character that is not expected to appear in user-facing strings.
pub const ANNOUNCEMENT_DELIMITER: char = '\x1F';

#[macro_export]
/// Start a new log group. All logs between this and the next `end_group!` will be grouped together.
///
Expand Down Expand Up @@ -51,9 +60,25 @@ macro_rules! end_group {
#[macro_export]
/// Logs at the announcement level. This is intended for important announcements like new features,
/// that do not require immediate user action.
///
/// Two forms are supported:
/// - `announcement!("message")`: logs a message with no explicit title; provider loggers fall
/// back to their default presentation (e.g. `"New CodSpeed Feature"` on GitHub Actions).
/// - `announcement!("title", "message")`: logs a message with a custom title; provider loggers
/// surface the title where supported (e.g. as the `title=` field of a GitHub Actions notice).
macro_rules! announcement {
($name:expr) => {
log::log!(target: $crate::logger::ANNOUNCEMENT_TARGET, log::Level::Info, "{}", $name);
($message:expr) => {
log::log!(target: $crate::logger::ANNOUNCEMENT_TARGET, log::Level::Info, "{}", $message);
};
($title:expr, $message:expr) => {
log::log!(
target: $crate::logger::ANNOUNCEMENT_TARGET,
log::Level::Info,
"{}{}{}",
$title,
$crate::logger::ANNOUNCEMENT_DELIMITER,
$message
);
};
}

Expand Down Expand Up @@ -86,12 +111,39 @@ pub(super) fn get_group_event(record: &log::Record) -> Option<GroupEvent> {
}
}

pub(super) fn get_announcement_event(record: &log::Record) -> Option<String> {
/// A decoded announcement log record.
///
/// Announcements are encoded into a single log record by [`announcement!`], optionally pairing
/// a `title` with the `message` via [`ANNOUNCEMENT_DELIMITER`]. Provider loggers consume this
/// to render announcements in their preferred format.
pub struct AnnouncementEvent {
pub title: Option<String>,
pub message: String,
}

/// Splits an announcement payload into its title and message parts using
/// [`ANNOUNCEMENT_DELIMITER`]. If no delimiter is present, the whole payload is treated as the
/// message and the title is `None`.
fn parse_announcement_args(raw: &str) -> AnnouncementEvent {
if let Some((title, message)) = raw.split_once(ANNOUNCEMENT_DELIMITER) {
AnnouncementEvent {
title: Some(title.to_string()),
message: message.to_string(),
}
} else {
AnnouncementEvent {
title: None,
message: raw.to_string(),
}
}
}

pub(super) fn get_announcement_event(record: &log::Record) -> Option<AnnouncementEvent> {
if record.target() != ANNOUNCEMENT_TARGET {
return None;
}

Some(record.args().to_string())
Some(parse_announcement_args(&record.args().to_string()))
}

#[macro_export]
Expand All @@ -113,3 +165,52 @@ pub(super) fn get_json_event(record: &log::Record) -> Option<JsonEvent> {

Some(JsonEvent(record.args().to_string()))
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn parses_announcement_without_title() {
let event = parse_announcement_args("hello");
assert!(event.title.is_none());
assert_eq!(event.message, "hello");
}

#[test]
fn parses_announcement_with_title() {
let raw = format!("OIDC Authentication{ANNOUNCEMENT_DELIMITER}Use OIDC instead of tokens.");
let event = parse_announcement_args(&raw);
assert_eq!(event.title.as_deref(), Some("OIDC Authentication"));
assert_eq!(event.message, "Use OIDC instead of tokens.");
}

#[test]
fn parses_announcement_with_empty_title() {
let raw = format!("{ANNOUNCEMENT_DELIMITER}message-only");
let event = parse_announcement_args(&raw);
assert_eq!(event.title.as_deref(), Some(""));
assert_eq!(event.message, "message-only");
}

#[test]
fn parses_announcement_preserving_multiline_message() {
let raw = format!("Title{ANNOUNCEMENT_DELIMITER}line1\nline2\nline3");
let event = parse_announcement_args(&raw);
assert_eq!(event.title.as_deref(), Some("Title"));
assert_eq!(event.message, "line1\nline2\nline3");
}

#[test]
fn splits_at_first_delimiter_only() {
let raw = format!(
"Title{ANNOUNCEMENT_DELIMITER}message containing the {ANNOUNCEMENT_DELIMITER} char"
);
let event = parse_announcement_args(&raw);
assert_eq!(event.title.as_deref(), Some("Title"));
assert_eq!(
event.message,
format!("message containing the {ANNOUNCEMENT_DELIMITER} char")
);
}
}
10 changes: 9 additions & 1 deletion src/run_environment/buildkite/logger.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,10 @@ use log::*;
use simplelog::SharedLogger;
use std::{env, io::Write};

/// Title used for announcements when no explicit title is provided. Preserves the legacy
/// `[ANNOUNCEMENT]` prefix that this logger emitted before per-call titles were supported.
const DEFAULT_BUILDKITE_ANNOUNCEMENT_TITLE: &str = "ANNOUNCEMENT";

/// A logger that prints logs in the format expected by Buildkite
///
/// See https://buildkite.com/docs/pipelines/managing-log-output
Expand Down Expand Up @@ -54,7 +58,11 @@ impl Log for BuildkiteLogger {
}

if let Some(announcement) = get_announcement_event(record) {
println!("[ANNOUNCEMENT] {announcement}");
let title = announcement
.title
.as_deref()
.unwrap_or(DEFAULT_BUILDKITE_ANNOUNCEMENT_TITLE);
println!("[{title}] {}", announcement.message);
return;
}

Expand Down
15 changes: 11 additions & 4 deletions src/run_environment/github_actions/logger.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
use crate::{
logger::{GroupEvent, get_announcement_event, get_group_event, get_json_event},
logger::{
DEFAULT_ANNOUNCEMENT_TITLE, GroupEvent, get_announcement_event, get_group_event,
get_json_event,
},
run_environment::logger::should_provider_logger_handle_record,
};
use log::*;
Expand Down Expand Up @@ -56,9 +59,13 @@ impl Log for GithubActionLogger {
}

if let Some(announcement) = get_announcement_event(record) {
let escaped_announcement = escape_multiline_message(&announcement);
// TODO: make the announcement title configurable
println!("::notice title=New CodSpeed Feature::{escaped_announcement}");
let title = announcement
.title
.as_deref()
.unwrap_or(DEFAULT_ANNOUNCEMENT_TITLE);
let escaped_title = escape_multiline_message(title);
let escaped_message = escape_multiline_message(&announcement.message);
println!("::notice title={escaped_title}::{escaped_message}");
return;
}

Expand Down
2 changes: 2 additions & 0 deletions src/run_environment/github_actions/provider.rs
Original file line number Diff line number Diff line change
Expand Up @@ -292,6 +292,7 @@ impl RunEnvironmentProvider for GitHubActionsProvider {
// Check if a static token is already set
if config.token.is_some() {
announcement!(
"OIDC Authentication",
"You can now authenticate your CI workflows using OpenID Connect (OIDC) tokens instead of `CODSPEED_TOKEN` secrets.\n\
This makes integrating and authenticating jobs safer and simpler.\n\
Learn more at https://codspeed.io/docs/integrations/ci/github-actions/configuration#oidc-recommended\n"
Expand Down Expand Up @@ -320,6 +321,7 @@ impl RunEnvironmentProvider for GitHubActionsProvider {
}

announcement!(
"OIDC Authentication",
"You can now authenticate your CI workflows using OpenID Connect (OIDC).\n\
This makes integrating and authenticating jobs safer and simpler.\n\
Learn more at https://codspeed.io/docs/integrations/ci/github-actions/configuration#oidc-recommended\n"
Expand Down
9 changes: 8 additions & 1 deletion src/run_environment/gitlab_ci/logger.rs
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,14 @@ impl Log for GitLabCILogger {
}

if let Some(announcement) = get_announcement_event(record) {
println!("{}", style(announcement).green());
match announcement.title {
Some(title) => println!(
"{}: {}",
style(title).bold().green(),
style(announcement.message).green()
),
None => println!("{}", style(announcement.message).green()),
}
return;
}

Expand Down