From 83ec070475a2568a652637e30cb41d5373c4235d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leo=20Bl=C3=B6cher?= Date: Mon, 27 Apr 2026 17:49:30 +0100 Subject: [PATCH 1/3] Extract sentry hook into separate foundations-sentry crate Sentry's Rust client regularly publishes new major versions. They are on version `0.47` now. Any `sentry` upgrade requires a `foundations` major release as well, because we depend on sentry's public API. By extracting the `sentry` feature from `foundations`, we can limit these major releases to the new crate. The new crate will use the latest sentry version from the start. We keep the old code around for compatibility, but mark it as deprecated. It will be removed in the next major release. There are some alternatives: - Allowing multiple major versions in our `Cargo.toml`. This is annoying because Cargo defaults to the latest version, causing duplicate dependencies and version mismatch errors. - Having separate features per `sentry` major release. This would mean we need to maintain (or at least keep around) multiple versions of the same code, when really we just want people to upgrade to the latest sentry release. --- Cargo.toml | 15 +- examples/Cargo.toml | 2 +- foundations-sentry/Cargo.toml | 22 +++ foundations-sentry/src/hook.rs | 106 ++++++++++++++ foundations-sentry/src/lib.rs | 43 ++++++ foundations-sentry/src/metrics.rs | 11 ++ foundations-sentry/src/settings.rs | 12 ++ foundations-sentry/tests/sentry_hook.rs | 137 ++++++++++++++++++ foundations/Cargo.toml | 9 +- foundations/src/lib.rs | 2 + foundations/src/sentry/mod.rs | 3 +- foundations/tests/sentry_hook.rs | 176 ------------------------ 12 files changed, 347 insertions(+), 191 deletions(-) create mode 100644 foundations-sentry/Cargo.toml create mode 100644 foundations-sentry/src/hook.rs create mode 100644 foundations-sentry/src/lib.rs create mode 100644 foundations-sentry/src/metrics.rs create mode 100644 foundations-sentry/src/settings.rs create mode 100644 foundations-sentry/tests/sentry_hook.rs delete mode 100644 foundations/tests/sentry_hook.rs diff --git a/Cargo.toml b/Cargo.toml index dbb00f7e..06428d6a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -2,6 +2,7 @@ members = [ "foundations", "foundations-macros", + "foundations-sentry", "examples", "tools/gen-syscall-enum", ] @@ -38,7 +39,7 @@ check-cfg = [ [workspace.dependencies] anyhow = "1.0.102" backtrace = "0.3.76" -foundations = { version = "5.6.5", path = "./foundations" } +foundations = { version = "5.6.5", path = "./foundations", default-features = false } foundations-macros = { version = "=5.6.5", path = "./foundations-macros", default-features = false } bindgen = { version = "0.72.1", default-features = false } cc = "1.2.61" @@ -95,14 +96,6 @@ tikv-jemalloc-ctl = "0.6.1" tower-service = "0.3.3" tracing-slog = "0.4.0" tracing-subscriber = "0.3.23" -sentry-core = { version = "0.36.0", default-features = false } -sentry = { version = "0.36.0", default-features = false, features = [ - "backtrace", - "contexts", - "panic", - "ureq", - "rustls", -] } zeroize = "1.8.2" # needed for minver @@ -121,3 +114,7 @@ neli-proc-macros = "0.2.2" parking_lot_core = "0.9.12" thiserror = "2.0.18" tower = "0.5.3" + +# Only used in deprecated `foundations` code +# TODO: remove before next major release +sentry-core = { version = "0.36.0", default-features = false } diff --git a/examples/Cargo.toml b/examples/Cargo.toml index e4963a02..58b09585 100644 --- a/examples/Cargo.toml +++ b/examples/Cargo.toml @@ -7,7 +7,7 @@ publish = false [dev-dependencies] anyhow = { workspace = true } -foundations = { workspace = true } +foundations = { workspace = true, features = ["default"] } futures-util = { workspace = true } http-body-util = { workspace = true } hyper = { workspace = true } diff --git a/foundations-sentry/Cargo.toml b/foundations-sentry/Cargo.toml new file mode 100644 index 00000000..5701eb50 --- /dev/null +++ b/foundations-sentry/Cargo.toml @@ -0,0 +1,22 @@ +[package] +name = "foundations-sentry" +description = "Foundations companion crate with Sentry integrations" +version = "1.0.0" +edition = { workspace = true } +repository = { workspace = true } +authors = { workspace = true } +license = { workspace = true } +readme = "../README.md" +categories = ["development-tools"] +keywords = ["foundations", "sentry", "telemetry", "metrics"] + +[lints] +workspace = true + +[dependencies] +foundations = { workspace = true, features = ["settings", "ratelimit", "metrics"] } +governor = { workspace = true } +sentry-core = "0.47.0" + +[dev-dependencies] +sentry-core = { version = "0.47.0", features = ["test"] } diff --git a/foundations-sentry/src/hook.rs b/foundations-sentry/src/hook.rs new file mode 100644 index 00000000..d3af9fa4 --- /dev/null +++ b/foundations-sentry/src/hook.rs @@ -0,0 +1,106 @@ +//! Sentry hook implementation for tracking sentry events and rate-limiting them. + +use crate::SentrySettings; +use foundations::ratelimit::StaticQuantaClock; +use governor::{Quota, RateLimiter}; +use std::borrow::Cow; +use std::num::NonZeroU32; +use std::sync::Arc; + +type Fingerprint = Cow<'static, str>; + +/// Clean up keys in the sentry rate limiter once every 20 minutes +/// (3 times per hour), with no burst allowed. +const SENTRY_LIMITER_CLEANUP_QUOTA: Quota = + Quota::per_hour(NonZeroU32::new(3).unwrap()).allow_burst(NonZeroU32::new(1).unwrap()); + +/// Install the sentry hook on the provided client options. +/// +/// This installs a `before_send` hook that increments `sentry_events_total` +/// and performs rate limiting, if configured. If a previous `before_send` +/// hook exists, it will be called after rate limiting has been applied. +/// Only unfiltered events are counted. +/// +/// See the [`foundations-sentry`](crate) crate-level docs for more information. +pub fn install_hook_with_settings( + options: &mut sentry_core::ClientOptions, + settings: &SentrySettings, +) { + let rate_limiter = settings.max_events_per_second.map(|rl| { + RateLimiter::dashmap_with_clock(Quota::per_second(rl), StaticQuantaClock::default()) + }); + + let previous = options.before_send.take(); + + options.before_send = Some(Arc::new(move |mut event| { + if let Some(limiter) = &rate_limiter { + foundations::ratelimit!(SENTRY_LIMITER_CLEANUP_QUOTA; limiter.retain_recent()); + + let fp = extract_fingerprint(&event); + if limiter.check_key(&fp).is_err() { + return None; + } + } + + if let Some(prev) = &previous { + event = prev(event)?; + } + + super::metrics::sentry::events_total(event.level).inc(); + + Some(event) + })); +} + +/// Derive a fingerprint for a sentry event to perform rate limiting. +/// +/// We check for the following event attributes, in order: +/// 1. Explicit fingerprint (if set and not defaulted) +/// 2. Event message +/// 3. First exception value/type +/// 4. Fallback: event level name +fn extract_fingerprint(event: &sentry_core::protocol::Event<'static>) -> Fingerprint { + use sentry_core::Level; + + // Try the explicitly-specified fingerprint first, but only if its not defaulted + let explicit_fp = &event.fingerprint; + if !explicit_fp.is_empty() && !is_sentry_default_fingerprint(explicit_fp) { + if let [fp] = explicit_fp.as_ref() { + // Just clone if the explicit fingerprint is a single element + return fp.clone(); + } + return explicit_fp.join("::").into(); + } + + // Try the event message, if there is one + if let Some(msg) = &event.message { + return msg.clone().into(); + } + + // Try the first attached exception, if there is one + if let Some(exc) = event.exception.first() { + if let Some(val) = &exc.value { + return val.clone().into(); + } + if !exc.ty.is_empty() { + return exc.ty.clone().into(); + } + } + + // Finally, fall back to the event level + Cow::Borrowed(match event.level { + Level::Debug => "level::debug", + Level::Info => "level::info", + Level::Warning => "level::warning", + Level::Error => "level::error", + Level::Fatal => "level::fatal", + }) +} + +// Adapted from https://github.com/getsentry/sentry-rust/blob/0.47.0/sentry-types/src/protocol/v7.rs#L1619 +fn is_sentry_default_fingerprint(fp: &[Cow<'_, str>]) -> bool { + if let [fp] = fp { + return matches!(fp.as_ref(), "{{ default }}" | "{{default}}"); + } + false +} diff --git a/foundations-sentry/src/lib.rs b/foundations-sentry/src/lib.rs new file mode 100644 index 00000000..d0301a23 --- /dev/null +++ b/foundations-sentry/src/lib.rs @@ -0,0 +1,43 @@ +#![allow(clippy::needless_doctest_main, reason = "for illustration")] +//! [`foundations`] companion crate with Sentry integrations. Includes rate limiting +//! of sentry events and tracking them with [`foundations`] metrics. +//! +//! This crate provides a sentry hook that increments the +//! `sentry_events_total{level=<...>}` metric for each sentry event. If a +//! previous `before_send` hook exists, it will be executed after rate limiting +//! and before the metric is incremented. Only unfiltered events are counted. +//! +//! For rate-limiting, we group events by fingerprint. Each group has a separate +//! rate limiter. The fingerprint of a sentry event is the first out of the following +//! attributes that is present and not defaulted: +//! +//! 1. Explicit `event.fingerprint` +//! 2. Event message +//! 3. First exception value, or exception type if no value is set +//! 4. Fallback: event level name (e.g., `error`) +//! +//! **note**: a clone of a client's [`sentry_core::ClientOptions`] will have the +//! hook installed. This means "child" sentry clients will inherit the hook. A +//! reinstall is only required if the [`sentry_core::ClientOptions::before_send`] +//! field is overwritten. +//! +//! # Usage +//! +//! To install the hook: +//! +//! ```rust +//! fn main() { +//! let mut client_opts = sentry_core::ClientOptions::default(); +//! let sentry_settings = foundations_sentry::SentrySettings::default(); +//! foundations_sentry::install_hook_with_settings(&mut client_opts, &sentry_settings); +//! // sentry::init(client_opts); +//! } +//! ``` + +pub mod metrics; + +mod hook; +mod settings; + +pub use self::hook::install_hook_with_settings; +pub use self::settings::SentrySettings; diff --git a/foundations-sentry/src/metrics.rs b/foundations-sentry/src/metrics.rs new file mode 100644 index 00000000..bffda47e --- /dev/null +++ b/foundations-sentry/src/metrics.rs @@ -0,0 +1,11 @@ +//! Sentry event related metrics. + +use foundations::telemetry::metrics::{Counter, metrics}; +use sentry_core::Level; + +/// Sentry metrics. +#[metrics(unprefixed)] +pub mod sentry { + /// Total number of sentry events observed. + pub fn events_total(level: Level) -> Counter; +} diff --git a/foundations-sentry/src/settings.rs b/foundations-sentry/src/settings.rs new file mode 100644 index 00000000..54eec57d --- /dev/null +++ b/foundations-sentry/src/settings.rs @@ -0,0 +1,12 @@ +use foundations::settings::settings; +use std::num::NonZeroU32; + +/// Sentry hook settings. +#[settings] +pub struct SentrySettings { + /// Maximum number of events that can be emitted per second, per fingerprint. + pub max_events_per_second: Option, + // In the future, we may offer different fingerprinting modes here. + // For example, we could offer a "dummy" mode that assigns the same fingerprint + // to all events so they all share a single rate limit. +} diff --git a/foundations-sentry/tests/sentry_hook.rs b/foundations-sentry/tests/sentry_hook.rs new file mode 100644 index 00000000..fd6539e2 --- /dev/null +++ b/foundations-sentry/tests/sentry_hook.rs @@ -0,0 +1,137 @@ +//! These tests assume a separate process is used. Make sure you run with `cargo +//! nextest run`. + +use sentry_core::{ClientOptions, Hub, Level}; +use std::num::NonZeroU32; +use std::sync::Arc; +use std::sync::atomic::{AtomicU64, Ordering}; + +use foundations_sentry::{SentrySettings, metrics}; + +const TEST_DSN: &str = "https://example@sentry.io/123"; + +fn simulate_sentry_event(hub: &Hub) { + hub.capture_message("test event", Level::Error); +} + +fn hub_with_settings(settings: &SentrySettings) -> Hub { + let mut options = ClientOptions::default(); + foundations_sentry::install_hook_with_settings(&mut options, settings); + hub_with_options(options) +} + +fn hub_with_options(mut options: ClientOptions) -> Hub { + options.dsn = Some(TEST_DSN.parse().unwrap()); + options.transport = Some(Arc::new(sentry_core::test::TestTransport::new())); + + let client = sentry_core::Client::with_options(options); + Hub::new(Some(Arc::new(client)), Default::default()) +} + +#[test] +fn sentry_hook_increments_metric_on_event() { + let hub = hub_with_settings(&Default::default()); + simulate_sentry_event(&hub); + assert_eq!(metrics::sentry::events_total(Level::Error).get(), 1); + + let metrics = foundations::telemetry::metrics::collect(&Default::default()).unwrap(); + let has_metric = metrics + .lines() + .any(|line| line == "sentry_events_total{level=\"error\"} 1"); + assert!(has_metric); +} + +#[test] +fn sentry_hook_increments_metric_on_multiple_events() { + let hub = hub_with_settings(&Default::default()); + + simulate_sentry_event(&hub); + simulate_sentry_event(&hub); + simulate_sentry_event(&hub); + + assert_eq!(metrics::sentry::events_total(Level::Error).get(), 3); +} + +#[test] +fn sentry_hook_rate_limits_events() { + let settings = SentrySettings { + max_events_per_second: Some(NonZeroU32::new(1).unwrap()), + }; + let hub = hub_with_settings(&settings); + + for _ in 0..3 { + simulate_sentry_event(&hub); + } + + let num_events = metrics::sentry::events_total(Level::Error).get(); + assert!(num_events >= 1); + assert!(num_events < 3); +} + +#[test] +fn sentry_hook_preserves_previous_before_send_hook() { + let previous_hook_count = Arc::new(AtomicU64::new(0)); + let counter = Arc::clone(&previous_hook_count); + + let mut options = ClientOptions { + // Install a custom before_send hook first + before_send: Some(Arc::new(move |event| { + counter.fetch_add(1, Ordering::Relaxed); + Some(event) + })), + ..Default::default() + }; + + // Now install foundations hook + foundations_sentry::install_hook_with_settings(&mut options, &Default::default()); + + let hub = hub_with_options(options); + + simulate_sentry_event(&hub); + simulate_sentry_event(&hub); + + // Both hooks should have been called + assert_eq!(previous_hook_count.load(Ordering::Relaxed), 2); + assert_eq!(metrics::sentry::events_total(Level::Error).get(), 2); +} + +#[test] +fn sentry_hook_works_across_threads() { + let hub = Arc::new(hub_with_settings(&Default::default())); + + // Simulate events from multiple threads + let handles: Vec<_> = (0..2) + .map(|_| { + let hub = Arc::clone(&hub); + std::thread::spawn(move || simulate_sentry_event(&hub)) + }) + .collect(); + + simulate_sentry_event(&hub); + + for h in handles { + h.join().unwrap(); + } + + assert_eq!(metrics::sentry::events_total(Level::Error).get(), 3); +} + +#[test] +fn cloned_client_options_have_hook_installed() { + // Initialize the first hub + let hub1 = hub_with_settings(&Default::default()); + + // Get the first hub's client and clone its options + let client1 = hub1.client().expect("client should be bound"); + let cloned_options = client1.options().clone(); + + // Create a second hub from the cloned options + let new_client = Arc::new(sentry_core::Client::with_options(cloned_options)); + let hub2 = Hub::new(Some(new_client), Default::default()); + + // Capture an event with the second hub/client + simulate_sentry_event(&hub2); + + // The hook should have been called via the cloned options + assert_eq!(metrics::sentry::events_total(Level::Error).get(), 1); +} diff --git a/foundations/Cargo.toml b/foundations/Cargo.toml index 43c8d069..9d9ffd02 100644 --- a/foundations/Cargo.toml +++ b/foundations/Cargo.toml @@ -45,7 +45,8 @@ platform-common-default = [ "sentry", ] -# Sentry integration for fatal error tracking +# DEPRECATED: Sentry integration for fatal error tracking. +# Use the separate `foundations-sentry` crate instead. sentry = ["dep:sentry-core", "dep:serde", "dep:governor", "ratelimit", "metrics"] # A subset of features that can be used both on server and client sides. Useful for libraries @@ -251,7 +252,6 @@ tikv-jemallocator = { workspace = true, optional = true, features = [ "stats", "background_threads", ] } -sentry-core = { workspace = true, optional = true } pin-project-lite = { workspace = true } zeroize = { workspace = true, optional = true } @@ -268,6 +268,10 @@ regex = { workspace = true, optional = true } thiserror = { workspace = true, optional = true } tower = { workspace = true, optional = true } +# Only used in deprecated code +# TODO: remove before next major release +sentry-core = { workspace = true, optional = true } + [target.'cfg(target_os = "linux")'.dependencies] tikv-jemalloc-ctl = { workspace = true, optional = true, features = [ "use_std", @@ -287,7 +291,6 @@ tokio = { workspace = true, features = ["macros", "rt-multi-thread"] } ipnetwork = { workspace = true } nix = { workspace = true , features = ["fs"] } tracing-subscriber = { workspace = true } -sentry = { workspace = true } [build-dependencies] bindgen = { workspace = true, features = ["runtime"], optional = true } diff --git a/foundations/src/lib.rs b/foundations/src/lib.rs index 74414dcc..1174958e 100644 --- a/foundations/src/lib.rs +++ b/foundations/src/lib.rs @@ -76,6 +76,8 @@ pub mod addr; pub mod panic; #[cfg(all(feature = "sentry", feature = "metrics"))] +#[deprecated = "Replaced by separate `foundations-sentry` crate."] +// TODO: remove before next major release pub mod sentry; #[cfg(feature = "cli")] diff --git a/foundations/src/sentry/mod.rs b/foundations/src/sentry/mod.rs index 25ad00c9..25fee616 100644 --- a/foundations/src/sentry/mod.rs +++ b/foundations/src/sentry/mod.rs @@ -1,4 +1,4 @@ -#![allow(clippy::needless_doctest_main)] +#![allow(deprecated, clippy::needless_doctest_main)] //! Sentry hook for tracking sentry events with metrics and rate-limiting them. //! //! This module provides a sentry hook that increments the @@ -38,7 +38,6 @@ pub mod metrics; mod hook; mod settings; -#[allow(deprecated)] pub use self::hook::install_hook; pub use self::hook::install_hook_with_settings; pub use self::settings::SentrySettings; diff --git a/foundations/tests/sentry_hook.rs b/foundations/tests/sentry_hook.rs deleted file mode 100644 index ba6037ef..00000000 --- a/foundations/tests/sentry_hook.rs +++ /dev/null @@ -1,176 +0,0 @@ -#![cfg(feature = "sentry")] -//! These tests assume a separate process is used. Make sure you run with `cargo -//! nextest run`. - -use std::num::NonZeroU32; -use std::sync::Arc; -use std::sync::atomic::{AtomicU64, Ordering}; - -use foundations::sentry::{Level, SentrySettings, metrics}; - -const TEST_DSN: &str = "https://example@sentry.io/123"; - -fn simulate_sentry_event() { - sentry::capture_message("test event", sentry::Level::Error); -} - -#[test] -fn sentry_hook_increments_metric_on_event() { - let mut options = sentry::ClientOptions::default(); - foundations::sentry::install_hook_with_settings(&mut options, &Default::default()); - - let _guard = sentry::init((TEST_DSN, options)); - - simulate_sentry_event(); - assert_eq!(metrics::sentry::events_total(Level::Error).get(), 1); -} - -#[test] -fn sentry_hook_metrics_are_well_formed() { - let mut options = sentry::ClientOptions::default(); - foundations::sentry::install_hook_with_settings(&mut options, &Default::default()); - - let _guard = sentry::init((TEST_DSN, options)); - - simulate_sentry_event(); - assert_eq!(metrics::sentry::events_total(Level::Error).get(), 1); - - let metrics = foundations::telemetry::metrics::collect(&Default::default()).unwrap(); - let has_metric = metrics - .lines() - .any(|line| line == "sentry_events_total{level=\"error\"} 1"); - assert!(has_metric); -} - -#[test] -fn sentry_hook_increments_metric_on_multiple_events() { - let mut options = sentry::ClientOptions::default(); - foundations::sentry::install_hook_with_settings(&mut options, &Default::default()); - - let _guard = sentry::init((TEST_DSN, options)); - - simulate_sentry_event(); - simulate_sentry_event(); - simulate_sentry_event(); - - assert_eq!(metrics::sentry::events_total(Level::Error).get(), 3); -} - -#[test] -fn sentry_hook_rate_limits_events() { - let mut options = sentry::ClientOptions::default(); - let settings = SentrySettings { - max_events_per_second: Some(NonZeroU32::new(1).unwrap()), - }; - foundations::sentry::install_hook_with_settings(&mut options, &settings); - - let _guard = sentry::init((TEST_DSN, options)); - - for _ in 0..3 { - simulate_sentry_event(); - } - - let num_events = metrics::sentry::events_total(Level::Error).get(); - assert!(num_events >= 1); - assert!(num_events < 3); -} - -#[test] -fn sentry_hook_preserves_previous_before_send_hook() { - let previous_hook_count = Arc::new(AtomicU64::new(0)); - let counter = Arc::clone(&previous_hook_count); - - let mut options = sentry::ClientOptions { - // Install a custom before_send hook first - before_send: Some(Arc::new(move |event| { - counter.fetch_add(1, Ordering::Relaxed); - Some(event) - })), - ..Default::default() - }; - - // Now install foundations hook - foundations::sentry::install_hook_with_settings(&mut options, &Default::default()); - - let _guard = sentry::init((TEST_DSN, options)); - - simulate_sentry_event(); - simulate_sentry_event(); - - // Both hooks should have been called - assert_eq!(previous_hook_count.load(Ordering::Relaxed), 2); - assert_eq!(metrics::sentry::events_total(Level::Error).get(), 2); -} - -#[test] -fn sentry_hook_works_across_threads() { - let mut options = sentry::ClientOptions::default(); - foundations::sentry::install_hook_with_settings(&mut options, &Default::default()); - - let _guard = sentry::init((TEST_DSN, options)); - - // Simulate events from multiple threads - simulate_sentry_event(); - - let handle1 = std::thread::spawn(simulate_sentry_event); - let handle2 = std::thread::spawn(simulate_sentry_event); - - handle1.join().unwrap(); - handle2.join().unwrap(); - - assert_eq!(metrics::sentry::events_total(Level::Error).get(), 3); -} - -#[test] -fn sentry_hook_works_in_tokio_tasks() { - let mut options = sentry::ClientOptions::default(); - foundations::sentry::install_hook_with_settings(&mut options, &Default::default()); - - let _guard = sentry::init((TEST_DSN, options)); - - // Event before tokio runtime - simulate_sentry_event(); - - let rt = tokio::runtime::Builder::new_multi_thread().build().unwrap(); - - let handle1 = rt.spawn(async { - simulate_sentry_event(); - }); - let handle2 = rt.spawn(async { - simulate_sentry_event(); - }); - - rt.block_on(async move { - handle1.await.unwrap(); - handle2.await.unwrap(); - }); - - assert_eq!(metrics::sentry::events_total(Level::Error).get(), 3); -} - -#[test] -fn cloned_client_options_have_hook_installed() { - use sentry::{Client, Hub, Scope}; - - let mut options = sentry::ClientOptions::default(); - foundations::sentry::install_hook_with_settings(&mut options, &Default::default()); - - // Initialize the global client - let _guard = sentry::init((TEST_DSN, options)); - - // Get the global client and clone its options - let global_client = Hub::current().client().expect("client should be bound"); - let cloned_options = global_client.options().clone(); - - // Create a new client from the cloned options - let new_client = Arc::new(Client::with_options(cloned_options)); - let hub = Arc::new(Hub::new(Some(new_client), Arc::new(Scope::default()))); - - // Run with the new hub and capture an event - Hub::run(hub, || { - simulate_sentry_event(); - }); - - // The hook should have been called via the cloned options - assert_eq!(metrics::sentry::events_total(Level::Error).get(), 1); -} From 37b2f11c10b36f17d4b3572fdd090eaf4f6ff179 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leo=20Bl=C3=B6cher?= Date: Mon, 27 Apr 2026 18:46:24 +0100 Subject: [PATCH 2/3] Configure cargo-release for separately-versioned crates --- Cargo.toml | 6 +++--- cliff.toml | 2 +- examples/Cargo.toml | 3 +++ foundations-macros/Cargo.toml | 5 +++++ foundations-sentry/Cargo.toml | 16 ++++++++++++++++ foundations-sentry/RELEASE_NOTES.md | 0 foundations/Cargo.toml | 3 +++ 7 files changed, 31 insertions(+), 4 deletions(-) create mode 100644 foundations-sentry/RELEASE_NOTES.md diff --git a/Cargo.toml b/Cargo.toml index 06428d6a..52dad98a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -16,10 +16,10 @@ authors = ["Cloudflare"] license = "BSD-3-Clause" [workspace.metadata.release] -pre-release-commit-message = "Release {{version}}" -shared-version = true -tag-prefix = "" +push = false publish = false +pre-release-commit-message = "chore: Release" +tag-message = "Release {{crate_name}} version {{version}}" [profile.release] debug = 1 diff --git a/cliff.toml b/cliff.toml index fb3c241d..81a6575c 100644 --- a/cliff.toml +++ b/cliff.toml @@ -20,5 +20,5 @@ commit_parsers = [ { message = "^Pull request", skip = true } ] filter_commits = false -tag_pattern = "[v0-9]*" +tag_pattern = "v[0-9]*" sort_commits = "newest" diff --git a/examples/Cargo.toml b/examples/Cargo.toml index 58b09585..ee999dbb 100644 --- a/examples/Cargo.toml +++ b/examples/Cargo.toml @@ -5,6 +5,9 @@ edition = { workspace = true } repository = { workspace = true } publish = false +[package.metadata.release] +release = false + [dev-dependencies] anyhow = { workspace = true } foundations = { workspace = true, features = ["default"] } diff --git a/foundations-macros/Cargo.toml b/foundations-macros/Cargo.toml index bbcab061..d06f69ce 100644 --- a/foundations-macros/Cargo.toml +++ b/foundations-macros/Cargo.toml @@ -8,6 +8,11 @@ authors = { workspace = true } license = { workspace = true } readme = "../README.md" +[package.metadata.release] +shared-version = "foundations" +tag-prefix = "" +tag-message = "Release foundations version {{version}}" + [features] default = ["settings_deny_unknown_fields_by_default"] settings_deny_unknown_fields_by_default = [] diff --git a/foundations-sentry/Cargo.toml b/foundations-sentry/Cargo.toml index 5701eb50..b5d82505 100644 --- a/foundations-sentry/Cargo.toml +++ b/foundations-sentry/Cargo.toml @@ -10,6 +10,22 @@ readme = "../README.md" categories = ["development-tools"] keywords = ["foundations", "sentry", "telemetry", "metrics"] +# Generic for any separately-versioned crate at repo root +[package.metadata.release] +pre-release-hook = [ + "git-cliff", + "-w", + "..", + "--include-path", + "{{crate_name}}/**", + "--tag-pattern", + "{{crate_name}}-v[0-9]*", + "-o", + "RELEASE_NOTES.md", + "--tag", + "{{version}}", +] + [lints] workspace = true diff --git a/foundations-sentry/RELEASE_NOTES.md b/foundations-sentry/RELEASE_NOTES.md new file mode 100644 index 00000000..e69de29b diff --git a/foundations/Cargo.toml b/foundations/Cargo.toml index 9d9ffd02..731a8f15 100644 --- a/foundations/Cargo.toml +++ b/foundations/Cargo.toml @@ -17,6 +17,9 @@ categories = [ keywords = ["service", "telemetry", "settings", "seccomp", "metrics"] [package.metadata.release] +shared-version = "foundations" +tag-prefix = "" +tag-message = "Release foundations version {{version}}" # run in the context of workspace root pre-release-hook = [ "git-cliff", From 8e823787a00b4c2bbfc540eb8c6d70955220ac21 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leo=20Bl=C3=B6cher?= Date: Mon, 27 Apr 2026 19:24:31 +0100 Subject: [PATCH 3/3] chore: Release --- foundations-sentry/RELEASE_NOTES.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/foundations-sentry/RELEASE_NOTES.md b/foundations-sentry/RELEASE_NOTES.md index e69de29b..35edf71a 100644 --- a/foundations-sentry/RELEASE_NOTES.md +++ b/foundations-sentry/RELEASE_NOTES.md @@ -0,0 +1,6 @@ + +1.0.0 +- 2026-04-27 Configure cargo-release for separately-versioned crates +- 2026-04-27 Extract sentry hook into separate foundations-sentry crate + +