Skip to content
Merged
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
18 changes: 18 additions & 0 deletions src/logger.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
/// This target is used exclusively to handle group events.
pub const GROUP_TARGET: &str = "codspeed::group";
pub const OPENED_GROUP_TARGET: &str = "codspeed::group::opened";
pub const ANNOUNCEMENT_TARGET: &str = "codspeed::announcement";

#[macro_export]
/// Start a new log group. All logs between this and the next `end_group!` will be grouped together.
Expand Down Expand Up @@ -43,6 +44,15 @@ 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.
macro_rules! announcement {
($name:expr) => {
log::log!(target: $crate::logger::ANNOUNCEMENT_TARGET, log::Level::Info, "{}", $name);
};
}

pub enum GroupEvent {
Start(String),
StartOpened(String),
Expand Down Expand Up @@ -72,6 +82,14 @@ pub(super) fn get_group_event(record: &log::Record) -> Option<GroupEvent> {
}
}

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

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

#[macro_export]
/// Log a structured JSON output
macro_rules! log_json {
Expand Down
2 changes: 1 addition & 1 deletion src/prelude.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
pub use crate::{end_group, log_json, start_group, start_opened_group};
pub use crate::{announcement, end_group, log_json, start_group, start_opened_group};
#[allow(unused_imports)]
pub use anyhow::{Context, Error, Result, anyhow, bail, ensure};
pub use itertools::Itertools;
Expand Down
11 changes: 8 additions & 3 deletions src/run/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -191,7 +191,7 @@ pub async fn run(
) -> Result<()> {
let output_json = args.message_format == Some(MessageFormat::Json);
let mut config = Config::try_from(args)?;
let provider = run_environment::get_provider(&config)?;
let mut provider = run_environment::get_provider(&config)?;
let logger = Logger::new(&provider)?;

#[allow(deprecated)]
Expand All @@ -214,8 +214,7 @@ pub async fn run(
debug!("Using the token from the CodSpeed configuration file");
config.set_token(codspeed_config.auth.token.clone());
} else {
// If relevant, set the OIDC token for authentication
provider.set_oidc_token(&mut config).await?;
provider.check_oidc_configuration(&config)?;
}

let system_info = SystemInfo::new()?;
Expand Down Expand Up @@ -266,6 +265,12 @@ pub async fn run(
};

if !config.skip_upload {
if provider.get_run_environment() != RunEnvironment::Local {
// If relevant, set the OIDC token for authentication
// Note: OIDC tokens can expire quickly, so we set it just before the upload
provider.set_oidc_token(&mut config).await?;
}

start_group!("Uploading performance data");
let upload_result =
uploader::upload(&config, &system_info, &provider, &run_data, executor.name()).await?;
Expand Down
7 changes: 6 additions & 1 deletion src/run/run_environment/buildkite/logger.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
use crate::{
logger::{GroupEvent, get_group_event, get_json_event},
logger::{GroupEvent, get_announcement_event, get_group_event, get_json_event},
run::run_environment::logger::should_provider_logger_handle_record,
};
use log::*;
Expand Down Expand Up @@ -53,6 +53,11 @@ impl Log for BuildkiteLogger {
return;
}

if let Some(announcement) = get_announcement_event(record) {
println!("[ANNOUNCEMENT] {announcement}");
return;
}

if level > self.log_level {
return;
}
Expand Down
19 changes: 14 additions & 5 deletions src/run/run_environment/github_actions/logger.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
use crate::{
logger::{GroupEvent, get_group_event, get_json_event},
logger::{GroupEvent, get_announcement_event, get_group_event, get_json_event},
run::run_environment::logger::should_provider_logger_handle_record,
};
use log::*;
Expand Down Expand Up @@ -55,6 +55,15 @@ impl Log for GithubActionLogger {
return;
}

if let Some(announcement) = get_announcement_event(record) {
// properly escape newlines so that GitHub Actions interprets them correctly
// https://github.com/actions/toolkit/issues/193#issuecomment-605394935
let escaped_announcement = announcement.replace('\n', "%0A");
// TODO: make the announcement title configurable
println!("::notice title=New CodSpeed Feature::{escaped_announcement}");
return;
}

if get_json_event(record).is_some() {
return;
}
Expand All @@ -70,10 +79,10 @@ impl Log for GithubActionLogger {
Level::Debug => "::debug::",
Level::Trace => "::debug::[TRACE]",
};
let message_string = message.to_string();
let lines = message_string.lines();
// ensure that all the lines of the message have the prefix, otherwise GitHub Actions will not recognize the command for the whole string
lines.for_each(|line| println!("{prefix}{line}"));
// properly escape newlines so that GitHub Actions interprets them correctly
// https://github.com/actions/toolkit/issues/193#issuecomment-605394935
let message_string = message.to_string().replace('\n', "%0A");
println!("{prefix}{message_string}");
}

fn flush(&self) {
Expand Down
118 changes: 78 additions & 40 deletions src/run/run_environment/github_actions/provider.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,6 @@ use crate::run::{

use super::logger::GithubActionLogger;

#[derive(Debug)]
pub struct GitHubActionsProvider {
pub owner: String,
pub repository: String,
Expand All @@ -39,6 +38,16 @@ pub struct GitHubActionsProvider {

/// Indicates whether the repository is private.
is_repository_private: bool,

/// OIDC configuration data necessary to request an OIDC token.
///
/// If None, OIDC is not configured for this run.
oidc_config: Option<OIDCTokenRequestData>,
}

struct OIDCTokenRequestData {
request_url: String,
request_token: String,
}

impl GitHubActionsProvider {
Expand Down Expand Up @@ -139,6 +148,7 @@ impl TryFrom<&Config> for GitHubActionsProvider {
repository_root_path,
is_head_repo_fork,
is_repository_private,
oidc_config: None,
})
}
}
Expand Down Expand Up @@ -259,32 +269,33 @@ impl RunEnvironmentProvider for GitHubActionsProvider {
Ok(commit_hash)
}

/// Set the OIDC token for GitHub Actions if necessary
/// Validate that the environment is correctly configured for OIDC usage.
///
/// ## Logic
/// - If the user has explicitly set a token in the configuration (i.e. "static token"), do not override it, but display an info message.
/// - If the user has explicitly set a token in the configuration (i.e. "static token"), inform the user that OIDC is available but do nothing.
/// - Otherwise, check if the necessary environment variables are set to use OIDC.
/// - Then attempt to request an OIDC token.
///
/// For Github Actions, there are two necessary environment variables:
/// - `ACTIONS_ID_TOKEN_REQUEST_TOKEN`
/// - `ACTIONS_ID_TOKEN_REQUEST_URL`
/// If environment variables are not set, this could be because:
/// - The user has misconfigured the workflow (missing `id-token` permission)
/// - The run is from a public fork, in which case GitHub Actions does not provide these environment variables for security reasons.
///
///
/// ## Notes
/// Retrieving the token requires that the workflow has the `id-token` permission enabled.
///
/// Docs:
/// - https://docs.github.com/en/actions/how-tos/secure-your-work/security-harden-deployments/oidc-with-reusable-workflows
/// - https://docs.github.com/en/actions/concepts/security/openid-connect
/// - https://docs.github.com/en/actions/reference/security/oidc#methods-for-requesting-the-oidc-token
async fn set_oidc_token(&self, config: &mut Config) -> Result<()> {
fn check_oidc_configuration(&mut self, config: &Config) -> Result<()> {
// Check if a static token is already set
if config.token.is_some() {
info!(
"CodSpeed now supports OIDC tokens for authentication.\n\
Benefit from enhanced security by adding the `id-token: write` permission to your workflow and removing the static token from your configuration.\n\
Learn more at https://codspeed.io/docs/integrations/ci/github-actions/configuration#oidc-recommended"
announcement!(
"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"
);

return Ok(());
Expand All @@ -303,50 +314,73 @@ impl RunEnvironmentProvider for GitHubActionsProvider {

if self.is_repository_private {
bail!(
"Unable to retrieve OIDC token for authentication. \n\
Make sure your workflow has the `id-token: write` permission set. \n\
"Unable to retrieve OIDC token for authentication.\n\
Make sure your workflow has the `id-token: write` permission set.\n\
See https://codspeed.io/docs/integrations/ci/github-actions/configuration#oidc-recommended"
)
}

info!(
"CodSpeed now supports OIDC tokens for authentication.\n\
Benefit from enhanced security and faster processing times by adding the `id-token: write` permission to your workflow.\n\
Learn more at https://codspeed.io/docs/integrations/ci/github-actions/configuration#oidc-recommended"
announcement!(
"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"
);

return Ok(());
}

let request_url = request_url.unwrap();
let request_url = format!("{request_url}&audience={}", self.get_oidc_audience());
let request_token = request_token.unwrap();

let token = match OIDC_CLIENT
.get(request_url)
.header("Accept", "application/json")
.header("Authorization", format!("Bearer {request_token}"))
.send()
.await
{
Ok(response) => match response.json::<OIDCResponse>().await {
Ok(oidc_response) => oidc_response.value,
self.oidc_config = Some(OIDCTokenRequestData {
request_url,
request_token,
});

Ok(())
}

/// Request the OIDC token from GitHub Actions if necessary.
///
/// All the validation has already been performed in `check_oidc_configuration`.
/// So if the oidc_config is None, we simply return.
async fn set_oidc_token(&self, config: &mut Config) -> Result<()> {
if let Some(oidc_config) = &self.oidc_config {
let request_url = format!(
"{}&audience={}",
oidc_config.request_url,
self.get_oidc_audience()
);

let token = match OIDC_CLIENT
.get(request_url)
.header("Accept", "application/json")
.header(
"Authorization",
format!("Bearer {}", oidc_config.request_token),
)
.send()
.await
{
Ok(response) => match response.json::<OIDCResponse>().await {
Ok(oidc_response) => oidc_response.value,
Err(_) => None,
},
Err(_) => None,
},
Err(_) => None,
};
};

if token.is_some() {
debug!("Successfully retrieved OIDC token for authentication.");
config.set_token(token);
} else if self.is_repository_private {
bail!(
"Unable to retrieve OIDC token for authentication. \n\
Make sure your workflow has the `id-token: write` permission set. \n\
See https://codspeed.io/docs/integrations/ci/github-actions/configuration#oidc-recommended"
)
} else {
warn!("Failed to retrieve OIDC token for authentication.");
if token.is_some() {
debug!("Successfully retrieved OIDC token for authentication.");
config.set_token(token);
} else if self.is_repository_private {
bail!(
"Unable to retrieve OIDC token for authentication. \n\
Make sure your workflow has the `id-token: write` permission set. \n\
See https://codspeed.io/docs/integrations/ci/github-actions/configuration#oidc-recommended"
)
} else {
warn!("Failed to retrieve OIDC token for authentication.");
}
}

Ok(())
Expand Down Expand Up @@ -641,6 +675,7 @@ mod tests {
repository_root_path: "/home/work/my-repo".into(),
is_head_repo_fork: false,
is_repository_private: false,
oidc_config: None,
};

let run_part = github_actions_provider.get_run_provider_run_part().unwrap();
Expand Down Expand Up @@ -685,6 +720,7 @@ mod tests {
repository_root_path: "/home/work/my-repo".into(),
is_head_repo_fork: false,
is_repository_private: false,
oidc_config: None,
};

let run_part = github_actions_provider.get_run_provider_run_part().unwrap();
Expand Down Expand Up @@ -738,6 +774,7 @@ mod tests {
repository_root_path: "/home/work/my-repo".into(),
is_head_repo_fork: false,
is_repository_private: false,
oidc_config: None,
};

let run_part = github_actions_provider.get_run_provider_run_part().unwrap();
Expand Down Expand Up @@ -789,6 +826,7 @@ mod tests {
repository_root_path: "/home/work/my-repo".into(),
is_head_repo_fork: false,
is_repository_private: false,
oidc_config: None,
};

let run_part = github_actions_provider.get_run_provider_run_part().unwrap();
Expand Down
7 changes: 6 additions & 1 deletion src/run/run_environment/gitlab_ci/logger.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ use std::{
};

use crate::{
logger::{GroupEvent, get_group_event, get_json_event},
logger::{GroupEvent, get_announcement_event, get_group_event, get_json_event},
run::run_environment::logger::should_provider_logger_handle_record,
};

Expand Down Expand Up @@ -112,6 +112,11 @@ impl Log for GitLabCILogger {
return;
}

if let Some(announcement) = get_announcement_event(record) {
println!("{}", style(announcement).green());
return;
}

if get_json_event(record).is_some() {
return;
}
Expand Down
8 changes: 8 additions & 0 deletions src/run/run_environment/provider.rs
Original file line number Diff line number Diff line change
Expand Up @@ -50,12 +50,20 @@ pub trait RunEnvironmentProvider {
OIDC_AUDIENCE
}

/// Check the OIDC configuration for the current run environment, if supported.
fn check_oidc_configuration(&mut self, _config: &Config) -> Result<()> {
Ok(())
}

/// Handle an OIDC token for the current run environment, if supported.
///
/// Updates the config if necessary.
///
/// Depending on the provider, this may involve requesting the token,
/// warning the user about potential misconfigurations, or other necessary steps.
///
/// Warning: OIDC tokens are typically short-lived. This method must be called
/// just before the upload step to ensure the token is valid during the upload.
async fn set_oidc_token(&self, _config: &mut Config) -> Result<()> {
Ok(())
}
Expand Down