From faedf7a1da4b7a7b902d789fa462a255621b1b66 Mon Sep 17 00:00:00 2001 From: John Coffey Date: Fri, 18 Sep 2026 12:08:41 -0700 Subject: [PATCH] Versioning: INBUXA's own dated version, with the Stalwart base shown inbuxa --version, the banner, startup events, OpenTelemetry and the JMAP implementation string read "2026.9.18 (Stalwart 0.16.22)". The base comes from Cargo, which keeps following upstream so version bumps merge cleanly. Received headers and IMAP ID carry the INBUXA version. --- crates/common/src/config/telemetry.rs | 4 +-- crates/common/src/lib.rs | 2 +- crates/common/src/manager/boot.rs | 12 ++++----- crates/common/src/manager/console.rs | 4 +-- crates/common/src/telemetry/tracers/otel.rs | 4 +-- crates/imap/src/op/capability.rs | 4 ++- crates/jmap-proto/src/request/capability.rs | 2 +- crates/types/src/branding.rs | 22 +++++++++++++++ docs/spec/SPEC.md | 30 +++++++++++++++++++++ tests/src/jmap/principal/get.rs | 2 +- 10 files changed, 70 insertions(+), 16 deletions(-) diff --git a/crates/common/src/config/telemetry.rs b/crates/common/src/config/telemetry.rs index d3fa7aa..8cedbcd 100644 --- a/crates/common/src/config/telemetry.rs +++ b/crates/common/src/config/telemetry.rs @@ -543,10 +543,10 @@ impl Metrics { let metrics = bp.setting_infallible::().await; let resource = Resource::builder() .with_service_name("stalwart") - .with_attribute(KeyValue::new(SERVICE_VERSION, env!("CARGO_PKG_VERSION"))) + .with_attribute(KeyValue::new(SERVICE_VERSION, types::brand_version_full!())) .build(); let instrumentation = InstrumentationScope::builder("stalwart") - .with_version(env!("CARGO_PKG_VERSION")) + .with_version(types::brand_version_full!()) .build(); Metrics { diff --git a/crates/common/src/lib.rs b/crates/common/src/lib.rs index a29b28c..f6061ce 100644 --- a/crates/common/src/lib.rs +++ b/crates/common/src/lib.rs @@ -79,7 +79,7 @@ pub static VERSION_PRIVATE: &str = env!("CARGO_PKG_VERSION"); pub static VERSION_PUBLIC: &str = "1.0.0"; pub static USER_AGENT: &str = concat!(types::brand!(), "/1.0.0"); -pub static DAEMON_NAME: &str = concat!(types::brand!(), " v", env!("CARGO_PKG_VERSION"),); +pub static DAEMON_NAME: &str = concat!(types::brand!(), " v", types::brand_version!(),); pub static PROD_ID: &str = types::brand_prodid!(); /* diff --git a/crates/common/src/manager/boot.rs b/crates/common/src/manager/boot.rs index 1d99026..5f1f5de 100644 --- a/crates/common/src/manager/boot.rs +++ b/crates/common/src/manager/boot.rs @@ -39,8 +39,8 @@ pub struct IpcReceivers { const HELP: &str = concat!( types::brand_server!(), - " v", - env!("CARGO_PKG_VERSION"), + " ", + types::brand_version_full!(), r#" Usage: inbuxa [OPTIONS] @@ -88,7 +88,7 @@ impl BootManager { std::process::exit(0); } ("version" | "V", _) => { - println!("{}", env!("CARGO_PKG_VERSION")); + println!("{}", types::brand_version_full!()); std::process::exit(0); } ("config" | "c", Some(value)) => { @@ -166,20 +166,20 @@ impl BootManager { Hostname = bootstrap.registry.local_hostname().to_string(), Details = "No configuration file was found. Port 8080 is open for initial setup.", - Version = env!("CARGO_PKG_VERSION"), + Version = types::brand_version_full!(), ); } else if bootstrap.registry.is_recovery_mode() { trc::event!( Server(trc::ServerEvent::RecoveryMode), Details = "Port 8080 is open for troubleshooting and recovery.", Hostname = bootstrap.registry.local_hostname().to_string(), - Version = env!("CARGO_PKG_VERSION"), + Version = types::brand_version_full!(), ); } else { trc::event!( Server(trc::ServerEvent::Startup), Hostname = bootstrap.registry.local_hostname().to_string(), - Version = env!("CARGO_PKG_VERSION"), + Version = types::brand_version_full!(), ); } diff --git a/crates/common/src/manager/console.rs b/crates/common/src/manager/console.rs index 4dd1fd8..0058a3b 100644 --- a/crates/common/src/manager/console.rs +++ b/crates/common/src/manager/console.rs @@ -13,8 +13,8 @@ use store::{Deserialize, IterateParams, SUBSPACE_INDEXES, SUBSPACE_REGISTRY_IDX, const HELP: &str = concat!( types::brand_server!(), - " v", - env!("CARGO_PKG_VERSION"), + " ", + types::brand_version_full!(), r#" Data Store CLI Enter commands (type 'help' for available commands). diff --git a/crates/common/src/telemetry/tracers/otel.rs b/crates/common/src/telemetry/tracers/otel.rs index 79903c8..f84359b 100644 --- a/crates/common/src/telemetry/tracers/otel.rs +++ b/crates/common/src/telemetry/tracers/otel.rs @@ -28,11 +28,11 @@ pub(crate) fn spawn_otel_tracer(builder: SubscriberBuilder, mut otel: OtelTracer tokio::spawn(async move { let resource = Resource::builder() .with_service_name("stalwart") - .with_attribute(KeyValue::new(SERVICE_VERSION, env!("CARGO_PKG_VERSION"))) + .with_attribute(KeyValue::new(SERVICE_VERSION, types::brand_version_full!())) .build(); let instrumentation = InstrumentationScope::builder("stalwart") - .with_version(env!("CARGO_PKG_VERSION")) + .with_version(types::brand_version_full!()) .build(); otel.log_exporter.set_resource(&resource); diff --git a/crates/imap/src/op/capability.rs b/crates/imap/src/op/capability.rs index e1ac102..de79b50 100644 --- a/crates/imap/src/op/capability.rs +++ b/crates/imap/src/op/capability.rs @@ -70,7 +70,9 @@ impl Session { concat!( "* ID (\"name\" \"", types::brand!(), - "\" \"version\" \"1.0.0\" \"vendor\" \"", + "\" \"version\" \"", + types::brand_version!(), + "\" \"vendor\" \"", types::brand!(), "\" \"support-url\" \"", types::brand_url!(), diff --git a/crates/jmap-proto/src/request/capability.rs b/crates/jmap-proto/src/request/capability.rs index 39a9f8d..22124a6 100644 --- a/crates/jmap-proto/src/request/capability.rs +++ b/crates/jmap-proto/src/request/capability.rs @@ -407,7 +407,7 @@ impl Session { impl Default for SieveSessionCapabilities { fn default() -> Self { Self { - implementation: concat!(types::brand!(), " v1.0.0"), + implementation: concat!(types::brand!(), " ", types::brand_version_full!()), } } } diff --git a/crates/types/src/branding.rs b/crates/types/src/branding.rs index ecd896f..427d578 100644 --- a/crates/types/src/branding.rs +++ b/crates/types/src/branding.rs @@ -64,3 +64,25 @@ pub fn env_var(name: &str) -> Result { found => found, } } + +/// INBUXA's own version, dated like the rest of its family: `YYYY.M.D`, with +/// a letter or `.N` suffix for a second release on one day. It's set here and +/// not in Cargo.toml, so upstream's version bumps merge without conflicts. +#[macro_export] +macro_rules! brand_version { + () => { + "2026.9.18" + }; +} + +/// The version with the Stalwart release it's built on, e.g. +/// `2026.9.18 (Stalwart 0.16.22)`. The base comes from Cargo, which follows +/// upstream, so it's always the base actually compiled in. It matters because +/// Stalwart's data upgrades are one-way. Once INBUXA stops tracking upstream, +/// this becomes just the version. +#[macro_export] +macro_rules! brand_version_full { + () => { + concat!($crate::brand_version!(), " (Stalwart ", env!("CARGO_PKG_VERSION"), ")") + }; +} diff --git a/docs/spec/SPEC.md b/docs/spec/SPEC.md index ae0ccec..1a6b5f4 100644 --- a/docs/spec/SPEC.md +++ b/docs/spec/SPEC.md @@ -209,6 +209,36 @@ Done 2026-09-18: - `install.sh` is a stub that says there's no release yet. Upstream's version would download and install Stalwart itself. +### 2.6 Versioning + +Decided 2026-09-18. + +- **INBUXA has its own version,** dated like the rest of its family + (ihasmail, ihasmail-oneshot, stalwart-migrator): `YYYY.M.D`, tagged + `vYYYY.M.D`, with a suffix for a second release on one day. The server's is + `types::brand_version!()`, and INBUXA Admin has its own. +- **The Stalwart base stays visible** while INBUXA tracks upstream: + `inbuxa --version`, the startup banner and events, OpenTelemetry's + `service.version`, and the JMAP `implementation` string all read + `2026.9.18 (Stalwart 0.16.22)`. It matters because Stalwart's data upgrades + are one-way, so anyone upgrading needs to know which base their data will be + converted to. Release notes and the strip report say it too. +- **Cargo versions follow upstream, untouched.** Every upstream release bumps + all ~30 manifests, so overriding them would conflict on every sync. The base + shown is read from Cargo, so it's always what was actually compiled in. + Upstream's internal data-format version (`types/src/semver.rs`), which drives + store migrations, is upstream's and is never changed. +- `DAEMON_NAME` (the `Received:` header) and the IMAP `ID` response carry the + INBUXA version alone. `VERSION_PUBLIC`'s deliberately vague `1.0.0` for Sieve + is left as upstream has it. +- **Stalwart 1.0** arrives like any release (strip, merge, release under the + next date), but as its own milestone: a merge dry run, a strip-report + comparison against 0.16, and an upgrade test on a copy of real data, because + it will almost certainly bring a one-way store migration. +- **Diverging from upstream** is expected eventually, though not soon. When it + happens, the base drops out of the version string and nothing else about + versioning changes. + ## 3. Clean room INBUXA runs on a paid Stalwart Enterprise license, so its maintainer is a diff --git a/tests/src/jmap/principal/get.rs b/tests/src/jmap/principal/get.rs index 3be7f5d..0c786f6 100644 --- a/tests/src/jmap/principal/get.rs +++ b/tests/src/jmap/principal/get.rs @@ -54,7 +54,7 @@ pub async fn test(test: &TestServer) { "urn:ietf:params:jmap:submission": {}, "urn:ietf:params:jmap:vacationresponse": {}, "urn:ietf:params:jmap:sieve": { - "implementation": "INBUXA v1.0.0" + "implementation": concat!(types::brand!(), " ", types::brand_version_full!()) }, "urn:ietf:params:jmap:blob": {}, "urn:ietf:params:jmap:quota": {},