From 7696dc14288a5512efb6c42d609ac466838dddcf Mon Sep 17 00:00:00 2001 From: moha-bekh Date: Mon, 28 Sep 2026 06:15:59 -0400 Subject: [PATCH 1/6] feat: expose the run id as a GitHub Actions step output A workflow step running after the benchmarks had no way to know which CodSpeed run the results were uploaded to. After a successful upload on GitHub Actions, write it to `$GITHUB_OUTPUT` as `run-id`. All uploads of a workflow run share the same CodSpeed run, so the id of the last upload is the run id. A failure to write the output only warns. Closes COD-3661 Co-Authored-By: Claude Opus 5.5 --- src/executor/orchestrator.rs | 4 ++ .../github_actions/provider.rs | 48 +++++++++++++++++++ src/run_environment/provider.rs | 5 ++ 3 files changed, 57 insertions(+) diff --git a/src/executor/orchestrator.rs b/src/executor/orchestrator.rs index ca2dbdf4f..ad2c9c5f7 100644 --- a/src/executor/orchestrator.rs +++ b/src/executor/orchestrator.rs @@ -226,6 +226,10 @@ impl Orchestrator { let last_upload_result = self.upload_all(&mut completed_runs, api_client).await?; end_group!(); + if let Err(e) = self.provider.export_run_id(&last_upload_result.run_id) { + warn!("Failed to expose the run id as a step output: {e}"); + } + if self.is_local() { poll_results( api_client, diff --git a/src/run_environment/github_actions/provider.rs b/src/run_environment/github_actions/provider.rs index 4a411db2b..dad418c45 100644 --- a/src/run_environment/github_actions/provider.rs +++ b/src/run_environment/github_actions/provider.rs @@ -7,6 +7,7 @@ use serde::Deserialize; use serde_json::Value; use simplelog::SharedLogger; use std::collections::BTreeMap; +use std::io::Write; use std::{env, fs}; use crate::api_client::{Authentication, CodSpeedAPIClient}; @@ -382,6 +383,20 @@ impl RunEnvironmentProvider for GitHubActionsProvider { Ok(()) } + + /// Write the run id as the `run-id` step output. + fn export_run_id(&self, run_id: &str) -> Result<()> { + let Ok(output_path) = env::var("GITHUB_OUTPUT") else { + return Ok(()); + }; + let mut output_file = fs::OpenOptions::new() + .append(true) + .create(true) + .open(output_path)?; + writeln!(output_file, "run-id={run_id}")?; + + Ok(()) + } } #[cfg(test)] @@ -833,4 +848,37 @@ mod tests { }, ) } + + #[test] + fn test_export_run_id() { + let output_file = tempfile::NamedTempFile::new().unwrap(); + fs::write(output_file.path(), "previous=value\n").unwrap(); + + with_var("GITHUB_OUTPUT", Some(output_file.path()), || { + let github_actions_provider = GitHubActionsProvider { + owner: "owner".into(), + repository: "repository".into(), + ref_: "refs/head/my-branch".into(), + head_ref: Some("my-branch".into()), + base_ref: None, + sender: None, + run_id: "123789".into(), + job_name: "my_job".into(), + event: RunEvent::Push, + repository_root_path: "/home/work/my-repo".into(), + is_head_repo_fork: false, + is_repository_private: false, + oidc_config: None, + }; + + github_actions_provider + .export_run_id("66f7e2c1a3b4d5e6f7a8b9c0") + .unwrap(); + }); + + assert_eq!( + fs::read_to_string(output_file.path()).unwrap(), + "previous=value\nrun-id=66f7e2c1a3b4d5e6f7a8b9c0\n" + ); + } } diff --git a/src/run_environment/provider.rs b/src/run_environment/provider.rs index 317ac2178..d8c1628c9 100644 --- a/src/run_environment/provider.rs +++ b/src/run_environment/provider.rs @@ -100,6 +100,11 @@ pub trait RunEnvironmentProvider { Ok(()) } + /// Expose the CodSpeed run id to the next steps of the CI job, if supported. + fn export_run_id(&self, _run_id: &str) -> Result<()> { + Ok(()) + } + /// Returns the metadata necessary for uploading results to CodSpeed. /// /// `orchestrator_run_part_suffix` is structured data from the orchestrator used to differentiate From c6aec2f16aaa8a072052d90726e197aa0fae1a7e Mon Sep 17 00:00:00 2001 From: moha-bekh Date: Mon, 28 Sep 2026 08:30:03 -0400 Subject: [PATCH 2/6] feat: only export the run id when asked to Writing to `$GITHUB_OUTPUT` on every upload would add a `run-id` output to any user `run:` step invoking the runner. Gate it behind `--export-run-id` / `CODSPEED_EXPORT_RUN_ID`, set by the CodSpeed action. Refs COD-3661 Co-Authored-By: Claude Opus 5.5 --- src/cli/exec/mod.rs | 1 + src/cli/run/mod.rs | 2 ++ src/cli/shared.rs | 9 +++++++++ src/executor/config.rs | 3 +++ src/executor/orchestrator.rs | 6 ++++-- 5 files changed, 19 insertions(+), 2 deletions(-) diff --git a/src/cli/exec/mod.rs b/src/cli/exec/mod.rs index e8ed95b04..ab4cb8efc 100644 --- a/src/cli/exec/mod.rs +++ b/src/cli/exec/mod.rs @@ -84,6 +84,7 @@ fn build_orchestrator_config( simulation_tool: args.shared.simulation_tool.unwrap_or_default(), profile_folder: args.shared.profile_folder, skip_upload: args.shared.skip_upload, + export_run_id: args.shared.export_run_id, skip_run: args.shared.skip_run, skip_setup: args.shared.skip_setup, allow_empty: args.shared.allow_empty, diff --git a/src/cli/run/mod.rs b/src/cli/run/mod.rs index b56fc4399..5bac95835 100644 --- a/src/cli/run/mod.rs +++ b/src/cli/run/mod.rs @@ -63,6 +63,7 @@ impl RunArgs { walltime_profiler: None, profile_folder: None, skip_upload: false, + export_run_id: false, skip_run: false, skip_setup: false, allow_empty: false, @@ -128,6 +129,7 @@ fn build_orchestrator_config( simulation_tool: args.shared.simulation_tool.unwrap_or_default(), profile_folder: args.shared.profile_folder, skip_upload: args.shared.skip_upload, + export_run_id: args.shared.export_run_id, skip_run: args.shared.skip_run, skip_setup: args.shared.skip_setup, allow_empty: args.shared.allow_empty, diff --git a/src/cli/shared.rs b/src/cli/shared.rs index 7aba09db5..b79554ce3 100644 --- a/src/cli/shared.rs +++ b/src/cli/shared.rs @@ -87,6 +87,15 @@ pub struct ExecAndRunSharedArgs { )] pub skip_upload: bool, + /// Used internally by the GitHub Action to expose the run id as the `run-id` step output + #[arg( + long, + default_value = "false", + hide = true, + env = "CODSPEED_EXPORT_RUN_ID" + )] + pub export_run_id: bool, + /// Used internally to upload the results after running the benchmarks in a sandbox environment /// with no internet access #[arg(long, default_value = "false", hide = true)] diff --git a/src/executor/config.rs b/src/executor/config.rs index 7d51d1b1d..ceba92ab9 100644 --- a/src/executor/config.rs +++ b/src/executor/config.rs @@ -77,6 +77,8 @@ pub struct OrchestratorConfig { pub profile_folder: Option, pub skip_upload: bool, + /// If true, expose the run id to the next steps of the CI job after the upload + pub export_run_id: bool, pub skip_run: bool, pub skip_setup: bool, /// If true, allow execution even when no benchmarks are found @@ -247,6 +249,7 @@ impl OrchestratorConfig { simulation_tool: SimulationTool::default(), profile_folder: None, skip_upload: false, + export_run_id: false, skip_run: false, skip_setup: false, allow_empty: false, diff --git a/src/executor/orchestrator.rs b/src/executor/orchestrator.rs index ad2c9c5f7..66eca201d 100644 --- a/src/executor/orchestrator.rs +++ b/src/executor/orchestrator.rs @@ -226,8 +226,10 @@ impl Orchestrator { let last_upload_result = self.upload_all(&mut completed_runs, api_client).await?; end_group!(); - if let Err(e) = self.provider.export_run_id(&last_upload_result.run_id) { - warn!("Failed to expose the run id as a step output: {e}"); + if self.config.export_run_id { + if let Err(e) = self.provider.export_run_id(&last_upload_result.run_id) { + warn!("Failed to expose the run id as a step output: {e}"); + } } if self.is_local() { From 9ac0bbb2967bc2aa3374791b19b5bab287889589 Mon Sep 17 00:00:00 2001 From: moha-bekh Date: Mon, 28 Sep 2026 09:04:28 -0400 Subject: [PATCH 3/6] fix: warn when the run id cannot be exported for a missing GITHUB_OUTPUT Now that the export is opt-in, a missing `$GITHUB_OUTPUT` means the caller asked for a `run-id` output that will never be written. Surface it as a warning instead of silently skipping. Refs COD-3661 Co-Authored-By: Claude Opus 5.5 --- src/run_environment/github_actions/provider.rs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/run_environment/github_actions/provider.rs b/src/run_environment/github_actions/provider.rs index dad418c45..fbaf1fd36 100644 --- a/src/run_environment/github_actions/provider.rs +++ b/src/run_environment/github_actions/provider.rs @@ -386,9 +386,7 @@ impl RunEnvironmentProvider for GitHubActionsProvider { /// Write the run id as the `run-id` step output. fn export_run_id(&self, run_id: &str) -> Result<()> { - let Ok(output_path) = env::var("GITHUB_OUTPUT") else { - return Ok(()); - }; + let output_path = get_env_variable("GITHUB_OUTPUT")?; let mut output_file = fs::OpenOptions::new() .append(true) .create(true) From 7ef9814a875caa98c4d8d509595059c4e6dbc7b9 Mon Sep 17 00:00:00 2001 From: moha-bekh Date: Tue, 29 Sep 2026 08:24:46 -0400 Subject: [PATCH 4/6] feat: print the run id as JSON outside of GitHub Actions Providers without a native step output now print `{"run_id": "..."}` on stdout when the run id export is enabled, instead of silently doing nothing. Refs COD-3661 Co-Authored-By: Claude Opus 5.5 --- src/run_environment/provider.rs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/run_environment/provider.rs b/src/run_environment/provider.rs index d8c1628c9..1a018349c 100644 --- a/src/run_environment/provider.rs +++ b/src/run_environment/provider.rs @@ -100,8 +100,9 @@ pub trait RunEnvironmentProvider { Ok(()) } - /// Expose the CodSpeed run id to the next steps of the CI job, if supported. - fn export_run_id(&self, _run_id: &str) -> Result<()> { + /// Expose the CodSpeed run id to the next steps of the CI job. + fn export_run_id(&self, run_id: &str) -> Result<()> { + println!("{}", serde_json::json!({ "run_id": run_id })); Ok(()) } From 9ee12a504a1990793cf7970a290e5e2169320710 Mon Sep 17 00:00:00 2001 From: moha-bekh Date: Tue, 29 Sep 2026 08:50:30 -0400 Subject: [PATCH 5/6] refactor: emit the run id JSON through the structured logger Use `log_json!` instead of `println!`, the channel already used for other machine-readable output. Locally the JSON still goes to stdout while logs stay on stderr; the GitLab, Buildkite and CircleCI loggers drop JSON events, so the run id is no longer printed there. Refs COD-3661 Co-Authored-By: Claude Opus 5.5 --- src/run_environment/provider.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/run_environment/provider.rs b/src/run_environment/provider.rs index 1a018349c..ffa483a12 100644 --- a/src/run_environment/provider.rs +++ b/src/run_environment/provider.rs @@ -102,7 +102,7 @@ pub trait RunEnvironmentProvider { /// Expose the CodSpeed run id to the next steps of the CI job. fn export_run_id(&self, run_id: &str) -> Result<()> { - println!("{}", serde_json::json!({ "run_id": run_id })); + log_json!(serde_json::json!({ "run_id": run_id })); Ok(()) } From d803d08a55da3f318dfe79981b7379036bd9808f Mon Sep 17 00:00:00 2001 From: moha-bekh Date: Wed, 30 Sep 2026 09:36:55 -0400 Subject: [PATCH 6/6] feat: always export the run id on GitHub Actions Drop the hidden `--export-run-id` flag and its `CODSPEED_EXPORT_RUN_ID` env var: the runner now always writes the `run-id` step output after uploading on GitHub Actions. Other providers no longer print the run id as JSON, since without the gate it would show up on every run. Refs COD-3661 Co-Authored-By: Claude Opus 5.5 --- src/cli/exec/mod.rs | 1 - src/cli/run/mod.rs | 2 -- src/cli/shared.rs | 9 --------- src/executor/config.rs | 3 --- src/executor/orchestrator.rs | 6 ++---- src/run_environment/github_actions/provider.rs | 1 - src/run_environment/provider.rs | 3 +-- 7 files changed, 3 insertions(+), 22 deletions(-) diff --git a/src/cli/exec/mod.rs b/src/cli/exec/mod.rs index ab4cb8efc..e8ed95b04 100644 --- a/src/cli/exec/mod.rs +++ b/src/cli/exec/mod.rs @@ -84,7 +84,6 @@ fn build_orchestrator_config( simulation_tool: args.shared.simulation_tool.unwrap_or_default(), profile_folder: args.shared.profile_folder, skip_upload: args.shared.skip_upload, - export_run_id: args.shared.export_run_id, skip_run: args.shared.skip_run, skip_setup: args.shared.skip_setup, allow_empty: args.shared.allow_empty, diff --git a/src/cli/run/mod.rs b/src/cli/run/mod.rs index 5bac95835..b56fc4399 100644 --- a/src/cli/run/mod.rs +++ b/src/cli/run/mod.rs @@ -63,7 +63,6 @@ impl RunArgs { walltime_profiler: None, profile_folder: None, skip_upload: false, - export_run_id: false, skip_run: false, skip_setup: false, allow_empty: false, @@ -129,7 +128,6 @@ fn build_orchestrator_config( simulation_tool: args.shared.simulation_tool.unwrap_or_default(), profile_folder: args.shared.profile_folder, skip_upload: args.shared.skip_upload, - export_run_id: args.shared.export_run_id, skip_run: args.shared.skip_run, skip_setup: args.shared.skip_setup, allow_empty: args.shared.allow_empty, diff --git a/src/cli/shared.rs b/src/cli/shared.rs index b79554ce3..7aba09db5 100644 --- a/src/cli/shared.rs +++ b/src/cli/shared.rs @@ -87,15 +87,6 @@ pub struct ExecAndRunSharedArgs { )] pub skip_upload: bool, - /// Used internally by the GitHub Action to expose the run id as the `run-id` step output - #[arg( - long, - default_value = "false", - hide = true, - env = "CODSPEED_EXPORT_RUN_ID" - )] - pub export_run_id: bool, - /// Used internally to upload the results after running the benchmarks in a sandbox environment /// with no internet access #[arg(long, default_value = "false", hide = true)] diff --git a/src/executor/config.rs b/src/executor/config.rs index ceba92ab9..7d51d1b1d 100644 --- a/src/executor/config.rs +++ b/src/executor/config.rs @@ -77,8 +77,6 @@ pub struct OrchestratorConfig { pub profile_folder: Option, pub skip_upload: bool, - /// If true, expose the run id to the next steps of the CI job after the upload - pub export_run_id: bool, pub skip_run: bool, pub skip_setup: bool, /// If true, allow execution even when no benchmarks are found @@ -249,7 +247,6 @@ impl OrchestratorConfig { simulation_tool: SimulationTool::default(), profile_folder: None, skip_upload: false, - export_run_id: false, skip_run: false, skip_setup: false, allow_empty: false, diff --git a/src/executor/orchestrator.rs b/src/executor/orchestrator.rs index 66eca201d..ad2c9c5f7 100644 --- a/src/executor/orchestrator.rs +++ b/src/executor/orchestrator.rs @@ -226,10 +226,8 @@ impl Orchestrator { let last_upload_result = self.upload_all(&mut completed_runs, api_client).await?; end_group!(); - if self.config.export_run_id { - if let Err(e) = self.provider.export_run_id(&last_upload_result.run_id) { - warn!("Failed to expose the run id as a step output: {e}"); - } + if let Err(e) = self.provider.export_run_id(&last_upload_result.run_id) { + warn!("Failed to expose the run id as a step output: {e}"); } if self.is_local() { diff --git a/src/run_environment/github_actions/provider.rs b/src/run_environment/github_actions/provider.rs index fbaf1fd36..0e55f1670 100644 --- a/src/run_environment/github_actions/provider.rs +++ b/src/run_environment/github_actions/provider.rs @@ -384,7 +384,6 @@ impl RunEnvironmentProvider for GitHubActionsProvider { Ok(()) } - /// Write the run id as the `run-id` step output. fn export_run_id(&self, run_id: &str) -> Result<()> { let output_path = get_env_variable("GITHUB_OUTPUT")?; let mut output_file = fs::OpenOptions::new() diff --git a/src/run_environment/provider.rs b/src/run_environment/provider.rs index ffa483a12..8858e16fa 100644 --- a/src/run_environment/provider.rs +++ b/src/run_environment/provider.rs @@ -101,8 +101,7 @@ pub trait RunEnvironmentProvider { } /// Expose the CodSpeed run id to the next steps of the CI job. - fn export_run_id(&self, run_id: &str) -> Result<()> { - log_json!(serde_json::json!({ "run_id": run_id })); + fn export_run_id(&self, _run_id: &str) -> Result<()> { Ok(()) }