From 584196f5fbc1f5fabff9c7e9f3f3eb8f9e45c164 Mon Sep 17 00:00:00 2001 From: Tobias Bieniek Date: Thu, 13 Nov 2025 12:58:50 +0100 Subject: [PATCH 1/3] Extract `crates_io_heroku` crate with `slug_commit()` fn This adds a new workspace crate to centralize access to Heroku-specific environment variables, starting with `HEROKU_SLUG_COMMIT`. The new `slug_commit()` function is now used in: - Sentry release tracking - Site metadata API endpoint - Database dump metadata Documentation notes that the environment variable is provided by the `runtime-dyno-metadata` Heroku Labs feature. --- Cargo.lock | 10 +++++++ Cargo.toml | 1 + crates/crates_io_database_dump/Cargo.toml | 1 + crates/crates_io_database_dump/src/lib.rs | 6 +++-- crates/crates_io_heroku/Cargo.toml | 12 +++++++++ crates/crates_io_heroku/README.md | 11 ++++++++ crates/crates_io_heroku/src/lib.rs | 33 +++++++++++++++++++++++ src/config/sentry.rs | 3 ++- src/controllers/site_metadata.rs | 3 ++- src/sentry/mod.rs | 3 ++- 10 files changed, 78 insertions(+), 5 deletions(-) create mode 100644 crates/crates_io_heroku/Cargo.toml create mode 100644 crates/crates_io_heroku/README.md create mode 100644 crates/crates_io_heroku/src/lib.rs diff --git a/Cargo.lock b/Cargo.lock index db97554c195..3d2cab7fc67 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1421,6 +1421,7 @@ dependencies = [ "crates_io_docs_rs", "crates_io_env_vars", "crates_io_github", + "crates_io_heroku", "crates_io_index", "crates_io_linecount", "crates_io_markdown", @@ -1570,6 +1571,7 @@ version = "0.0.0" dependencies = [ "anyhow", "chrono", + "crates_io_heroku", "crates_io_test_db", "diesel", "diesel-async", @@ -1645,6 +1647,14 @@ dependencies = [ "tracing-subscriber", ] +[[package]] +name = "crates_io_heroku" +version = "0.0.0" +dependencies = [ + "anyhow", + "crates_io_env_vars", +] + [[package]] name = "crates_io_index" version = "0.0.0" diff --git a/Cargo.toml b/Cargo.toml index e5b0b550e1e..3f3756ab29c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -74,6 +74,7 @@ crates_io_diesel_helpers = { path = "crates/crates_io_diesel_helpers" } crates_io_docs_rs = { path = "crates/crates_io_docs_rs" } crates_io_env_vars = { path = "crates/crates_io_env_vars" } crates_io_github = { path = "crates/crates_io_github" } +crates_io_heroku = { path = "crates/crates_io_heroku" } crates_io_index = { path = "crates/crates_io_index" } crates_io_linecount = { path = "crates/crates_io_linecount" } crates_io_markdown = { path = "crates/crates_io_markdown" } diff --git a/crates/crates_io_database_dump/Cargo.toml b/crates/crates_io_database_dump/Cargo.toml index 984b8783a81..da0c229b65c 100644 --- a/crates/crates_io_database_dump/Cargo.toml +++ b/crates/crates_io_database_dump/Cargo.toml @@ -10,6 +10,7 @@ workspace = true [dependencies] anyhow = "=1.0.100" chrono = { version = "=0.4.42", default-features = false, features = ["clock", "serde"] } +crates_io_heroku = { path = "../crates_io_heroku" } flate2 = "=1.1.5" minijinja = "=2.12.0" serde = { version = "=1.0.228", features = ["derive"] } diff --git a/crates/crates_io_database_dump/src/lib.rs b/crates/crates_io_database_dump/src/lib.rs index c1f9e06c04a..892f5cb218b 100644 --- a/crates/crates_io_database_dump/src/lib.rs +++ b/crates/crates_io_database_dump/src/lib.rs @@ -70,8 +70,10 @@ impl DumpDirectory { } let metadata = Metadata { timestamp: &self.timestamp, - crates_io_commit: std::env::var("HEROKU_SLUG_COMMIT") - .unwrap_or_else(|_| "unknown".to_owned()), + crates_io_commit: crates_io_heroku::slug_commit() + .ok() + .flatten() + .unwrap_or_else(|| "unknown".to_owned()), }; let path = self.path().join("metadata.json"); debug!(?path, "Writing metadata.json file…"); diff --git a/crates/crates_io_heroku/Cargo.toml b/crates/crates_io_heroku/Cargo.toml new file mode 100644 index 00000000000..8d330d0374a --- /dev/null +++ b/crates/crates_io_heroku/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "crates_io_heroku" +version = "0.0.0" +license = "MIT OR Apache-2.0" +edition = "2024" + +[lints] +workspace = true + +[dependencies] +anyhow = "=1.0.100" +crates_io_env_vars = { path = "../crates_io_env_vars" } diff --git a/crates/crates_io_heroku/README.md b/crates/crates_io_heroku/README.md new file mode 100644 index 00000000000..ee231d8ee63 --- /dev/null +++ b/crates/crates_io_heroku/README.md @@ -0,0 +1,11 @@ +# crates_io_heroku + +This package contains utilities for accessing Heroku-specific environment +variables. + +When the `runtime-dyno-metadata` Heroku Labs feature is enabled, Heroku +exposes application and environment information through environment variables. +This crate provides convenient functions to access these values. + +For more information, see: + diff --git a/crates/crates_io_heroku/src/lib.rs b/crates/crates_io_heroku/src/lib.rs new file mode 100644 index 00000000000..1fdd03d7e71 --- /dev/null +++ b/crates/crates_io_heroku/src/lib.rs @@ -0,0 +1,33 @@ +#![doc = include_str!("../README.md")] + +use crates_io_env_vars::var; + +/// Returns the Git SHA of the currently deployed commit. +/// +/// This value comes from the `HEROKU_SLUG_COMMIT` environment variable, +/// which is set by Heroku when the `runtime-dyno-metadata` Labs feature +/// is enabled. If the variable is not set (e.g., in local development +/// or when the feature is disabled), returns `None`. +/// +/// Note: `HEROKU_SLUG_COMMIT` is deprecated by Heroku in favor of +/// `HEROKU_BUILD_COMMIT`, but this function continues to use +/// `HEROKU_SLUG_COMMIT` for backward compatibility with existing +/// deployments. +/// +/// See for more +/// information. +/// +/// # Examples +/// +/// ``` +/// use crates_io_heroku::slug_commit; +/// +/// if let Ok(Some(commit)) = slug_commit() { +/// println!("Running commit: {}", commit); +/// } else { +/// println!("Commit SHA unknown"); +/// } +/// ``` +pub fn slug_commit() -> anyhow::Result> { + var("HEROKU_SLUG_COMMIT") +} diff --git a/src/config/sentry.rs b/src/config/sentry.rs index 3b0645edf05..cb18d445472 100644 --- a/src/config/sentry.rs +++ b/src/config/sentry.rs @@ -1,5 +1,6 @@ use anyhow::Context; use crates_io_env_vars::{required_var, var, var_parsed}; +use crates_io_heroku::slug_commit; use sentry::IntoDsn; use sentry::types::Dsn; @@ -28,7 +29,7 @@ impl SentryConfig { Ok(Self { dsn, environment, - release: var("HEROKU_SLUG_COMMIT")?, + release: slug_commit()?, traces_sample_rate: var_parsed("SENTRY_TRACES_SAMPLE_RATE")?.unwrap_or(0.0), }) } diff --git a/src/controllers/site_metadata.rs b/src/controllers/site_metadata.rs index 4b9467a5dfd..aeea2895b2c 100644 --- a/src/controllers/site_metadata.rs +++ b/src/controllers/site_metadata.rs @@ -1,6 +1,7 @@ use crate::app::AppState; use axum::Json; use axum::response::IntoResponse; +use crates_io_heroku::slug_commit; use serde::Serialize; #[derive(Debug, Serialize, utoipa::ToSchema)] @@ -33,7 +34,7 @@ pub struct MetadataResponse<'a> { pub async fn get_site_metadata(state: AppState) -> impl IntoResponse { let read_only = state.config.db.are_all_read_only(); - let deployed_sha = dotenvy::var("HEROKU_SLUG_COMMIT"); + let deployed_sha = slug_commit().ok().flatten(); let deployed_sha = deployed_sha.as_deref().unwrap_or("unknown"); Json(MetadataResponse { diff --git a/src/sentry/mod.rs b/src/sentry/mod.rs index 614a8cceaff..56bc19b1d83 100644 --- a/src/sentry/mod.rs +++ b/src/sentry/mod.rs @@ -12,7 +12,8 @@ use tracing::warn; /// be set if a DSN is provided. /// /// `HEROKU_SLUG_COMMIT`, if present, will be used as the `release` property -/// on all events. +/// on all events. This environment variable is provided by Heroku when the +/// `runtime-dyno-metadata` Labs feature is enabled. pub fn init() -> Option { let config = match SentryConfig::from_environment() { Ok(config) => config, From b493cb6b7417dd1c45992641f155921b00818b5b Mon Sep 17 00:00:00 2001 From: Tobias Bieniek Date: Thu, 13 Nov 2025 13:02:21 +0100 Subject: [PATCH 2/3] heroku: Add `build_commit()` fn Support `HEROKU_BUILD_COMMIT` environment variable, which is the current standard for retrieving the deployed commit SHA from Heroku. This variable is provided by the `runtime-dyno-build-metadata` Labs feature. `HEROKU_SLUG_COMMIT` remains supported via `slug_commit()` for backward compatibility, though it is deprecated by Heroku. --- crates/crates_io_heroku/src/lib.rs | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/crates/crates_io_heroku/src/lib.rs b/crates/crates_io_heroku/src/lib.rs index 1fdd03d7e71..ca342363adb 100644 --- a/crates/crates_io_heroku/src/lib.rs +++ b/crates/crates_io_heroku/src/lib.rs @@ -31,3 +31,31 @@ use crates_io_env_vars::var; pub fn slug_commit() -> anyhow::Result> { var("HEROKU_SLUG_COMMIT") } + +/// Returns the Git SHA of the currently deployed commit. +/// +/// This value comes from the `HEROKU_BUILD_COMMIT` environment variable, +/// which is set by Heroku when the `runtime-dyno-build-metadata` Labs +/// feature is enabled. If the variable is not set (e.g., in local development +/// or when the feature is disabled), returns `None`. +/// +/// This is the recommended function to use, as `HEROKU_BUILD_COMMIT` is +/// the current standard while `HEROKU_SLUG_COMMIT` is deprecated. +/// +/// See for more +/// information. +/// +/// # Examples +/// +/// ``` +/// use crates_io_heroku::build_commit; +/// +/// if let Ok(Some(commit)) = build_commit() { +/// println!("Running commit: {}", commit); +/// } else { +/// println!("Commit SHA unknown"); +/// } +/// ``` +pub fn build_commit() -> anyhow::Result> { + var("HEROKU_BUILD_COMMIT") +} From 91071eb0a9ce4780f5d41221e60bf8705fa6d69b Mon Sep 17 00:00:00 2001 From: Tobias Bieniek Date: Thu, 13 Nov 2025 13:04:42 +0100 Subject: [PATCH 3/3] heroku: Add and use `commit()` fn with fallback logic Use `HEROKU_BUILD_COMMIT` first, falling back to `HEROKU_SLUG_COMMIT` for backward compatibility with deployments using the older `runtime-dyno-metadata` Labs feature. --- crates/crates_io_database_dump/src/lib.rs | 2 +- crates/crates_io_heroku/src/lib.rs | 36 +++++++++++++++++++++++ src/config/sentry.rs | 4 +-- src/controllers/site_metadata.rs | 4 +-- 4 files changed, 41 insertions(+), 5 deletions(-) diff --git a/crates/crates_io_database_dump/src/lib.rs b/crates/crates_io_database_dump/src/lib.rs index 892f5cb218b..7709278b992 100644 --- a/crates/crates_io_database_dump/src/lib.rs +++ b/crates/crates_io_database_dump/src/lib.rs @@ -70,7 +70,7 @@ impl DumpDirectory { } let metadata = Metadata { timestamp: &self.timestamp, - crates_io_commit: crates_io_heroku::slug_commit() + crates_io_commit: crates_io_heroku::commit() .ok() .flatten() .unwrap_or_else(|| "unknown".to_owned()), diff --git a/crates/crates_io_heroku/src/lib.rs b/crates/crates_io_heroku/src/lib.rs index ca342363adb..203b21803dc 100644 --- a/crates/crates_io_heroku/src/lib.rs +++ b/crates/crates_io_heroku/src/lib.rs @@ -2,6 +2,42 @@ use crates_io_env_vars::var; +/// Returns the Git SHA of the currently deployed commit. +/// +/// This function tries `HEROKU_BUILD_COMMIT` first (the current standard), +/// and falls back to `HEROKU_SLUG_COMMIT` (deprecated) if the former is not +/// set. This provides compatibility with both old and new Heroku deployments. +/// +/// Both environment variables are set by Heroku when the appropriate Labs +/// features are enabled (`runtime-dyno-build-metadata` for `HEROKU_BUILD_COMMIT`, +/// `runtime-dyno-metadata` for `HEROKU_SLUG_COMMIT`). +/// +/// Returns `None` if neither variable is set (e.g., in local development). +/// +/// See for more +/// information. +/// +/// # Examples +/// +/// ``` +/// use crates_io_heroku::commit; +/// +/// if let Ok(Some(commit)) = commit() { +/// println!("Running commit: {}", commit); +/// } else { +/// println!("Commit SHA unknown"); +/// } +/// ``` +pub fn commit() -> anyhow::Result> { + // Try the current standard first + if let Some(commit) = build_commit()? { + return Ok(Some(commit)); + } + + // Fall back to the deprecated variable for backward compatibility + slug_commit() +} + /// Returns the Git SHA of the currently deployed commit. /// /// This value comes from the `HEROKU_SLUG_COMMIT` environment variable, diff --git a/src/config/sentry.rs b/src/config/sentry.rs index cb18d445472..b429cf4d3b3 100644 --- a/src/config/sentry.rs +++ b/src/config/sentry.rs @@ -1,6 +1,6 @@ use anyhow::Context; use crates_io_env_vars::{required_var, var, var_parsed}; -use crates_io_heroku::slug_commit; +use crates_io_heroku::commit; use sentry::IntoDsn; use sentry::types::Dsn; @@ -29,7 +29,7 @@ impl SentryConfig { Ok(Self { dsn, environment, - release: slug_commit()?, + release: commit()?, traces_sample_rate: var_parsed("SENTRY_TRACES_SAMPLE_RATE")?.unwrap_or(0.0), }) } diff --git a/src/controllers/site_metadata.rs b/src/controllers/site_metadata.rs index aeea2895b2c..05f3de22063 100644 --- a/src/controllers/site_metadata.rs +++ b/src/controllers/site_metadata.rs @@ -1,7 +1,7 @@ use crate::app::AppState; use axum::Json; use axum::response::IntoResponse; -use crates_io_heroku::slug_commit; +use crates_io_heroku::commit; use serde::Serialize; #[derive(Debug, Serialize, utoipa::ToSchema)] @@ -34,7 +34,7 @@ pub struct MetadataResponse<'a> { pub async fn get_site_metadata(state: AppState) -> impl IntoResponse { let read_only = state.config.db.are_all_read_only(); - let deployed_sha = slug_commit().ok().flatten(); + let deployed_sha = commit().ok().flatten(); let deployed_sha = deployed_sha.as_deref().unwrap_or("unknown"); Json(MetadataResponse {