diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index e5c89155..75297835 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -35,19 +35,32 @@ cargo release -p exec-harness --execute beta After releasing `memtrack` or `exec-harness`, you **must** update the version references in the runner code: -1. **For memtrack**: Update `MEMTRACK_CODSPEED_VERSION` in `src/executor/memory/executor.rs`: +1. **For memtrack**: Update the `MEMTRACK_INSTALLER` pin record in `src/binary_pins.rs` (see [Pinned binary hashes](#pinned-binary-hashes) below). - ```rust - const MEMTRACK_CODSPEED_VERSION: &str = "X.Y.Z"; // Update to new version - ``` - -2. **For exec-harness**: Update `EXEC_HARNESS_VERSION` in `src/executor/orchestrator.rs`: - ```rust - const EXEC_HARNESS_VERSION: &str = "X.Y.Z"; // Update to new version - ``` +2. **For exec-harness**: Update the `EXEC_HARNESS_INSTALLER` pin record in `src/binary_pins.rs`. These constants are used by the runner to download and install the correct versions of the binaries from GitHub releases. +### Pinned binary hashes + +Every binary the runner downloads at install time (the patched valgrind `.deb`, the memtrack installer, the exec-harness installer, the mongo-tracer installer) is SHA-256-pinned. Each artifact keeps its version, URL template, and hash together in `src/binary_pins.rs`. + +When you bump a pinned version, regenerate the hash for each affected URL and update the matching pin record: + +```bash +curl -sL '' | sha256sum +``` + +For valgrind, that is one hash per supported `(distro_version, arch)` combination. `src/binary_pins.rs` also holds `VALGRIND_CODSPEED_VERSION` (the upstream semver, used to detect an already-installed copy) and `VALGRIND_DEB_REV` (the `.deb` revision suffix); the `.deb` package version is `{VALGRIND_CODSPEED_VERSION}-{VALGRIND_DEB_REV}`. Bump `VALGRIND_CODSPEED_VERSION` for a new upstream release, and `VALGRIND_DEB_REV` when the same upstream is repackaged. + +After updating, run the network-bound verification test that downloads every pinned URL and checks the bytes against the declared hash: + +```bash +GITHUB_ACTIONS=true cargo test --lib binary_pins::tests::all_pinned_binaries_match_their_declared_sha256 +``` + +This is also run in CI, but running it locally before opening the PR avoids a release-time round trip if a hash is wrong. + ### Releasing the Main Runner The main runner (`codspeed-runner`) should be released after ensuring all dependency versions are correct. @@ -56,8 +69,10 @@ The main runner (`codspeed-runner`) should be released after ensuring all depend **Verify binary version references**: Check that version constants in the runner code match the released versions: -- `MEMTRACK_CODSPEED_VERSION` in `src/executor/memory/executor.rs` -- `EXEC_HARNESS_VERSION` in `src/executor/orchestrator.rs` +- `MEMTRACK_VERSION` in `src/binary_pins.rs` +- `EXEC_HARNESS_VERSION` in `src/binary_pins.rs` + +Also confirm the SHA-256 entries in the pin records in `src/binary_pins.rs` match the released artifacts. #### Release Command diff --git a/src/binary_installer/mod.rs b/src/binary_installer/mod.rs index c4f488f0..d8bdb75b 100644 --- a/src/binary_installer/mod.rs +++ b/src/binary_installer/mod.rs @@ -1,29 +1,26 @@ -use crate::cli::run::helpers::download_file; +use crate::binary_pins::PinnedBinary; +use crate::cli::run::helpers::download_pinned_file; use crate::prelude::*; use semver::Version; use std::process::Command; use tempfile::NamedTempFile; -use url::Url; mod versions; -/// Ensure a binary is installed, or install it from a runner's GitHub release using the installer script. +/// Ensure a binary is installed, or install it from a `PinnedBinary` installer script. /// /// This function checks if the binary is already installed with the correct version. -/// If not, it downloads and executes an installer script from the CodSpeed runner repository. +/// If not, it downloads and executes the pinned installer script. /// /// # Arguments /// * `binary_name` - The binary command name (e.g., "codspeed-memtrack", "codspeed-exec-harness") /// * `version` - The version to install (e.g., "4.4.2-alpha.2") -/// * `get_installer_url` - A closure that returns the URL to download the installer script. -pub async fn ensure_binary_installed( +/// * `installer` - The `PinnedBinary` installer to download. +pub async fn ensure_binary_installed( binary_name: &str, version: &str, - get_installer_url: F, -) -> Result<()> -where - F: FnOnce() -> String, -{ + installer: PinnedBinary, +) -> Result<()> { if is_command_installed( binary_name, Version::parse(version).context("Invalid version format")?, @@ -32,13 +29,11 @@ where return Ok(()); } - let installer_url = Url::parse(&get_installer_url()).context("Invalid installer URL")?; + debug!("Downloading installer for {binary_name}"); - debug!("Downloading installer from: {installer_url}"); - - // Download the installer script to a temporary file + // Download the installer script to a temporary file (with sha256 verification) let temp_file = NamedTempFile::new().context("Failed to create temporary file")?; - download_file(&installer_url, temp_file.path()).await?; + download_pinned_file(installer, temp_file.path()).await?; // Execute the installer script let output = Command::new("sh") diff --git a/src/binary_pins.rs b/src/binary_pins.rs new file mode 100644 index 00000000..c7c2bd52 --- /dev/null +++ b/src/binary_pins.rs @@ -0,0 +1,236 @@ +// Pinned downloads. Each artifact record keeps the version, URL template, and +// expected SHA-256 together so bumps happen in one place. See CONTRIBUTING.md +// for the regeneration workflow. + +use semver::Version; +use std::sync::LazyLock; + +/// Upstream valgrind-codspeed version. Single source of truth for the .deb +/// download (combined with `VALGRIND_DEB_REV`) and for detecting an already +/// installed copy. +pub const VALGRIND_CODSPEED_VERSION: Version = Version::new(3, 26, 0); +/// Suffix appended to `VALGRIND_CODSPEED_VERSION` to form the .deb package version. +/// Bumps when the .deb is repackaged without a new upstream valgrind release. +const VALGRIND_DEB_REV: &str = "0codspeed2"; +/// String form of `VALGRIND_CODSPEED_VERSION` as it appears in `valgrind --version` +/// output, used to identify a CodSpeed build at runtime. +pub static VALGRIND_CODSPEED_VERSION_STRING: LazyLock = + LazyLock::new(|| format!("{VALGRIND_CODSPEED_VERSION}.codspeed")); + +#[derive(Debug, Clone, Copy)] +struct BinaryPin { + version: &'static str, + url_template: &'static str, + sha256: &'static str, +} + +impl BinaryPin { + fn url(&self) -> String { + self.url_template.replace("{version}", self.version) + } +} + +/// Ubuntu release for which CodSpeed publishes a patched valgrind .deb. +/// Variants double as the value used in the download URL and as the +/// installation key, so any `ValgrindTarget` constructed in the runner +/// resolves to a real pin without a runtime fallback. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum DistroVersion { + Ubuntu2204, + Ubuntu2404, +} + +impl DistroVersion { + fn as_str(self) -> &'static str { + match self { + DistroVersion::Ubuntu2204 => "22.04", + DistroVersion::Ubuntu2404 => "24.04", + } + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum Arch { + Amd64, + Arm64, +} + +impl Arch { + fn as_str(self) -> &'static str { + match self { + Arch::Amd64 => "amd64", + Arch::Arm64 => "arm64", + } + } +} + +/// A `(DistroVersion, Arch)` pair for which the runner ships a pinned +/// valgrind .deb. Both `url()` and `sha256()` are exhaustive matches over +/// the type, so any value constructible here resolves to a real pin. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct ValgrindTarget { + pub distro_version: DistroVersion, + pub arch: Arch, +} + +static VALGRIND_DEB_VERSION: LazyLock = + LazyLock::new(|| format!("{VALGRIND_CODSPEED_VERSION}-{VALGRIND_DEB_REV}")); +const VALGRIND_DEB_URL_TEMPLATE: &str = "https://github.com/CodSpeedHQ/valgrind-codspeed/releases/download/{version}/valgrind_{version}_ubuntu-{distro_version}_{arch}.deb"; + +impl ValgrindTarget { + fn url(self) -> String { + VALGRIND_DEB_URL_TEMPLATE + .replace("{version}", &VALGRIND_DEB_VERSION) + .replace("{distro_version}", self.distro_version.as_str()) + .replace("{arch}", self.arch.as_str()) + } + + fn sha256(self) -> &'static str { + match (self.distro_version, self.arch) { + (DistroVersion::Ubuntu2204, Arch::Amd64) => { + "1c677ae440cc77fac6bedded02b2af38c3515c76ffb72ed1c3258f8b839de560" + } + (DistroVersion::Ubuntu2404, Arch::Amd64) => { + "2e21cf5b1dea52bc7e23156fab6adc8893fac5ff101f33a4b68c0ecdb1716f3f" + } + (DistroVersion::Ubuntu2204, Arch::Arm64) => { + "597391c2f61d238454c84c61f929711fe54bf5eae43e01dc27021cf532a8b653" + } + (DistroVersion::Ubuntu2404, Arch::Arm64) => { + "f2fd8440f991014eef7fbed288eae210b4e1e299e11d75c278b55ead1defa493" + } + } + } +} + +const MEMTRACK_INSTALLER: BinaryPin = BinaryPin { + version: "1.2.3", + url_template: "https://github.com/CodSpeedHQ/codspeed/releases/download/memtrack-v{version}/memtrack-installer.sh", + sha256: "67f30ebe17d5da4246b51d8663394026385d95203ff09e81289772159e969603", +}; +pub const MEMTRACK_VERSION: &str = MEMTRACK_INSTALLER.version; + +const EXEC_HARNESS_INSTALLER: BinaryPin = BinaryPin { + version: "1.3.0", + url_template: "https://github.com/CodSpeedHQ/codspeed/releases/download/exec-harness-v{version}/exec-harness-installer.sh", + sha256: "75cbff4fdaefe98927d24fff43fd600c621eb1263b0c40b0fd32c68fa6d88ebd", +}; +pub const EXEC_HARNESS_VERSION: &str = EXEC_HARNESS_INSTALLER.version; + +const MONGO_TRACER_INSTALLER: BinaryPin = BinaryPin { + version: "cs-mongo-tracer-v0.2.0", + url_template: "https://codspeed-public-assets.s3.eu-west-1.amazonaws.com/mongo-tracer/{version}/cs-mongo-tracer-installer.sh", + sha256: "685f1d540cb24c2aa6f447991958339c6b70ec7664df2dba2713b8b3d77687e7", +}; + +/// A binary the runner downloads at install time. The download helper looks +/// up the URL and SHA-256 via `url()` and `sha256()` and rejects the install +/// if the bytes don't match. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum PinnedBinary { + ValgrindDeb(ValgrindTarget), + MemtrackInstaller, + ExecHarnessInstaller, + MongoTracerInstaller, +} + +impl PinnedBinary { + pub fn url(&self) -> String { + match self { + PinnedBinary::ValgrindDeb(target) => target.url(), + PinnedBinary::MemtrackInstaller => MEMTRACK_INSTALLER.url(), + PinnedBinary::ExecHarnessInstaller => EXEC_HARNESS_INSTALLER.url(), + PinnedBinary::MongoTracerInstaller => MONGO_TRACER_INSTALLER.url(), + } + } + + pub fn sha256(&self) -> &'static str { + match self { + PinnedBinary::ValgrindDeb(target) => target.sha256(), + PinnedBinary::MemtrackInstaller => MEMTRACK_INSTALLER.sha256, + PinnedBinary::ExecHarnessInstaller => EXEC_HARNESS_INSTALLER.sha256, + PinnedBinary::MongoTracerInstaller => MONGO_TRACER_INSTALLER.sha256, + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::cli::run::helpers::download_pinned_file; + use tempfile::NamedTempFile; + + const INSTALLER_BINARIES: &[PinnedBinary] = &[ + PinnedBinary::MemtrackInstaller, + PinnedBinary::ExecHarnessInstaller, + PinnedBinary::MongoTracerInstaller, + ]; + + const ALL_VALGRIND_TARGETS: &[ValgrindTarget] = &[ + ValgrindTarget { + distro_version: DistroVersion::Ubuntu2204, + arch: Arch::Amd64, + }, + ValgrindTarget { + distro_version: DistroVersion::Ubuntu2404, + arch: Arch::Amd64, + }, + ValgrindTarget { + distro_version: DistroVersion::Ubuntu2204, + arch: Arch::Arm64, + }, + ValgrindTarget { + distro_version: DistroVersion::Ubuntu2404, + arch: Arch::Arm64, + }, + ]; + + fn assert_installer_variant_is_listed(binary: PinnedBinary) { + match binary { + PinnedBinary::ValgrindDeb(_) => {} + PinnedBinary::MemtrackInstaller + | PinnedBinary::ExecHarnessInstaller + | PinnedBinary::MongoTracerInstaller => { + assert!(INSTALLER_BINARIES.contains(&binary)); + } + } + } + + fn all_pinned_binaries() -> impl Iterator { + ALL_VALGRIND_TARGETS + .iter() + .copied() + .map(PinnedBinary::ValgrindDeb) + .chain(INSTALLER_BINARIES.iter().copied()) + } + + #[test] + fn installer_variant_list_is_exhaustive() { + assert_installer_variant_is_listed(PinnedBinary::MemtrackInstaller); + assert_installer_variant_is_listed(PinnedBinary::ExecHarnessInstaller); + assert_installer_variant_is_listed(PinnedBinary::MongoTracerInstaller); + } + + // Network-bound: downloads every pinned URL and asserts its bytes hash to + // the declared SHA-256. Skipped locally; CI sets `GITHUB_ACTIONS=true`. + // Run after bumping a version to make sure the release won't ship a stale + // or mistyped hash. + #[test_with::env(GITHUB_ACTIONS)] + #[tokio::test(flavor = "multi_thread")] + async fn all_pinned_binaries_match_their_declared_sha256() { + let results = futures::future::join_all(all_pinned_binaries().map(|binary| async move { + let temp = NamedTempFile::new().expect("failed to create temp file"); + download_pinned_file(binary, temp.path()) + .await + .map_err(|e| format!("{binary:?} ({}): {e}", binary.url())) + })) + .await; + + let failures: Vec<_> = results.into_iter().filter_map(Result::err).collect(); + assert!( + failures.is_empty(), + "pinned binaries failed verification:\n - {}", + failures.join("\n - "), + ); + } +} diff --git a/src/cli/run/helpers/download_file.rs b/src/cli/run/helpers/download_file.rs index ce9df1d8..7b9e67b7 100644 --- a/src/cli/run/helpers/download_file.rs +++ b/src/cli/run/helpers/download_file.rs @@ -1,9 +1,10 @@ +use crate::binary_pins::PinnedBinary; use crate::{prelude::*, request_client::REQUEST_CLIENT}; use std::path::Path; use url::Url; -pub async fn download_file(url: &Url, path: &Path) -> Result<()> { +async fn download_file(url: &Url, path: &Path) -> Result<()> { debug!("Downloading file: {url}"); let response = REQUEST_CLIENT .get(url.clone()) @@ -23,3 +24,26 @@ pub async fn download_file(url: &Url, path: &Path) -> Result<()> { .map_err(|e| anyhow!("Failed to write to file: {}, {}", path.display(), e))?; Ok(()) } + +/// Download a `PinnedBinary` and verify its bytes against its pinned +/// SHA-256. On mismatch the partial file is +/// removed and an error is returned. +pub async fn download_pinned_file(binary: PinnedBinary, path: &Path) -> Result<()> { + let url_str = binary.url(); + let url = Url::parse(&url_str).context("failed to parse pinned URL")?; + download_file(&url, path).await?; + + let actual = sha256::try_digest(path) + .with_context(|| format!("failed to compute sha256 of {}", path.display()))?; + let expected = binary.sha256(); + + if actual != expected { + let _ = std::fs::remove_file(path); + bail!( + "Hash mismatch for {url_str}: expected {expected}, got {actual}. The downloaded file has been deleted." + ); + } + + debug!("Verified sha256 of {url_str}"); + Ok(()) +} diff --git a/src/cli/run/helpers/mod.rs b/src/cli/run/helpers/mod.rs index 98af84a2..09c05191 100644 --- a/src/cli/run/helpers/mod.rs +++ b/src/cli/run/helpers/mod.rs @@ -5,7 +5,7 @@ mod format_memory; mod get_env_var; mod parse_git_remote; -pub(crate) use download_file::download_file; +pub(crate) use download_file::download_pinned_file; pub(crate) use find_repository_root::find_repository_root; pub(crate) use format_duration::format_duration; pub(crate) use format_memory::format_memory; diff --git a/src/cli/shared.rs b/src/cli/shared.rs index 14ed0272..077b47aa 100644 --- a/src/cli/shared.rs +++ b/src/cli/shared.rs @@ -96,7 +96,7 @@ pub struct ExecAndRunSharedArgs { pub allow_empty: bool, /// The version of the go-runner to use (e.g., 1.2.3, 1.0.0-beta.1) - /// If not specified, the latest version will be installed + /// If not specified, the runner installs the pinned default version #[arg(long, env = "CODSPEED_GO_RUNNER_VERSION", value_parser = parse_version)] pub go_runner_version: Option, diff --git a/src/executor/helpers/introspected_golang/go.sh b/src/executor/helpers/introspected_golang/go.sh index 5601d136..8c1bef44 100755 --- a/src/executor/helpers/introspected_golang/go.sh +++ b/src/executor/helpers/introspected_golang/go.sh @@ -12,6 +12,73 @@ debug_log "Called with arguments: $*" debug_log "Number of arguments: $#" +GO_RUNNER_INSTALLER_SHA256S=" +0.1.1 f841950fe630dbc5c1bd534eb7cb8acf476b744297081710866092551110a68f +0.1.2 cdbe066adccad7eb11ca53daf57a2c754666966d6ec9ab02e399f6f00a87f3eb +0.2.0 10b75883bd8c09133281d652ea3ae1897f35606dd8a4a722d5c9b304666eaf7e +0.3.0 d50608b9ceff1426badf5cb868bb76eca74428715006029519248d61c4781799 +0.4.0 dfb13bd30afef8a430680813b5605f46243962f6e31359f387d7c90ee9018be3 +0.4.1 94b8b82a095597e3bf4fea12cccd6bbc2e52e619d7953c3fcb695651ae5e804c +0.4.2 5fca476647e269aedc91e5920265867de56e4dcada5bd83d99a507e437236e06 +0.5.0 82c68678904691903567f542117bcbf6aceca87b34db14c946915b988f12cf8f +0.5.1 db2de37e815913ce72f761a8c25aeef4a64fe2407a870e0dc2ea149a3decd904 +0.6.0 e7bf7c37b07bc43610cf35140556b51cb03b4355914307f8100bef5f7f37c85d +0.6.1 a38e0e3417abce9260f1b8c3ff7407bb93900a0ad83ee191725a0266f97c797c +0.6.2 616200762cc2fa582fae56ea58e77ad6c056bd658f6907d5be56d39d59ec6616 +1.0.0 0540d8abe62357acefb85b9f1a9ff81dcfef70d6be8bea35096bf26a295a91f8 +1.0.1 c26f463883a77591e5a2e2f17f0995a989cbada0d4f5115f327900badac07918 +1.0.2 4e4ecfb1888ced253f0acbbc132db0b1d7e99351d40f3eff789a518a6130ee35 +1.1.0 d16e0e14bdfaea61a6da1d46d7b3b36f940b64335c8affbdc85b802d6e949a97 +1.2.0 072876ccd43b8c73c123df206eda4b1f82f9ff03b1330efe35e5eaa5c1b6cefe +" + +DEFAULT_GO_RUNNER_VERSION="1.2.0" + +get_go_runner_installer_sha256() { + if ! awk -v v="$1" '$1==v{print $2; f=1} END{exit !f}' <<<"$GO_RUNNER_INSTALLER_SHA256S"; then + echo "ERROR: No pinned sha256 for codspeed-go-runner version $1" >&2 + exit 1 + fi +} + +sha256_file() { + if command -v sha256sum >/dev/null 2>&1; then + sha256sum "$1" | awk '{ print $1 }' + elif command -v shasum >/dev/null 2>&1; then + shasum -a 256 "$1" | awk '{ print $1 }' + else + echo "ERROR: Could not find sha256sum or shasum to verify downloaded installer" >&2 + exit 1 + fi +} + +install_go_runner() { + local version="$1" + local expected_sha256 + expected_sha256=$(get_go_runner_installer_sha256 "$version") + local download_url="https://github.com/CodSpeedHQ/codspeed-go/releases/download/v${version}/codspeed-go-runner-installer.sh" + local tmp_dir + tmp_dir=$(mktemp -d) + local installer_path="$tmp_dir/codspeed-go-runner-installer.sh" + + cleanup_go_runner_installer() { + rm -rf "$tmp_dir" + } + trap cleanup_go_runner_installer RETURN + + curl -fsSL "$download_url" -o "$installer_path" + + local actual_sha256 + actual_sha256=$(sha256_file "$installer_path") + if [ "$actual_sha256" != "$expected_sha256" ]; then + echo "ERROR: Hash mismatch for $download_url: expected $expected_sha256, got $actual_sha256" >&2 + exit 1 + fi + + bash "$installer_path" --quiet +} + + # Currently only walltime is supported if [ "${CODSPEED_RUNNER_MODE:-}" != "walltime" ]; then echo "CRITICAL: Go benchmarks can only be run with the walltime instrument" @@ -47,17 +114,9 @@ case "$1" in # Find go-runner or install if not found GO_RUNNER=$(which codspeed-go-runner 2>/dev/null || true) if [ -z "$GO_RUNNER" ]; then - # Build the installer URL with the specified version or use latest - INSTALLER_VERSION="${CODSPEED_GO_RUNNER_VERSION:-latest}" - if [ "$INSTALLER_VERSION" = "latest" ]; then - DOWNLOAD_URL="http://github.com/CodSpeedHQ/codspeed-go/releases/latest/download/codspeed-go-runner-installer.sh" - echo "::warning::Installing the latest version of codspeed-go-runner. This can silently introduce breaking changes. We recommend pinning a specific version via the \`go-runner-version\` option in the action." >&2 - else - DOWNLOAD_URL="http://github.com/CodSpeedHQ/codspeed-go/releases/download/v${INSTALLER_VERSION}/codspeed-go-runner-installer.sh" - fi - - debug_log "Installing go-runner from: $DOWNLOAD_URL" - curl -fsSL "$DOWNLOAD_URL" | bash -s -- --quiet + INSTALLER_VERSION="${CODSPEED_GO_RUNNER_VERSION:-$DEFAULT_GO_RUNNER_VERSION}" + debug_log "Installing go-runner v${INSTALLER_VERSION}" + install_go_runner "$INSTALLER_VERSION" GO_RUNNER=$(which codspeed-go-runner 2>/dev/null || true) fi diff --git a/src/executor/helpers/introspected_golang/mod.rs b/src/executor/helpers/introspected_golang/mod.rs index 9a3aeb93..d6e85f08 100644 --- a/src/executor/helpers/introspected_golang/mod.rs +++ b/src/executor/helpers/introspected_golang/mod.rs @@ -17,3 +17,94 @@ pub fn setup() -> Result { script_file.set_permissions(perms)?; Ok(script_folder) } + +#[cfg(test)] +mod tests { + use super::*; + use crate::request_client::REQUEST_CLIENT; + + fn pinned_go_runner_installers() -> Vec<(String, String)> { + let start = INTROSPECTED_GO_SCRIPT + .find("GO_RUNNER_INSTALLER_SHA256S=\"") + .expect("GO_RUNNER_INSTALLER_SHA256S table not found in go.sh"); + let body_start = INTROSPECTED_GO_SCRIPT[start..] + .find('\n') + .map(|i| start + i + 1) + .expect("malformed GO_RUNNER_INSTALLER_SHA256S table"); + let body_end = INTROSPECTED_GO_SCRIPT[body_start..] + .find("\n\"") + .map(|i| body_start + i) + .expect("unterminated GO_RUNNER_INSTALLER_SHA256S table"); + + INTROSPECTED_GO_SCRIPT[body_start..body_end] + .lines() + .filter(|line| !line.trim().is_empty()) + .map(|line| { + let mut parts = line.split_whitespace(); + let version = parts.next().expect("missing version").to_string(); + let sha256 = parts.next().expect("missing sha256").to_string(); + assert!( + parts.next().is_none(), + "unexpected extra column in GO_RUNNER_INSTALLER_SHA256S row: {line:?}" + ); + (version, sha256) + }) + .collect() + } + + #[test] + fn pinned_go_runner_installers_parses_table() { + let pins = pinned_go_runner_installers(); + assert!(!pins.is_empty(), "no go-runner installer pins parsed"); + for (version, sha256) in &pins { + assert!(!version.is_empty(), "empty version in pin row"); + assert_eq!(sha256.len(), 64, "sha256 must be 64 hex chars: {sha256}"); + assert!( + sha256.chars().all(|c| c.is_ascii_hexdigit()), + "sha256 must be hex: {sha256}", + ); + } + } + + // Network-bound: downloads every pinned go-runner installer and asserts its + // bytes hash to the declared SHA-256. Skipped locally; CI sets + // `GITHUB_ACTIONS=true`. Run after bumping a version to make sure the + // release won't ship a stale or mistyped hash. + #[test_with::env(GITHUB_ACTIONS)] + #[tokio::test(flavor = "multi_thread")] + async fn all_pinned_go_runner_installers_match_their_declared_sha256() { + let pins = pinned_go_runner_installers(); + + let results = futures::future::join_all(pins.into_iter().map(|(version, expected)| async move { + let url = format!( + "https://github.com/CodSpeedHQ/codspeed-go/releases/download/v{version}/codspeed-go-runner-installer.sh" + ); + let bytes = REQUEST_CLIENT + .get(&url) + .send() + .await + .map_err(|e| format!("{version} ({url}): request failed: {e}"))? + .error_for_status() + .map_err(|e| format!("{version} ({url}): {e}"))? + .bytes() + .await + .map_err(|e| format!("{version} ({url}): read failed: {e}"))?; + let actual = sha256::digest(bytes.as_ref()); + if actual != expected { + Err(format!( + "{version} ({url}): expected {expected}, got {actual}" + )) + } else { + Ok(()) + } + })) + .await; + + let failures: Vec<_> = results.into_iter().filter_map(Result::err).collect(); + assert!( + failures.is_empty(), + "pinned go-runner installers failed verification:\n - {}", + failures.join("\n - "), + ); + } +} diff --git a/src/executor/memory/setup.rs b/src/executor/memory/setup.rs index 3db3aada..bab8b6d9 100644 --- a/src/executor/memory/setup.rs +++ b/src/executor/memory/setup.rs @@ -1,10 +1,11 @@ use crate::binary_installer::ensure_binary_installed; +use crate::binary_pins::{self, PinnedBinary}; use crate::executor::{ToolInstallStatus, ToolStatus}; use crate::prelude::*; use std::process::Command; pub const MEMTRACK_COMMAND: &str = "codspeed-memtrack"; -pub const MEMTRACK_CODSPEED_VERSION: &str = "1.2.3"; +pub const MEMTRACK_CODSPEED_VERSION: &str = binary_pins::MEMTRACK_VERSION; pub fn get_memtrack_status() -> ToolStatus { let tool_name = MEMTRACK_COMMAND.to_string(); @@ -70,16 +71,10 @@ pub fn get_memtrack_status() -> ToolStatus { } pub async fn install_memtrack() -> Result<()> { - let get_memtrack_installer_url = || { - format!( - "https://github.com/CodSpeedHQ/codspeed/releases/download/memtrack-v{MEMTRACK_CODSPEED_VERSION}/memtrack-installer.sh" - ) - }; - ensure_binary_installed( MEMTRACK_COMMAND, MEMTRACK_CODSPEED_VERSION, - get_memtrack_installer_url, + PinnedBinary::MemtrackInstaller, ) .await } diff --git a/src/executor/orchestrator.rs b/src/executor/orchestrator.rs index 0ab89209..10f58d50 100644 --- a/src/executor/orchestrator.rs +++ b/src/executor/orchestrator.rs @@ -1,6 +1,7 @@ use super::{ExecutionContext, ExecutorName, get_executor_from_mode, run_executor}; use crate::api_client::CodSpeedAPIClient; use crate::binary_installer::ensure_binary_installed; +use crate::binary_pins::{self, PinnedBinary}; use crate::cli::exec::multi_targets; use crate::cli::run::logger::Logger; use crate::executor::config::BenchmarkTarget; @@ -18,7 +19,7 @@ use std::collections::BTreeMap; use std::path::{Path, PathBuf}; pub const EXEC_HARNESS_COMMAND: &str = "exec-harness"; -pub const EXEC_HARNESS_VERSION: &str = "1.3.0"; +pub const EXEC_HARNESS_VERSION: &str = binary_pins::EXEC_HARNESS_VERSION; /// Shared orchestration state created once per CLI invocation. /// @@ -82,11 +83,11 @@ impl Orchestrator { .collect(); if !exec_targets.is_empty() { - ensure_binary_installed(EXEC_HARNESS_COMMAND, EXEC_HARNESS_VERSION, || { - format!( - "https://github.com/CodSpeedHQ/codspeed/releases/download/exec-harness-v{EXEC_HARNESS_VERSION}/exec-harness-installer.sh" - ) - }) + ensure_binary_installed( + EXEC_HARNESS_COMMAND, + EXEC_HARNESS_VERSION, + PinnedBinary::ExecHarnessInstaller, + ) .await?; let pipe_cmd = multi_targets::build_exec_targets_pipe_command(&exec_targets)?; diff --git a/src/executor/valgrind/setup.rs b/src/executor/valgrind/setup.rs index adcf1961..362ebac6 100644 --- a/src/executor/valgrind/setup.rs +++ b/src/executor/valgrind/setup.rs @@ -1,52 +1,56 @@ -use crate::cli::run::helpers::download_file; +use crate::binary_pins::{ + Arch, DistroVersion, PinnedBinary, VALGRIND_CODSPEED_VERSION, VALGRIND_CODSPEED_VERSION_STRING, + ValgrindTarget, +}; +use crate::cli::run::helpers::download_pinned_file; use crate::executor::helpers::apt; use crate::executor::{ToolInstallStatus, ToolStatus}; use crate::prelude::*; use crate::system::{LinuxDistribution, SupportedOs, SystemInfo}; -use crate::{ - VALGRIND_CODSPEED_DEB_VERSION, VALGRIND_CODSPEED_VERSION, VALGRIND_CODSPEED_VERSION_STRING, -}; use semver::Version; use std::{env, path::Path, process::Command}; -use url::Url; -pub(super) fn get_codspeed_valgrind_filename(system_info: &SystemInfo) -> Result { +fn get_codspeed_valgrind_target(system_info: &SystemInfo) -> Result { let SupportedOs::Linux(distro) = &system_info.os else { bail!("Unsupported system"); }; - let (deb_ubuntu_version, architecture) = match (distro, system_info.arch.as_str()) { + let (distro_version, arch) = match (distro, system_info.arch.as_str()) { (LinuxDistribution::Ubuntu { version }, "x86_64") | (LinuxDistribution::Debian { version }, "x86_64") if version == "22.04" || version == "12" => { - ("22.04", "amd64") + (DistroVersion::Ubuntu2204, Arch::Amd64) } (LinuxDistribution::Ubuntu { version }, "x86_64") if version == "24.04" => { - ("24.04", "amd64") + (DistroVersion::Ubuntu2404, Arch::Amd64) } (LinuxDistribution::Ubuntu { version }, "aarch64") | (LinuxDistribution::Debian { version }, "aarch64") if version == "22.04" || version == "12" => { - ("22.04", "arm64") + (DistroVersion::Ubuntu2204, Arch::Arm64) } (LinuxDistribution::Ubuntu { version }, "aarch64") if version == "24.04" => { - ("24.04", "arm64") + (DistroVersion::Ubuntu2404, Arch::Arm64) } _ => bail!("Unsupported system"), }; - Ok(format!( - "valgrind_{}_ubuntu-{}_{}.deb", - VALGRIND_CODSPEED_DEB_VERSION.as_str(), - deb_ubuntu_version, - architecture - )) + Ok(ValgrindTarget { + distro_version, + arch, + }) +} + +fn get_codspeed_valgrind_binary(system_info: &SystemInfo) -> Result { + Ok(PinnedBinary::ValgrindDeb(get_codspeed_valgrind_target( + system_info, + )?)) } pub(super) fn is_codspeed_valgrind_installation_supported(system_info: &SystemInfo) -> bool { - get_codspeed_valgrind_filename(system_info).is_ok() + get_codspeed_valgrind_target(system_info).is_ok() } /// Parse a valgrind version string and extract the semantic version. @@ -168,13 +172,9 @@ pub async fn install_valgrind( is_valgrind_installed, || async { debug!("Installing valgrind"); - let valgrind_deb_url = format!( - "https://github.com/CodSpeedHQ/valgrind-codspeed/releases/download/{}/{}", - VALGRIND_CODSPEED_DEB_VERSION.as_str(), - get_codspeed_valgrind_filename(system_info)? - ); + let binary = get_codspeed_valgrind_binary(system_info)?; let deb_path = env::temp_dir().join("valgrind-codspeed.deb"); - download_file(&Url::parse(valgrind_deb_url.as_str()).unwrap(), &deb_path).await?; + download_pinned_file(binary, &deb_path).await?; apt::install(system_info, &[deb_path.to_str().unwrap()])?; // Return package names for caching @@ -191,7 +191,7 @@ mod tests { use super::*; #[test] - fn test_system_info_to_codspeed_valgrind_version_ubuntu() { + fn test_system_info_to_codspeed_valgrind_url_ubuntu() { let system_info = SystemInfo { os: SupportedOs::Linux(LinuxDistribution::Ubuntu { version: "22.04".into(), @@ -200,13 +200,13 @@ mod tests { ..SystemInfo::test() }; assert_snapshot!( - get_codspeed_valgrind_filename(&system_info).unwrap(), - @"valgrind_3.26.0-0codspeed2_ubuntu-22.04_amd64.deb" + get_codspeed_valgrind_binary(&system_info).unwrap().url(), + @"https://github.com/CodSpeedHQ/valgrind-codspeed/releases/download/3.26.0-0codspeed2/valgrind_3.26.0-0codspeed2_ubuntu-22.04_amd64.deb" ); } #[test] - fn test_system_info_to_codspeed_valgrind_version_ubuntu_24() { + fn test_system_info_to_codspeed_valgrind_url_ubuntu_24() { let system_info = SystemInfo { os: SupportedOs::Linux(LinuxDistribution::Ubuntu { version: "24.04".into(), @@ -215,13 +215,13 @@ mod tests { ..SystemInfo::test() }; assert_snapshot!( - get_codspeed_valgrind_filename(&system_info).unwrap(), - @"valgrind_3.26.0-0codspeed2_ubuntu-24.04_amd64.deb" + get_codspeed_valgrind_binary(&system_info).unwrap().url(), + @"https://github.com/CodSpeedHQ/valgrind-codspeed/releases/download/3.26.0-0codspeed2/valgrind_3.26.0-0codspeed2_ubuntu-24.04_amd64.deb" ); } #[test] - fn test_system_info_to_codspeed_valgrind_version_debian() { + fn test_system_info_to_codspeed_valgrind_url_debian() { let system_info = SystemInfo { os: SupportedOs::Linux(LinuxDistribution::Debian { version: "12".into(), @@ -230,13 +230,13 @@ mod tests { ..SystemInfo::test() }; assert_snapshot!( - get_codspeed_valgrind_filename(&system_info).unwrap(), - @"valgrind_3.26.0-0codspeed2_ubuntu-22.04_amd64.deb" + get_codspeed_valgrind_binary(&system_info).unwrap().url(), + @"https://github.com/CodSpeedHQ/valgrind-codspeed/releases/download/3.26.0-0codspeed2/valgrind_3.26.0-0codspeed2_ubuntu-22.04_amd64.deb" ); } #[test] - fn test_system_info_to_codspeed_valgrind_version_ubuntu_arm() { + fn test_system_info_to_codspeed_valgrind_url_ubuntu_arm() { let system_info = SystemInfo { os: SupportedOs::Linux(LinuxDistribution::Ubuntu { version: "22.04".into(), @@ -245,31 +245,31 @@ mod tests { ..SystemInfo::test() }; assert_snapshot!( - get_codspeed_valgrind_filename(&system_info).unwrap(), - @"valgrind_3.26.0-0codspeed2_ubuntu-22.04_arm64.deb" + get_codspeed_valgrind_binary(&system_info).unwrap().url(), + @"https://github.com/CodSpeedHQ/valgrind-codspeed/releases/download/3.26.0-0codspeed2/valgrind_3.26.0-0codspeed2_ubuntu-22.04_arm64.deb" ); } #[test] - fn test_codspeed_valgrind_filename_unsupported_os() { + fn test_codspeed_valgrind_unsupported_os() { let system_info = SystemInfo { os: SupportedOs::Macos { version: "14.0".into(), }, ..SystemInfo::test() }; - assert!(get_codspeed_valgrind_filename(&system_info).is_err()); + assert!(get_codspeed_valgrind_binary(&system_info).is_err()); } #[test] - fn test_codspeed_valgrind_filename_unsupported_distro() { + fn test_codspeed_valgrind_unsupported_distro() { let system_info = SystemInfo { os: SupportedOs::Linux(LinuxDistribution::Ubuntu { version: "20.04".into(), }), ..SystemInfo::test() }; - assert!(get_codspeed_valgrind_filename(&system_info).is_err()); + assert!(get_codspeed_valgrind_binary(&system_info).is_err()); } #[test] diff --git a/src/instruments/mongo_tracer.rs b/src/instruments/mongo_tracer.rs index c16007a6..7f10d21d 100644 --- a/src/instruments/mongo_tracer.rs +++ b/src/instruments/mongo_tracer.rs @@ -11,8 +11,9 @@ use reqwest::Client; use tokio::fs; use url::Url; -use crate::{MONGODB_TRACER_VERSION, cli::run::helpers::get_env_variable}; -use crate::{cli::run::helpers::download_file, prelude::*}; +use crate::binary_pins::PinnedBinary; +use crate::cli::run::helpers::{download_pinned_file, get_env_variable}; +use crate::prelude::*; use super::MongoDBConfig; @@ -227,16 +228,8 @@ impl MongoTracer { pub async fn install_mongodb_tracer() -> Result<()> { debug!("Installing mongodb-tracer"); - // TODO: release the tracer and update this url - let installer_url = format!( - "https://codspeed-public-assets.s3.eu-west-1.amazonaws.com/mongo-tracer/{MONGODB_TRACER_VERSION}/cs-mongo-tracer-installer.sh" - ); let installer_path = env::temp_dir().join("cs-mongo-tracer-installer.sh"); - download_file( - &Url::parse(installer_url.as_str()).unwrap(), - &installer_path, - ) - .await?; + download_pinned_file(PinnedBinary::MongoTracerInstaller, &installer_path).await?; let output = Command::new("bash") .arg(installer_path.to_str().unwrap()) diff --git a/src/lib.rs b/src/lib.rs index 76fe2b85..7a555e51 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -2,6 +2,7 @@ mod api_client; mod binary_installer; +mod binary_pins; pub mod cli; mod config; mod executor; @@ -20,16 +21,4 @@ pub use local_logger::clean_logger; pub use project_config::{ProjectConfig, ProjectOptions, Target, TargetOptions, WalltimeOptions}; pub use runner_mode::RunnerMode; -use semver::Version; -use std::sync::LazyLock; - pub const VERSION: &str = env!("CARGO_PKG_VERSION"); -pub const MONGODB_TRACER_VERSION: &str = "cs-mongo-tracer-v0.2.0"; - -pub const VALGRIND_CODSPEED_VERSION: Version = Version::new(3, 26, 0); -pub const VALGRIND_CODSPEED_DEB_REVISION_SUFFIX: &str = "0codspeed2"; -pub static VALGRIND_CODSPEED_VERSION_STRING: LazyLock = - LazyLock::new(|| format!("{VALGRIND_CODSPEED_VERSION}.codspeed")); -pub static VALGRIND_CODSPEED_DEB_VERSION: LazyLock = LazyLock::new(|| { - format!("{VALGRIND_CODSPEED_VERSION}-{VALGRIND_CODSPEED_DEB_REVISION_SUFFIX}") -});