commit 6c27a358c36fc8027d2919a2325ce91a5d0d1791
parent cc57eabbc1e8bcd58bdd03d2a4d1ba46b4695a15
Author: triesap <tyson@radroots.org>
Date: Mon, 27 Jul 2026 13:18:02 +0000
release: close public dependency graph
- enforce the versioned strict publication policy schema
- derive every dependency edge and publish order from Cargo metadata
- publish the transport-neutral crate as the public delivery boundary
- remove public feature paths into internal adapters and stores
Diffstat:
16 files changed, 631 insertions(+), 617 deletions(-)
diff --git a/Cargo.lock b/Cargo.lock
@@ -4815,7 +4815,6 @@ version = "1.0.0-alpha.1"
dependencies = [
"radroots_identity",
"radroots_nostr_signer",
- "radroots_nostrdb",
"radroots_protected_store",
"radroots_runtime",
"radroots_secret_vault",
@@ -4984,7 +4983,6 @@ dependencies = [
"radroots_runtime_paths",
"radroots_secret_vault",
"radroots_transport",
- "radroots_transport_reticulum",
"serde",
"serde_json",
"tempfile",
@@ -5184,11 +5182,9 @@ dependencies = [
"dto_bindgen_core",
"hex",
"nostr",
- "radroots_authority",
"radroots_core",
"radroots_event",
"radroots_event_codec",
- "radroots_event_store",
"radroots_nostr",
"radroots_test_fixtures",
"radroots_transport",
diff --git a/contracts/releases/publish_policy.toml b/contracts/releases/publish_policy.toml
@@ -1,3 +1,6 @@
+[schema]
+version = 1
+
[release]
version = "1.0.0-alpha.1"
@@ -28,6 +31,7 @@ public = [
"radroots_secret_vault",
"radroots_sql_core",
"radroots_trade",
+ "radroots_transport",
]
internal = [
"radroots_authority",
@@ -51,7 +55,6 @@ internal = [
"radroots_test_fixtures",
"radroots_trade_sp1_guest",
"radroots_trade_sp1_host",
- "radroots_transport",
"radroots_transport_nostr",
"radroots_transport_publish_protocol",
"radroots_transport_reticulum",
@@ -63,6 +66,7 @@ yank_only = []
[publish_order]
crates = [
+ "radroots_transport",
"radroots_runtime_distribution",
"radroots_replica_schema",
"radroots_protocol_contract_v1",
diff --git a/crates/nostr_accounts/Cargo.toml b/crates/nostr_accounts/Cargo.toml
@@ -11,6 +11,7 @@ repository.workspace = true
homepage.workspace = true
documentation = "https://docs.rs/radroots_nostr_accounts"
readme = "README"
+exclude = ["src/nostrdb_bridge.rs"]
[features]
default = ["std", "file-store", "memory-vault"]
@@ -26,7 +27,6 @@ std = [
file-store = ["std"]
memory-vault = ["std", "radroots_secret_vault/memory-vault"]
os-keyring = ["std", "radroots_secret_vault/os-keyring"]
-nostrdb-bridge = ["std", "dep:radroots_nostrdb"]
[dependencies]
radroots_identity = { workspace = true, optional = true, default-features = false, features = [
@@ -34,11 +34,6 @@ radroots_identity = { workspace = true, optional = true, default-features = fals
"json-file",
] }
radroots_nostr_signer = { workspace = true, optional = true }
-radroots_nostrdb = { workspace = true, optional = true, default-features = false, features = [
- "nostrdb",
- "giftwrap",
- "rt",
-] }
radroots_protected_store = { workspace = true, optional = true, default-features = false, features = [
"std",
] }
diff --git a/crates/nostr_accounts/README b/crates/nostr_accounts/README
@@ -11,8 +11,7 @@ and vault-backed stores for the `radroots` core libraries.
* shared bootstrap helpers for local account stores backed by protected files
and runtime-selected secret vaults;
* vault integrations through `radroots_secret_vault`, including memory and OS
- keyring backends;
- * optional nostrdb bridge helpers for account registration and lookup.
+ keyring backends.
## Copyright
diff --git a/crates/nostr_accounts/src/lib.rs b/crates/nostr_accounts/src/lib.rs
@@ -6,8 +6,6 @@ pub mod error;
pub mod manager;
#[cfg(feature = "std")]
pub mod model;
-#[cfg(feature = "nostrdb-bridge")]
-pub mod nostrdb_bridge;
#[cfg(feature = "std")]
pub mod store;
#[cfg(feature = "std")]
@@ -22,8 +20,6 @@ pub mod prelude {
RADROOTS_NOSTR_ACCOUNTS_STORE_VERSION, RadrootsNostrAccountRecord,
RadrootsNostrAccountStatus, RadrootsNostrAccountStoreState,
};
- #[cfg(feature = "nostrdb-bridge")]
- pub use crate::nostrdb_bridge::radroots_nostr_accounts_register_default_secret_with_nostrdb;
#[cfg(feature = "std")]
pub use crate::store::{
RadrootsNostrAccountStore, RadrootsNostrFileAccountStore, RadrootsNostrMemoryAccountStore,
diff --git a/crates/outbox/contracts/migration_authority_v1.manifest.json b/crates/outbox/contracts/migration_authority_v1.manifest.json
@@ -305,10 +305,10 @@
},
{
"file": {
- "byte_length": 486359,
+ "byte_length": 495349,
"hash_algorithm": "sha256_bytes_v1",
"path": "tools/xtask/src/contract.rs",
- "sha256": "96468090685099e9d3722d45cf05170f3da83376150cbf58fee776157e6be649"
+ "sha256": "fc6c2e794ef5c787ed134f38982b1c18518cde30fb21643669428fa05a3eccd3"
},
"role": "contract_dispatch"
},
diff --git a/crates/outbox/contracts/migration_authority_v1.manifest.sha256 b/crates/outbox/contracts/migration_authority_v1.manifest.sha256
@@ -1 +1 @@
-7756d99889ee5875664d1c831c0a430d3352dac2ab958b9abf4bdb8cd5a360a4
+168348114a174c2f745ae388182d604832dee24efa2c24781d03a3a10d64462a
diff --git a/crates/outbox/contracts/phase1_publication_v1.manifest.json b/crates/outbox/contracts/phase1_publication_v1.manifest.json
@@ -112,9 +112,9 @@
},
{
"file": {
- "byte_length": 486359,
+ "byte_length": 495349,
"path": "tools/xtask/src/contract.rs",
- "sha256": "96468090685099e9d3722d45cf05170f3da83376150cbf58fee776157e6be649"
+ "sha256": "fc6c2e794ef5c787ed134f38982b1c18518cde30fb21643669428fa05a3eccd3"
},
"role": "contract_dispatch"
},
diff --git a/crates/outbox/contracts/phase1_publication_v1.manifest.sha256 b/crates/outbox/contracts/phase1_publication_v1.manifest.sha256
@@ -1 +1 @@
-b9e4b58a8745555d0bcd66128de509e788b90dcb1d49ec73b9b6fa81a0ed0307
+e8fea684b1e1f37d99e0ffbc974ec4fb1b069f5ab9b50b5c8bdafa624c1c7246
diff --git a/crates/runtime/Cargo.toml b/crates/runtime/Cargo.toml
@@ -17,7 +17,6 @@ default = []
cli = ["dep:clap"]
transport = ["dep:radroots_event", "dep:radroots_transport"]
transport-nostr = ["transport"]
-transport-reticulum = ["transport", "dep:radroots_transport_reticulum"]
transport-workers = ["transport"]
[dependencies]
@@ -38,7 +37,6 @@ radroots_secret_vault = { workspace = true, features = ["std"] }
radroots_transport = { workspace = true, optional = true, default-features = false, features = [
"serde",
] }
-radroots_transport_reticulum = { workspace = true, optional = true, default-features = false }
serde = { workspace = true }
serde_json = { workspace = true }
tempfile = { workspace = true }
diff --git a/crates/runtime/src/transport.rs b/crates/runtime/src/transport.rs
@@ -1339,10 +1339,6 @@ mod tests {
RadrootsTransportSatisfactionPolicy, RadrootsTransportStatus, RadrootsTransportTarget,
RadrootsTransportTargetReceipt, RadrootsTransportTargetSet,
};
- #[cfg(feature = "transport-reticulum")]
- use radroots_transport::{
- RadrootsTransportCapabilityAvailability, RadrootsTransportCapabilityMaturity,
- };
#[cfg(feature = "transport-workers")]
use std::sync::{Arc, Mutex};
@@ -1776,73 +1772,6 @@ mod tests {
assert_eq!(delivery_request.now_ms(), 123_456);
}
- #[cfg(feature = "transport-reticulum")]
- #[tokio::test]
- async fn registry_preserves_default_reticulum_reject_behavior() {
- let mut registry = RadrootsRuntimeTransportRegistry::new();
- registry
- .register(radroots_transport_reticulum::RadrootsReticulumTransport::default())
- .expect("register");
- let transport = registry
- .transport(&RadrootsTransportKind::Reticulum)
- .expect("reticulum transport");
- let request = RadrootsRuntimeTransportDispatchRequest::new(
- "reticulum-delivery",
- opaque_payload(),
- vec![target(RadrootsTransportKind::Reticulum, "reticulum:local")],
- RadrootsTransportSatisfactionPolicy::any_accepted(),
- 1_000,
- )
- .expect("request");
- let receipt = transport
- .deliver(
- request
- .transport_delivery_request()
- .expect("delivery request"),
- )
- .await
- .expect("receipt");
- let status = transport.status().await.expect("status");
- assert_eq!(
- status.implementation(),
- RadrootsTransportImplementationState::Real
- );
- assert_eq!(
- status.maturity(),
- RadrootsTransportCapabilityMaturity::Preview
- );
- assert_eq!(
- status.availability(),
- RadrootsTransportCapabilityAvailability::Unavailable
- );
- assert!(!status.capabilities().can_deliver());
- assert!(!status.capabilities().can_fetch());
- let fetch = transport
- .fetch(
- RadrootsTransportFetchRequest::new(
- "reticulum-fetch",
- RadrootsTransportTargetSet::new(vec![target(
- RadrootsTransportKind::Reticulum,
- "reticulum:local",
- )])
- .expect("target set"),
- )
- .expect("fetch request"),
- )
- .await
- .expect("fetch");
- assert_eq!(fetch.fetched_count(), 0);
-
- assert_eq!(
- receipt.satisfied_target_count(RadrootsTransportSatisfactionClass::Accepted),
- 0
- );
- assert_eq!(
- receipt.target_receipts()[0].status(),
- RadrootsTransportDeliveryTargetStatus::FailedRetryable
- );
- }
-
#[test]
fn bounded_queue_tracks_capacity_inflight_and_shutdown() {
let mut queue = RadrootsRuntimeBoundedQueue::new(2).expect("queue");
diff --git a/crates/trade/Cargo.toml b/crates/trade/Cargo.toml
@@ -23,21 +23,11 @@ dto-bindgen = [
"radroots_event/dto-bindgen",
]
std = [
- "radroots_authority/std",
"radroots_core/std",
"radroots_event/std",
"radroots_event_codec/std",
"radroots_event_codec/nostr",
]
-event_store = [
- "std",
- "serde_json",
- "dep:radroots_event_store",
- "dep:sqlx",
- "radroots_event_store/sqlite",
- "radroots_event_store/runtime-tokio",
- "sqlx/sqlite-bundled",
-]
serde = [
"dep:serde",
"radroots_core/serde",
@@ -54,11 +44,9 @@ serde_json = [
]
[dependencies]
-radroots_authority = { workspace = true, default-features = false }
radroots_core = { workspace = true, default-features = false }
radroots_event = { workspace = true, default-features = false }
radroots_event_codec = { workspace = true, default-features = false }
-radroots_event_store = { workspace = true, optional = true, default-features = false }
base64 = { workspace = true, optional = true }
dto_bindgen = { workspace = true, optional = true }
dto_bindgen_core = { workspace = true, optional = true }
@@ -71,9 +59,6 @@ serde_json = { workspace = true, default-features = false, features = [
"alloc",
], optional = true }
sha2 = { workspace = true, default-features = false, optional = true }
-sqlx = { workspace = true, default-features = false, features = [
- "derive",
-], optional = true }
[dev-dependencies]
nostr = { workspace = true }
diff --git a/crates/trade/src/operational_listing/draft.rs b/crates/trade/src/operational_listing/draft.rs
@@ -10,9 +10,7 @@ use alloc::{format, string::ToString, vec::Vec};
#[cfg(feature = "std")]
use std::{string::ToString, vec::Vec};
-use radroots_authority::RadrootsActorContext;
use radroots_event::{
- contract::RadrootsActorRole,
ids::{
RadrootsClassifiedListingAddress, RadrootsIdParseError, RadrootsInventoryBinId,
RadrootsPublicKey,
@@ -94,9 +92,6 @@ pub enum RadrootsOperationalListingEditError {
InvalidFarmPubkey(RadrootsIdParseError),
InvalidClassifiedListingAddress(RadrootsIdParseError),
InvalidModel(RadrootsOperationalListingValidationError),
- ActorRoleUnsatisfied {
- required_role: RadrootsActorRole,
- },
FarmPubkeyMismatch {
expected_pubkey: RadrootsPublicKey,
actual_pubkey: RadrootsPublicKey,
@@ -121,10 +116,6 @@ impl fmt::Display for RadrootsOperationalListingEditError {
Self::InvalidModel(error) => {
write!(f, "invalid listing edit model: {error}")
}
- Self::ActorRoleUnsatisfied { required_role } => write!(
- f,
- "listing edit actor does not satisfy required role {required_role:?}"
- ),
Self::FarmPubkeyMismatch { .. } => {
f.write_str("listing edit farm pubkey does not match seller")
}
@@ -172,17 +163,13 @@ fn listing_addr(
.expect("typed listing identity must form a listing address")
}
+/// Canonicalizes an operational listing edit after the caller has authorized
+/// `authorized_seller_pubkey` for the seller-owned listing boundary.
pub fn canonicalize_operational_listing_edit(
- actor: &RadrootsActorContext,
+ authorized_seller_pubkey: &RadrootsPublicKey,
mut document: RadrootsOperationalListingEditDocumentV1,
) -> Result<RadrootsOperationalListingCanonicalEdit, RadrootsOperationalListingEditError> {
- if !actor.satisfies(RadrootsActorRole::Seller) {
- return Err(RadrootsOperationalListingEditError::ActorRoleUnsatisfied {
- required_role: RadrootsActorRole::Seller,
- });
- }
-
- let seller_pubkey = actor.pubkey().clone();
+ let seller_pubkey = authorized_seller_pubkey.clone();
let farm_pubkey = document.listing.farm.pubkey.as_str();
if farm_pubkey.is_empty() {
document.listing.farm.pubkey = seller_pubkey.as_str().to_string();
@@ -193,13 +180,11 @@ pub fn canonicalize_operational_listing_edit(
#[cfg(test)]
mod tests {
- use radroots_authority::RadrootsActorContext;
use radroots_core::{
RadrootsCoreCurrency, RadrootsCoreDecimal, RadrootsCoreMoney, RadrootsCoreQuantity,
RadrootsCoreQuantityPrice, RadrootsCoreUnit,
};
use radroots_event::{
- contract::RadrootsActorRole,
farm::RadrootsFarmRef,
ids::{
RadrootsClassifiedListingAddress, RadrootsDTag, RadrootsInventoryBinId,
@@ -292,12 +277,8 @@ mod tests {
}
}
- fn seller_actor() -> RadrootsActorContext {
- RadrootsActorContext::explicit_pubkey(SELLER, [RadrootsActorRole::Seller]).expect("actor")
- }
-
- fn buyer_actor() -> RadrootsActorContext {
- RadrootsActorContext::explicit_pubkey(SELLER, [RadrootsActorRole::Buyer]).expect("actor")
+ fn seller_pubkey() -> RadrootsPublicKey {
+ RadrootsPublicKey::parse(SELLER).expect("seller")
}
#[test]
@@ -316,7 +297,7 @@ mod tests {
let document: RadrootsOperationalListingEditDocumentV1 =
serde_json::from_str(&json).expect("deserialize document");
- let canonical = canonicalize_operational_listing_edit(&seller_actor(), document)
+ let canonical = canonicalize_operational_listing_edit(&seller_pubkey(), document)
.expect("canonical draft");
assert_eq!(canonical.seller_pubkey().as_str(), SELLER);
@@ -362,7 +343,7 @@ mod tests {
listing.farm.pubkey.clear();
let document = RadrootsOperationalListingEditDocumentV1::new(listing);
- let canonical = canonicalize_operational_listing_edit(&seller_actor(), document)
+ let canonical = canonicalize_operational_listing_edit(&seller_pubkey(), document)
.expect("canonical draft");
assert_eq!(canonical.seller_pubkey().as_str(), SELLER);
@@ -374,26 +355,12 @@ mod tests {
}
#[test]
- fn canonicalize_operational_listing_edit_rejects_non_seller_actor() {
- let document = RadrootsOperationalListingEditDocumentV1::new(listing());
-
- let error = canonicalize_operational_listing_edit(&buyer_actor(), document).unwrap_err();
-
- assert_eq!(
- error,
- RadrootsOperationalListingEditError::ActorRoleUnsatisfied {
- required_role: RadrootsActorRole::Seller
- }
- );
- }
-
- #[test]
fn canonicalize_operational_listing_edit_rejects_mismatched_farm_pubkey() {
let mut listing = listing();
listing.farm.pubkey = OTHER.to_string();
let document = RadrootsOperationalListingEditDocumentV1::new(listing);
- let error = canonicalize_operational_listing_edit(&seller_actor(), document).unwrap_err();
+ let error = canonicalize_operational_listing_edit(&seller_pubkey(), document).unwrap_err();
assert!(matches!(
error,
@@ -407,7 +374,7 @@ mod tests {
listing.farm.pubkey = "bad".to_string();
let document = RadrootsOperationalListingEditDocumentV1::new(listing);
- let error = canonicalize_operational_listing_edit(&seller_actor(), document).unwrap_err();
+ let error = canonicalize_operational_listing_edit(&seller_pubkey(), document).unwrap_err();
assert!(matches!(
error,
@@ -472,7 +439,7 @@ mod tests {
listing.primary_bin_id = bin_id("bin-2");
let document = RadrootsOperationalListingEditDocumentV1::new(listing);
- let error = canonicalize_operational_listing_edit(&seller_actor(), document).unwrap_err();
+ let error = canonicalize_operational_listing_edit(&seller_pubkey(), document).unwrap_err();
assert_eq!(
error,
@@ -507,7 +474,7 @@ mod tests {
listing.bins.push(listing.bins[0].clone());
let document = RadrootsOperationalListingEditDocumentV1::new(listing);
- let error = canonicalize_operational_listing_edit(&seller_actor(), document).unwrap_err();
+ let error = canonicalize_operational_listing_edit(&seller_pubkey(), document).unwrap_err();
assert_eq!(
error,
diff --git a/crates/transport/Cargo.toml b/crates/transport/Cargo.toml
@@ -1,6 +1,6 @@
[package]
name = "radroots_transport"
-publish = false
+publish = ["crates-io"]
version = "1.0.0-alpha.1"
edition.workspace = true
authors = ["Tyson Lupul <tyson@radroots.org>"]
diff --git a/tools/xtask/src/contract.rs b/tools/xtask/src/contract.rs
@@ -79,6 +79,8 @@ use sha2::{Digest, Sha256};
use std::collections::{BTreeMap, BTreeSet};
use std::fs;
use std::path::{Path, PathBuf};
+#[cfg(not(test))]
+use std::process::Command;
pub(crate) fn validate_artifact_contracts(workspace_root: &Path) -> Result<(), String> {
validate_event_contract_registry_v7_inventory(workspace_root)?;
@@ -3211,42 +3213,76 @@ const CANONICAL_EVENT_BOUNDARY_EXPECTATIONS: [EventBoundaryExpectation; 44] = [
];
#[derive(Debug, Deserialize)]
+#[serde(deny_unknown_fields)]
struct ReleaseContractFile {
+ schema: ReleasePolicySchema,
release: ReleaseSection,
- #[serde(default)]
classification: ReleaseClassification,
- #[serde(default)]
- publish: Option<ReleaseCrateSet>,
- #[serde(default)]
- internal: Option<ReleaseCrateSet>,
publish_order: ReleaseCrateSet,
}
-#[derive(Debug, Default, Deserialize)]
+#[derive(Debug, Deserialize)]
+#[serde(deny_unknown_fields)]
+struct ReleasePolicySchema {
+ version: u32,
+}
+
+#[derive(Debug, Deserialize)]
+#[serde(deny_unknown_fields)]
struct ReleaseClassification {
- #[serde(default)]
public: Vec<String>,
- #[serde(default)]
internal: Vec<String>,
- #[serde(default)]
deferred: Vec<String>,
- #[serde(default)]
retired: Vec<String>,
- #[serde(default)]
yank_only: Vec<String>,
}
#[derive(Debug, Deserialize)]
+#[serde(deny_unknown_fields)]
struct ReleaseSection {
version: String,
}
#[derive(Debug, Deserialize)]
+#[serde(deny_unknown_fields)]
struct ReleaseCrateSet {
crates: Vec<String>,
}
#[derive(Debug, Deserialize)]
+struct CargoMetadataDocument {
+ packages: Vec<CargoMetadataPackage>,
+ workspace_members: Vec<String>,
+}
+
+#[derive(Debug, Deserialize)]
+struct CargoMetadataPackage {
+ id: String,
+ name: String,
+ publish: Option<Vec<String>>,
+ dependencies: Vec<CargoMetadataDependency>,
+}
+
+#[derive(Clone, Debug, Deserialize, PartialEq, Eq)]
+struct CargoMetadataDependency {
+ name: String,
+ source: Option<String>,
+ kind: Option<String>,
+ rename: Option<String>,
+ optional: bool,
+ target: Option<String>,
+ path: Option<PathBuf>,
+}
+
+#[derive(Debug)]
+struct ReleaseWorkspacePackage {
+ publish: Option<Vec<String>>,
+ dependencies: Vec<CargoMetadataDependency>,
+}
+
+type ReleaseWorkspacePackages = BTreeMap<String, ReleaseWorkspacePackage>;
+
+#[derive(Debug, Deserialize)]
#[serde(deny_unknown_fields)]
struct ConformanceVectorFile {
suite: String,
@@ -3265,32 +3301,12 @@ struct ConformanceVectorEntry {
}
impl ReleaseContractFile {
- fn uses_classification(&self) -> bool {
- !self.classification.public.is_empty()
- || !self.classification.internal.is_empty()
- || !self.classification.deferred.is_empty()
- || !self.classification.retired.is_empty()
- || !self.classification.yank_only.is_empty()
- }
-
fn public_crates(&self) -> Vec<String> {
- if self.uses_classification() {
- return self.classification.public.clone();
- }
- self.publish
- .as_ref()
- .map(|set| set.crates.clone())
- .unwrap_or_default()
+ self.classification.public.clone()
}
fn internal_crates(&self) -> Vec<String> {
- if self.uses_classification() {
- return self.classification.internal.clone();
- }
- self.internal
- .as_ref()
- .map(|set| set.crates.clone())
- .unwrap_or_default()
+ self.classification.internal.clone()
}
fn deferred_crates(&self) -> Vec<String> {
@@ -4805,6 +4821,7 @@ struct WorkspacePackageRecord {
#[cfg_attr(not(test), allow(dead_code))]
manifest_path: PathBuf,
publish_enabled: bool,
+ #[cfg(test)]
publish: Option<PackagePublish>,
manifest_value: toml::Value,
}
@@ -4829,12 +4846,12 @@ fn workspace_package_records(workspace_root: &Path) -> Result<Vec<WorkspacePacka
};
let name = package_manifest.package.name;
let publish_enabled = package_publish_enabled(package_manifest.package.publish.as_ref());
- let publish = package_manifest.package.publish.clone();
records.push(WorkspacePackageRecord {
name,
manifest_path,
publish_enabled,
- publish,
+ #[cfg(test)]
+ publish: package_manifest.package.publish.clone(),
manifest_value,
});
}
@@ -4992,6 +5009,7 @@ fn workspace_package_publish_flags(
Ok(flags)
}
+#[cfg(test)]
fn workspace_package_publish_configs(
workspace_root: &Path,
) -> Result<BTreeMap<String, Option<PackagePublish>>, String> {
@@ -5007,6 +5025,7 @@ fn workspace_package_publish_configs(
Ok(configs)
}
+#[cfg(test)]
fn read_workspace_package_dependencies(
workspace_root: &Path,
) -> Result<BTreeMap<String, BTreeSet<String>>, String> {
@@ -5039,6 +5058,176 @@ fn read_workspace_package_dependencies(
Ok(deps)
}
+#[cfg(not(test))]
+fn load_release_workspace_packages(
+ workspace_root: &Path,
+) -> Result<ReleaseWorkspacePackages, String> {
+ const COMMAND: &str = "cargo metadata --locked --format-version 1";
+ let output = Command::new("cargo")
+ .args(["metadata", "--locked", "--format-version", "1"])
+ .current_dir(workspace_root)
+ .output()
+ .map_err(|error| format!("run {COMMAND}: {error}"))?;
+ if !output.status.success() {
+ return Err(format!(
+ "{COMMAND} failed: {}",
+ String::from_utf8_lossy(&output.stderr).trim()
+ ));
+ }
+ let metadata = serde_json::from_slice::<CargoMetadataDocument>(&output.stdout)
+ .map_err(|error| format!("parse {COMMAND} output: {error}"))?;
+ release_workspace_packages_from_metadata(metadata)
+}
+
+#[cfg(test)]
+fn load_release_workspace_packages(
+ workspace_root: &Path,
+) -> Result<ReleaseWorkspacePackages, String> {
+ let dependencies = read_workspace_package_dependencies(workspace_root)?;
+ let mut packages = BTreeMap::new();
+ for record in workspace_package_records(workspace_root)? {
+ let package_dependencies = dependencies
+ .get(&record.name)
+ .into_iter()
+ .flatten()
+ .map(|name| CargoMetadataDependency {
+ name: name.clone(),
+ source: None,
+ kind: None,
+ rename: None,
+ optional: false,
+ target: None,
+ path: Some(PathBuf::from(name)),
+ })
+ .collect();
+ let publish = match record.publish {
+ None => None,
+ Some(PackagePublish::Bool(false)) => Some(Vec::new()),
+ Some(PackagePublish::Bool(true)) => None,
+ Some(PackagePublish::Registries(registries)) => Some(registries),
+ };
+ if packages
+ .insert(
+ record.name.clone(),
+ ReleaseWorkspacePackage {
+ publish,
+ dependencies: package_dependencies,
+ },
+ )
+ .is_some()
+ {
+ return Err(format!("duplicate workspace package name {}", record.name));
+ }
+ }
+ Ok(packages)
+}
+
+fn release_workspace_packages_from_metadata(
+ metadata: CargoMetadataDocument,
+) -> Result<ReleaseWorkspacePackages, String> {
+ let workspace_ids = collect_unique_set(
+ &metadata.workspace_members,
+ "cargo metadata workspace_members",
+ )?;
+ let mut packages = BTreeMap::new();
+ let mut found_ids = BTreeSet::new();
+ for package in metadata.packages {
+ if !workspace_ids.contains(&package.id) {
+ continue;
+ }
+ found_ids.insert(package.id);
+ let dependencies = package
+ .dependencies
+ .into_iter()
+ .filter(|dependency| dependency.kind.as_deref() != Some("dev"))
+ .collect();
+ if packages
+ .insert(
+ package.name.clone(),
+ ReleaseWorkspacePackage {
+ publish: package.publish,
+ dependencies,
+ },
+ )
+ .is_some()
+ {
+ return Err(format!(
+ "cargo metadata contains duplicate workspace package name {}",
+ package.name
+ ));
+ }
+ }
+ if found_ids != workspace_ids {
+ let missing = workspace_ids
+ .difference(&found_ids)
+ .cloned()
+ .collect::<BTreeSet<_>>();
+ return Err(format!(
+ "cargo metadata workspace_members reference missing package ids: {}",
+ join_set(&missing)
+ ));
+ }
+ Ok(packages)
+}
+
+fn validate_public_dependency_closure(
+ packages: &ReleaseWorkspacePackages,
+ public_packages: &BTreeSet<String>,
+) -> Result<(), String> {
+ for package_name in public_packages {
+ let package = packages
+ .get(package_name)
+ .ok_or_else(|| format!("cargo metadata is missing public package {package_name}"))?;
+ for dependency in &package.dependencies {
+ if packages.contains_key(&dependency.name) {
+ if !public_packages.contains(&dependency.name) {
+ return Err(format!(
+ "public package {package_name} has non-public {} dependency {}{}",
+ dependency.kind.as_deref().unwrap_or("normal"),
+ dependency.name,
+ release_dependency_context(dependency)
+ ));
+ }
+ continue;
+ }
+ if dependency.path.is_some() {
+ return Err(format!(
+ "public package {package_name} has unversioned path dependency {} outside the workspace{}",
+ dependency.name,
+ release_dependency_context(dependency)
+ ));
+ }
+ match dependency.source.as_deref() {
+ Some(source) if source.starts_with("registry+") => {}
+ Some(source) => {
+ return Err(format!(
+ "public package {package_name} has non-registry dependency {} from {source}{}",
+ dependency.name,
+ release_dependency_context(dependency)
+ ));
+ }
+ None => {
+ return Err(format!(
+ "public package {package_name} has dependency {} without a registry source{}",
+ dependency.name,
+ release_dependency_context(dependency)
+ ));
+ }
+ }
+ }
+ }
+ Ok(())
+}
+
+fn release_dependency_context(dependency: &CargoMetadataDependency) -> String {
+ format!(
+ " (rename={}, optional={}, target={})",
+ dependency.rename.as_deref().unwrap_or("none"),
+ dependency.optional,
+ dependency.target.as_deref().unwrap_or("all")
+ )
+}
+
fn validate_publishable_dto_tooling_sources(
workspace_root: &Path,
public_crates: &BTreeSet<String>,
@@ -8277,6 +8466,7 @@ fn validate_coverage_policy_parity(
Ok(())
}
+#[cfg(test)]
fn publish_config_is_public(publish: Option<&PackagePublish>) -> bool {
matches!(
publish,
@@ -8285,10 +8475,19 @@ fn publish_config_is_public(publish: Option<&PackagePublish>) -> bool {
)
}
+#[cfg(test)]
fn publish_config_is_non_public(publish: Option<&PackagePublish>) -> bool {
matches!(publish, Some(PackagePublish::Bool(false)))
}
+fn metadata_publish_config_is_public(publish: Option<&Vec<String>>) -> bool {
+ matches!(publish, Some(registries) if registries.as_slice() == ["crates-io"])
+}
+
+fn metadata_publish_config_is_non_public(publish: Option<&Vec<String>>) -> bool {
+ matches!(publish, Some(registries) if registries.is_empty())
+}
+
#[cfg(test)]
fn validate_release_publish_policy(
workspace_root: &Path,
@@ -8296,6 +8495,9 @@ fn validate_release_publish_policy(
contract_version: &str,
) -> Result<(), String> {
let release = load_release_contract(workspace_root, contract_version)?;
+ if release.schema.version != 1 {
+ return Err("release policy schema.version must be 1".to_string());
+ }
if release.release.version.trim().is_empty() {
return Err("release.version must not be empty".to_string());
}
@@ -8306,23 +8508,11 @@ fn validate_release_publish_policy(
));
}
- let workspace_packages = workspace_package_names(workspace_root)?
- .into_iter()
- .collect::<BTreeSet<_>>();
- let uses_classification = release.uses_classification();
- let public_field = if uses_classification {
- "classification.public"
- } else {
- "publish.crates"
- };
- let internal_field = if uses_classification {
- "classification.internal"
- } else {
- "internal.crates"
- };
+ let metadata_packages = load_release_workspace_packages(workspace_root)?;
+ let workspace_packages = metadata_packages.keys().cloned().collect::<BTreeSet<_>>();
- let public_set = collect_unique_set(&release.public_crates(), public_field)?;
- let internal_set = collect_unique_set(&release.internal_crates(), internal_field)?;
+ let public_set = collect_unique_set(&release.public_crates(), "classification.public")?;
+ let internal_set = collect_unique_set(&release.internal_crates(), "classification.internal")?;
let deferred_set = collect_unique_set(&release.deferred_crates(), "classification.deferred")?;
let retired_set = collect_unique_set(&release.retired_crates(), "classification.retired")?;
let yank_only_set =
@@ -8397,30 +8587,27 @@ fn validate_release_publish_policy(
.enumerate()
.map(|(idx, name)| (name.clone(), idx))
.collect::<BTreeMap<_, _>>();
- let dependencies = read_workspace_package_dependencies(workspace_root)
- .expect("workspace package manifests were already parsed");
+ validate_public_dependency_closure(&metadata_packages, &public_set)?;
for crate_name in &public_set {
- let crate_deps = &dependencies[crate_name];
+ let crate_deps = &metadata_packages[crate_name].dependencies;
let crate_order = order_index[crate_name];
for dep in crate_deps {
- if !public_set.contains(dep) {
+ if !public_set.contains(&dep.name) {
continue;
}
- let dep_order = order_index[dep];
+ let dep_order = order_index[&dep.name];
if dep_order >= crate_order {
return Err(format!(
"publish order must place dependency {} before {}",
- dep, crate_name
+ dep.name, crate_name
));
}
}
}
- let publish_configs = workspace_package_publish_configs(workspace_root)
- .expect("workspace publish configs are stable");
for crate_name in &public_set {
- let publish = publish_configs[crate_name].as_ref();
- if !publish_config_is_public(publish) {
+ let publish = metadata_packages[crate_name].publish.as_ref();
+ if !metadata_publish_config_is_public(publish) {
return Err(format!(
"public crate {} must set publish = [\"crates-io\"]",
crate_name
@@ -8433,8 +8620,8 @@ fn validate_release_publish_policy(
.chain(retired_set.iter())
.chain(yank_only_set.iter())
{
- let publish = publish_configs[crate_name].as_ref();
- if !publish_config_is_non_public(publish) {
+ let publish = metadata_packages[crate_name].publish.as_ref();
+ if !metadata_publish_config_is_non_public(publish) {
return Err(format!(
"non-public crate {} must set publish = false",
crate_name
@@ -8484,15 +8671,8 @@ fn validate_release_preflight_with_override_and_profile(
)?;
let policy =
load_coverage_policy(&bundle.root).expect("validated contract includes coverage policy");
- let publish_crates = collect_unique_set(
- &release.public_crates(),
- if release.uses_classification() {
- "classification.public"
- } else {
- "publish.crates"
- },
- )
- .expect("validated contract enforces unique public crates");
+ let publish_crates = collect_unique_set(&release.public_crates(), "classification.public")
+ .expect("validated contract enforces unique public crates");
let required_crate_list = policy
.required_crates()
.expect("validated contract includes required crates");
@@ -8598,6 +8778,9 @@ fn validate_release_publish_policy_with_override(
contract_version,
release_policy_override,
)?;
+ if release.schema.version != 1 {
+ return Err("release policy schema.version must be 1".to_string());
+ }
if release.release.version.trim().is_empty() {
return Err("release.version must not be empty".to_string());
}
@@ -8608,23 +8791,11 @@ fn validate_release_publish_policy_with_override(
));
}
- let workspace_packages = workspace_package_names(workspace_root)?
- .into_iter()
- .collect::<BTreeSet<_>>();
- let uses_classification = release.uses_classification();
- let public_field = if uses_classification {
- "classification.public"
- } else {
- "publish.crates"
- };
- let internal_field = if uses_classification {
- "classification.internal"
- } else {
- "internal.crates"
- };
+ let metadata_packages = load_release_workspace_packages(workspace_root)?;
+ let workspace_packages = metadata_packages.keys().cloned().collect::<BTreeSet<_>>();
- let public_set = collect_unique_set(&release.public_crates(), public_field)?;
- let internal_set = collect_unique_set(&release.internal_crates(), internal_field)?;
+ let public_set = collect_unique_set(&release.public_crates(), "classification.public")?;
+ let internal_set = collect_unique_set(&release.internal_crates(), "classification.internal")?;
let deferred_set = collect_unique_set(&release.deferred_crates(), "classification.deferred")?;
let retired_set = collect_unique_set(&release.retired_crates(), "classification.retired")?;
let yank_only_set =
@@ -8699,30 +8870,27 @@ fn validate_release_publish_policy_with_override(
.enumerate()
.map(|(idx, name)| (name.clone(), idx))
.collect::<BTreeMap<_, _>>();
- let dependencies = read_workspace_package_dependencies(workspace_root)
- .expect("workspace package manifests were already parsed");
+ validate_public_dependency_closure(&metadata_packages, &public_set)?;
for crate_name in &public_set {
- let crate_deps = &dependencies[crate_name];
+ let crate_deps = &metadata_packages[crate_name].dependencies;
let crate_order = order_index[crate_name];
for dep in crate_deps {
- if !public_set.contains(dep) {
+ if !public_set.contains(&dep.name) {
continue;
}
- let dep_order = order_index[dep];
+ let dep_order = order_index[&dep.name];
if dep_order >= crate_order {
return Err(format!(
"publish order must place dependency {} before {}",
- dep, crate_name
+ dep.name, crate_name
));
}
}
}
- let publish_configs = workspace_package_publish_configs(workspace_root)
- .expect("workspace publish configs are stable");
for crate_name in &public_set {
- let publish = publish_configs[crate_name].as_ref();
- if !publish_config_is_public(publish) {
+ let publish = metadata_packages[crate_name].publish.as_ref();
+ if !metadata_publish_config_is_public(publish) {
return Err(format!(
"public crate {} must set publish = [\"crates-io\"]",
crate_name
@@ -8735,8 +8903,8 @@ fn validate_release_publish_policy_with_override(
.chain(retired_set.iter())
.chain(yank_only_set.iter())
{
- let publish = publish_configs[crate_name].as_ref();
- if !publish_config_is_non_public(publish) {
+ let publish = metadata_packages[crate_name].publish.as_ref();
+ if !metadata_publish_config_is_non_public(publish) {
return Err(format!(
"non-public crate {} must set publish = false",
crate_name
@@ -9271,14 +9439,18 @@ summary = "Exercise synthetic major release governance."
);
write_file(
&root_release_policy_path(&root),
- r#"[release]
-version = "1.0.0"
+ r#"[schema]
+version = 1
-[publish]
-crates = ["radroots_a"]
+[release]
+version = "1.0.0"
-[internal]
-crates = ["radroots_b"]
+[classification]
+public = ["radroots_a"]
+internal = ["radroots_b"]
+deferred = []
+retired = []
+yank_only = []
[publish_order]
crates = ["radroots_a"]
@@ -9454,7 +9626,199 @@ vector = "contracts/conformance/vectors/operational_listing/build_draft.v1.json"
}
fn write_root_release_policy(root: &Path, raw: &str) {
- write_file(&root_release_policy_path(root), raw);
+ write_file(
+ &root_release_policy_path(root),
+ &format!("[schema]\nversion = 1\n\n{raw}"),
+ );
+ }
+
+ fn write_basic_release_policy(
+ root: &Path,
+ version: &str,
+ public: &[&str],
+ internal: &[&str],
+ publish_order: &[&str],
+ ) {
+ let render = |values: &[&str]| {
+ values
+ .iter()
+ .map(|value| format!("{value:?}"))
+ .collect::<Vec<_>>()
+ .join(", ")
+ };
+ write_root_release_policy(
+ root,
+ &format!(
+ "[release]\nversion = {version:?}\n\n[classification]\npublic = [{}]\ninternal = [{}]\ndeferred = []\nretired = []\nyank_only = []\n\n[publish_order]\ncrates = [{}]\n",
+ render(public),
+ render(internal),
+ render(publish_order)
+ ),
+ );
+ }
+
+ fn metadata_dependency(
+ name: &str,
+ source: Option<&str>,
+ kind: Option<&str>,
+ rename: Option<&str>,
+ optional: bool,
+ target: Option<&str>,
+ path: Option<&str>,
+ ) -> CargoMetadataDependency {
+ CargoMetadataDependency {
+ name: name.to_string(),
+ source: source.map(str::to_string),
+ kind: kind.map(str::to_string),
+ rename: rename.map(str::to_string),
+ optional,
+ target: target.map(str::to_string),
+ path: path.map(PathBuf::from),
+ }
+ }
+
+ #[test]
+ fn release_policy_metadata_covers_all_dependency_forms() {
+ let metadata = CargoMetadataDocument {
+ workspace_members: vec![
+ "path+file:///workspace/a#radroots_a@1.0.0".to_string(),
+ "path+file:///workspace/b#radroots_b@1.0.0".to_string(),
+ "path+file:///workspace/c#radroots_c@1.0.0".to_string(),
+ ],
+ packages: vec![
+ CargoMetadataPackage {
+ id: "path+file:///workspace/a#radroots_a@1.0.0".to_string(),
+ name: "radroots_a".to_string(),
+ publish: Some(vec!["crates-io".to_string()]),
+ dependencies: vec![
+ metadata_dependency(
+ "radroots_b",
+ None,
+ Some("build"),
+ Some("renamed_b"),
+ true,
+ Some("cfg(target_os = \"ios\")"),
+ Some("/workspace/b"),
+ ),
+ metadata_dependency(
+ "radroots_c",
+ None,
+ Some("dev"),
+ None,
+ false,
+ None,
+ Some("/workspace/c"),
+ ),
+ metadata_dependency(
+ "serde",
+ Some("registry+https://github.com/rust-lang/crates.io-index"),
+ None,
+ None,
+ false,
+ None,
+ None,
+ ),
+ ],
+ },
+ CargoMetadataPackage {
+ id: "path+file:///workspace/b#radroots_b@1.0.0".to_string(),
+ name: "radroots_b".to_string(),
+ publish: Some(vec!["crates-io".to_string()]),
+ dependencies: Vec::new(),
+ },
+ CargoMetadataPackage {
+ id: "path+file:///workspace/c#radroots_c@1.0.0".to_string(),
+ name: "radroots_c".to_string(),
+ publish: Some(Vec::new()),
+ dependencies: Vec::new(),
+ },
+ ],
+ };
+ let mut packages =
+ release_workspace_packages_from_metadata(metadata).expect("parse cargo metadata");
+ assert_eq!(packages["radroots_a"].dependencies.len(), 2);
+
+ let public = BTreeSet::from(["radroots_a".to_string(), "radroots_b".to_string()]);
+ validate_public_dependency_closure(&packages, &public)
+ .expect("renamed optional target build dependency must be covered");
+
+ packages
+ .get_mut("radroots_a")
+ .expect("public package")
+ .dependencies
+ .push(metadata_dependency(
+ "radroots_c",
+ None,
+ None,
+ Some("internal_alias"),
+ true,
+ Some("cfg(unix)"),
+ Some("/workspace/c"),
+ ));
+ let internal_err = validate_public_dependency_closure(&packages, &public)
+ .expect_err("public dependency on internal package must fail");
+ assert!(internal_err.contains("non-public normal dependency radroots_c"));
+ assert!(internal_err.contains("rename=internal_alias, optional=true, target=cfg(unix)"));
+
+ let dependencies = &mut packages
+ .get_mut("radroots_a")
+ .expect("public package")
+ .dependencies;
+ dependencies.pop();
+ dependencies.push(metadata_dependency(
+ "external_path",
+ None,
+ None,
+ None,
+ false,
+ None,
+ Some("/outside/workspace"),
+ ));
+ assert!(
+ validate_public_dependency_closure(&packages, &public)
+ .expect_err("external path dependency must fail")
+ .contains("unversioned path dependency external_path")
+ );
+
+ let dependencies = &mut packages
+ .get_mut("radroots_a")
+ .expect("public package")
+ .dependencies;
+ dependencies.pop();
+ dependencies.push(metadata_dependency(
+ "git_dep",
+ Some("git+https://example.com/dependency"),
+ None,
+ None,
+ false,
+ None,
+ None,
+ ));
+ assert!(
+ validate_public_dependency_closure(&packages, &public)
+ .expect_err("git dependency must fail")
+ .contains("non-registry dependency git_dep")
+ );
+
+ let dependencies = &mut packages
+ .get_mut("radroots_a")
+ .expect("public package")
+ .dependencies;
+ dependencies.pop();
+ dependencies.push(metadata_dependency(
+ "source_less",
+ None,
+ None,
+ None,
+ false,
+ None,
+ None,
+ ));
+ assert!(
+ validate_public_dependency_closure(&packages, &public)
+ .expect_err("source-less external dependency must fail")
+ .contains("without a registry source")
+ );
}
fn configure_root_release_policy_workspace(root: &Path) {
@@ -11383,122 +11747,30 @@ readme = { workspace = true }
}
#[test]
- fn release_contract_helpers_cover_classification_and_env_override_paths() {
- let release = ReleaseSection {
- version: "1.0.0".to_string(),
- };
- let empty_order = ReleaseCrateSet { crates: Vec::new() };
-
- let legacy = ReleaseContractFile {
- release: ReleaseSection {
- version: release.version.clone(),
- },
- classification: ReleaseClassification::default(),
- publish: Some(ReleaseCrateSet {
- crates: vec!["radroots_public".to_string()],
- }),
- internal: Some(ReleaseCrateSet {
- crates: vec!["radroots_internal".to_string()],
- }),
- publish_order: ReleaseCrateSet {
- crates: empty_order.crates.clone(),
- },
- };
- assert!(!legacy.uses_classification());
- assert_eq!(legacy.public_crates(), vec!["radroots_public".to_string()]);
- assert_eq!(
- legacy.internal_crates(),
- vec!["radroots_internal".to_string()]
- );
-
- let empty_legacy = ReleaseContractFile {
- release: ReleaseSection {
- version: release.version.clone(),
- },
- classification: ReleaseClassification::default(),
- publish: None,
- internal: None,
- publish_order: ReleaseCrateSet {
- crates: empty_order.crates.clone(),
- },
- };
- assert!(!empty_legacy.uses_classification());
- assert_eq!(empty_legacy.public_crates(), Vec::<String>::new());
- assert_eq!(empty_legacy.internal_crates(), Vec::<String>::new());
-
- let internal = ReleaseContractFile {
- release: ReleaseSection {
- version: release.version.clone(),
- },
- classification: ReleaseClassification {
- internal: vec!["radroots_internal_only".to_string()],
- ..ReleaseClassification::default()
- },
- publish: None,
- internal: None,
- publish_order: ReleaseCrateSet {
- crates: empty_order.crates.clone(),
- },
- };
- assert!(internal.uses_classification());
-
- let deferred = ReleaseContractFile {
+ fn release_contract_helpers_cover_strict_classification_and_override_paths() {
+ let release = ReleaseContractFile {
+ schema: ReleasePolicySchema { version: 1 },
release: ReleaseSection {
- version: release.version.clone(),
+ version: "1.0.0".to_string(),
},
classification: ReleaseClassification {
+ public: vec!["radroots_public".to_string()],
+ internal: vec!["radroots_internal".to_string()],
deferred: vec!["radroots_deferred".to_string()],
- ..ReleaseClassification::default()
- },
- publish: None,
- internal: None,
- publish_order: ReleaseCrateSet {
- crates: empty_order.crates.clone(),
- },
- };
- assert!(deferred.uses_classification());
- assert_eq!(
- deferred.deferred_crates(),
- vec!["radroots_deferred".to_string()]
- );
-
- let retired = ReleaseContractFile {
- release: ReleaseSection {
- version: release.version.clone(),
- },
- classification: ReleaseClassification {
retired: vec!["radroots_retired".to_string()],
- ..ReleaseClassification::default()
+ yank_only: vec!["radroots_yank_only".to_string()],
},
- publish: None,
- internal: None,
publish_order: ReleaseCrateSet {
- crates: empty_order.crates.clone(),
- },
- };
- assert!(retired.uses_classification());
- assert_eq!(
- retired.retired_crates(),
- vec!["radroots_retired".to_string()]
- );
-
- let yank_only = ReleaseContractFile {
- release,
- classification: ReleaseClassification {
- yank_only: vec!["radroots_yank_only".to_string()],
- ..ReleaseClassification::default()
+ crates: vec!["radroots_public".to_string()],
},
- publish: None,
- internal: None,
- publish_order: empty_order,
};
- assert!(yank_only.uses_classification());
- assert_eq!(
- yank_only.yank_only_crates(),
- vec!["radroots_yank_only".to_string()]
- );
+ assert_eq!(release.public_crates(), vec!["radroots_public"]);
+ assert_eq!(release.internal_crates(), vec!["radroots_internal"]);
+ assert_eq!(release.deferred_crates(), vec!["radroots_deferred"]);
+ assert_eq!(release.retired_crates(), vec!["radroots_retired"]);
+ assert_eq!(release.yank_only_crates(), vec!["radroots_yank_only"]);
- let root = create_synthetic_workspace("release_contract_env_override");
+ let root = create_synthetic_workspace("release_contract_strict_override");
let policy_path = root_release_policy_path(&root);
let resolved =
resolve_release_contract_path_with_override(&root, "1.0.0", Some(policy_path.clone()))
@@ -11514,7 +11786,6 @@ readme = { workspace = true }
.expect_err("missing fixture policy should fail");
assert!(err.contains("release policy override points to a missing file"));
assert!(err.contains(&missing_policy.display().to_string()));
-
let _ = fs::remove_dir_all(&root);
}
@@ -11810,117 +12081,50 @@ crates = ["unknown"]
fn release_publish_policy_reports_contract_errors() {
let root = create_synthetic_workspace("release_policy_errors");
let contract_root = root.join("contracts");
- let release_policy_path = root_release_policy_path(&root);
-
- write_file(
- &release_policy_path,
- r#"[release]
-version = ""
-
-[publish]
-crates = ["radroots_a"]
-[internal]
-crates = ["radroots_b"]
-
-[publish_order]
-crates = ["radroots_a"]
-"#,
- );
+ write_basic_release_policy(&root, "", &["radroots_a"], &["radroots_b"], &["radroots_a"]);
let empty_version = validate_release_publish_policy(&root, &contract_root, "1.0.0")
.expect_err("empty release version");
assert!(empty_version.contains("must not be empty"));
- write_file(
- &release_policy_path,
- r#"[release]
-version = "2.0.0"
-
-[publish]
-crates = ["radroots_a"]
-
-[internal]
-crates = ["radroots_b"]
-
-[publish_order]
-crates = ["radroots_a"]
-"#,
+ write_basic_release_policy(
+ &root,
+ "2.0.0",
+ &["radroots_a"],
+ &["radroots_b"],
+ &["radroots_a"],
);
let version_mismatch = validate_release_publish_policy(&root, &contract_root, "1.0.0")
.expect_err("release version mismatch");
assert!(version_mismatch.contains("must match contract version"));
- write_file(
- &release_policy_path,
- r#"[release]
-version = "1.0.0"
-
-[publish]
-crates = ["radroots_a"]
-
-[internal]
-crates = ["radroots_a"]
-
-[publish_order]
-crates = ["radroots_a"]
-"#,
+ write_basic_release_policy(
+ &root,
+ "1.0.0",
+ &["radroots_a"],
+ &["radroots_a"],
+ &["radroots_a"],
);
let overlap = validate_release_publish_policy(&root, &contract_root, "1.0.0")
.expect_err("publish/internal overlap");
assert!(overlap.contains("overlap is not allowed"));
- write_file(
- &release_policy_path,
- r#"[release]
-version = "1.0.0"
-
-[publish]
-crates = ["radroots_a"]
-
-[internal]
-crates = []
-
-[publish_order]
-crates = ["radroots_a"]
-"#,
- );
+ write_basic_release_policy(&root, "1.0.0", &["radroots_a"], &[], &["radroots_a"]);
let missing_workspace = validate_release_publish_policy(&root, &contract_root, "1.0.0")
.expect_err("missing workspace crate");
assert!(missing_workspace.contains("missing workspace crates"));
- write_file(
- &release_policy_path,
- r#"[release]
-version = "1.0.0"
-
-[publish]
-crates = ["radroots_a"]
-
-[internal]
-crates = ["radroots_b"]
-
-[publish_order]
-crates = []
-"#,
- );
+ write_basic_release_policy(&root, "1.0.0", &["radroots_a"], &["radroots_b"], &[]);
let missing_publish_order = validate_release_publish_policy(&root, &contract_root, "1.0.0")
.expect_err("missing publish order entries");
assert!(missing_publish_order.contains("missing publish crates"));
- write_file(
- &release_policy_path,
- r#"[release]
-version = "1.0.0"
-
-[publish]
-crates = ["radroots_a"]
-
-[internal]
-crates = ["radroots_b"]
-
-[publish_order]
-crates = ["radroots_a", "radroots_b"]
-"#,
+ write_basic_release_policy(
+ &root,
+ "1.0.0",
+ &["radroots_a"],
+ &["radroots_b"],
+ &["radroots_a", "radroots_b"],
);
let extra_publish_order = validate_release_publish_policy(&root, &contract_root, "1.0.0")
.expect_err("extra publish order entries");
@@ -11956,39 +12160,23 @@ documentation = "https://docs.example.com/b"
readme = "README"
"#,
);
- write_file(
- &release_policy_path,
- r#"[release]
-version = "1.0.0"
-
-[publish]
-crates = ["radroots_a", "radroots_b"]
-
-[internal]
-crates = []
-
-[publish_order]
-crates = ["radroots_a", "radroots_b"]
-"#,
+ write_basic_release_policy(
+ &root,
+ "1.0.0",
+ &["radroots_a", "radroots_b"],
+ &[],
+ &["radroots_a", "radroots_b"],
);
let dependency_order = validate_release_publish_policy(&root, &contract_root, "1.0.0")
.expect_err("dependency order violation");
assert!(dependency_order.contains("must place dependency"));
- write_file(
- &release_policy_path,
- r#"[release]
-version = "1.0.0"
-
-[publish]
-crates = ["radroots_a"]
-
-[internal]
-crates = ["radroots_b"]
-
-[publish_order]
-crates = ["radroots_a"]
-"#,
+ write_basic_release_policy(
+ &root,
+ "1.0.0",
+ &["radroots_a"],
+ &["radroots_b"],
+ &["radroots_a"],
);
write_file(
&root.join("crates").join("b").join("Cargo.toml"),
@@ -11999,8 +12187,9 @@ edition = "2024"
publish = false
"#,
);
- validate_release_publish_policy(&root, &contract_root, "1.0.0")
- .expect("internal dependency should be ignored in publish ordering");
+ let internal_dependency = validate_release_publish_policy(&root, &contract_root, "1.0.0")
+ .expect_err("public dependency on internal package must fail");
+ assert!(internal_dependency.contains("non-public normal dependency radroots_b"));
write_file(
&root.join("crates").join("a").join("Cargo.toml"),
@@ -12011,20 +12200,12 @@ edition = "2024"
publish = false
"#,
);
- write_file(
- &release_policy_path,
- r#"[release]
-version = "1.0.0"
-
-[publish]
-crates = ["radroots_a"]
-
-[internal]
-crates = ["radroots_b"]
-
-[publish_order]
-crates = ["radroots_a"]
-"#,
+ write_basic_release_policy(
+ &root,
+ "1.0.0",
+ &["radroots_a"],
+ &["radroots_b"],
+ &["radroots_a"],
);
let publish_flag = validate_release_publish_policy(&root, &contract_root, "1.0.0")
.expect_err("publish crate must be publishable");
@@ -12776,20 +12957,12 @@ crates = ["radroots_a", "radroots_b"]
);
let missing_release = temp_root("release_missing_workspace_manifest");
- write_root_release_policy(
+ write_basic_release_policy(
&missing_release,
- r#"[release]
-version = "1.0.0"
-
-[publish]
-crates = ["radroots_a"]
-
-[internal]
-crates = ["radroots_b"]
-
-[publish_order]
-crates = ["radroots_a"]
-"#,
+ "1.0.0",
+ &["radroots_a"],
+ &["radroots_b"],
+ &["radroots_a"],
);
let release_workspace_err =
validate_release_publish_policy(&missing_release, &contract_root, "1.0.0")
@@ -12802,77 +12975,45 @@ crates = ["radroots_a"]
.expect_err("release contract read error");
assert!(release_load_err.contains(RELEASE_POLICY_RELATIVE));
- write_file(
- &release_policy_path,
- r#"[release]
-version = "1.0.0"
-
-[publish]
-crates = ["radroots_a", "radroots_a"]
-
-[internal]
-crates = ["radroots_b"]
-
-[publish_order]
-crates = ["radroots_a"]
-"#,
+ write_basic_release_policy(
+ &root,
+ "1.0.0",
+ &["radroots_a", "radroots_a"],
+ &["radroots_b"],
+ &["radroots_a"],
);
let duplicate_publish = validate_release_publish_policy(&root, &contract_root, "1.0.0")
.expect_err("duplicate publish crates");
- assert!(duplicate_publish.contains("publish.crates has duplicate crate"));
-
- write_file(
- &release_policy_path,
- r#"[release]
-version = "1.0.0"
+ assert!(duplicate_publish.contains("classification.public has duplicate crate"));
-[publish]
-crates = ["radroots_a"]
-
-[internal]
-crates = ["radroots_b", "radroots_b"]
-
-[publish_order]
-crates = ["radroots_a"]
-"#,
+ write_basic_release_policy(
+ &root,
+ "1.0.0",
+ &["radroots_a"],
+ &["radroots_b", "radroots_b"],
+ &["radroots_a"],
);
let duplicate_internal = validate_release_publish_policy(&root, &contract_root, "1.0.0")
.expect_err("duplicate internal crates");
- assert!(duplicate_internal.contains("internal.crates has duplicate crate"));
-
- write_file(
- &release_policy_path,
- r#"[release]
-version = "1.0.0"
-
-[publish]
-crates = ["radroots_a"]
+ assert!(duplicate_internal.contains("classification.internal has duplicate crate"));
-[internal]
-crates = ["radroots_b"]
-
-[publish_order]
-crates = ["radroots_a", "radroots_a"]
-"#,
+ write_basic_release_policy(
+ &root,
+ "1.0.0",
+ &["radroots_a"],
+ &["radroots_b"],
+ &["radroots_a", "radroots_a"],
);
let duplicate_order = validate_release_publish_policy(&root, &contract_root, "1.0.0")
.expect_err("duplicate publish order");
assert!(duplicate_order.contains("publish_order.crates has duplicate crate"));
- write_file(
- &release_policy_path,
- r#"[release]
-version = "1.0.0"
-
-[publish]
-crates = ["radroots_a"]
-
-[internal]
-crates = ["radroots_b"]
-
-[publish_order]
-crates = ["radroots_a"]
-"#,
+ write_basic_release_policy(
+ &root,
+ "1.0.0",
+ &["radroots_a"],
+ &["radroots_b"],
+ &["radroots_a"],
);
write_file(
&root.join("crates").join("a").join("Cargo.toml"),
@@ -13071,14 +13212,18 @@ require_conformance_vectors = true
let duplicate_publish = create_synthetic_workspace("preflight_duplicate_publish");
write_file(
&root_release_policy_path(&duplicate_publish),
- r#"[release]
-version = "1.0.0"
+ r#"[schema]
+version = 1
-[publish]
-crates = ["radroots_a", "radroots_a"]
+[release]
+version = "1.0.0"
-[internal]
-crates = ["radroots_b"]
+[classification]
+public = ["radroots_a", "radroots_a"]
+internal = ["radroots_b"]
+deferred = []
+retired = []
+yank_only = []
[publish_order]
crates = ["radroots_a"]
@@ -13086,7 +13231,7 @@ crates = ["radroots_a"]
);
let duplicate_publish_err = validate_generic_release_preflight(&duplicate_publish)
.expect_err("duplicate publish crates");
- assert!(duplicate_publish_err.contains("publish.crates has duplicate crate"));
+ assert!(duplicate_publish_err.contains("classification.public has duplicate crate"));
let _ = fs::remove_dir_all(&duplicate_publish);
let duplicate_required = create_synthetic_workspace("preflight_duplicate_required");
@@ -13336,7 +13481,6 @@ Volume,
let root = create_synthetic_workspace("coverage_release_extra_errors");
let contract_root = root.join("contracts");
let coverage_root = coverage_root(&contract_root);
- let release_policy_path = root_release_policy_path(&root);
write_file(
&coverage_root.join("coverage.toml"),
@@ -13372,39 +13516,23 @@ crates = ["radroots_b"]
.expect_err("required list must match workspace crates");
assert!(required_list_mismatch.contains("missing workspace crates"));
- write_file(
- &release_policy_path,
- r#"[release]
-version = "1.0.0"
-
-[publish]
-crates = ["radroots_a", "radroots_b", "radroots_extra"]
-
-[internal]
-crates = []
-
-[publish_order]
-crates = ["radroots_a", "radroots_b"]
-"#,
+ write_basic_release_policy(
+ &root,
+ "1.0.0",
+ &["radroots_a", "radroots_b", "radroots_extra"],
+ &[],
+ &["radroots_a", "radroots_b"],
);
let release_extra = validate_release_publish_policy(&root, &contract_root, "1.0.0")
.expect_err("release extra crate");
assert!(release_extra.contains("include unknown crates"));
- write_file(
- &release_policy_path,
- r#"[release]
-version = "1.0.0"
-
-[publish]
-crates = ["radroots_a"]
-
-[internal]
-crates = ["radroots_b"]
-
-[publish_order]
-crates = ["radroots_a", "radroots_b"]
-"#,
+ write_basic_release_policy(
+ &root,
+ "1.0.0",
+ &["radroots_a"],
+ &["radroots_b"],
+ &["radroots_a", "radroots_b"],
);
let publish_order_extra = validate_release_publish_policy(&root, &contract_root, "1.0.0")
.expect_err("publish order non-publish crate");
@@ -13416,16 +13544,17 @@ crates = ["radroots_a", "radroots_b"]
#[test]
fn validate_contract_bundle_reports_release_policy_errors() {
let release_error_root = create_synthetic_workspace("bundle_release_policy_error");
- write_file(
- &root_release_policy_path(&release_error_root),
+ write_root_release_policy(
+ &release_error_root,
r#"[release]
version = "1.0.0"
-[publish]
-crates = ["radroots_a"]
-
-[internal]
-crates = ["radroots_b"]
+[classification]
+public = ["radroots_a"]
+internal = ["radroots_b"]
+deferred = []
+retired = []
+yank_only = []
[publish_order]
crates = []
diff --git a/tools/xtask/src/contract/release_provenance.rs b/tools/xtask/src/contract/release_provenance.rs
@@ -11,6 +11,7 @@ use std::path::{Component, Path, PathBuf};
use std::process::Command;
const SCHEMA_VERSION: u32 = 1;
+const PUBLISH_POLICY_SCHEMA_VERSION: u32 = 1;
const CONTRACT_ID: &str = "radroots.release.phase1_publication_provenance.v1";
const AUTHORITY: &str = "candidate_release_evidence_not_protocol_authority_v1";
const HASH_ALGORITHM: &str = "sha256_bytes_v1";
@@ -173,6 +174,7 @@ struct TestProfiles {
#[derive(Debug, Deserialize)]
#[serde(deny_unknown_fields)]
struct PublishPolicy {
+ schema: PublishPolicySchema,
release: ReleaseVersion,
classification: ReleaseClassification,
publish_order: PublishOrder,
@@ -180,6 +182,12 @@ struct PublishPolicy {
#[derive(Debug, Deserialize)]
#[serde(deny_unknown_fields)]
+struct PublishPolicySchema {
+ version: u32,
+}
+
+#[derive(Debug, Deserialize)]
+#[serde(deny_unknown_fields)]
struct ReleaseVersion {
version: String,
}
@@ -414,6 +422,11 @@ fn validate_test_profile(label: &str, profile: &TestProfile) -> Result<(), Strin
}
fn validate_publish_policy(policy: &PublishPolicy) -> Result<(), String> {
+ if policy.schema.version != PUBLISH_POLICY_SCHEMA_VERSION {
+ return Err(format!(
+ "release publish policy schema version must be {PUBLISH_POLICY_SCHEMA_VERSION}"
+ ));
+ }
if policy.release.version.trim().is_empty() {
return Err("release.version must not be empty".to_owned());
}
@@ -1288,6 +1301,9 @@ mod tests {
fn test_policy() -> PublishPolicy {
PublishPolicy {
+ schema: PublishPolicySchema {
+ version: PUBLISH_POLICY_SCHEMA_VERSION,
+ },
release: ReleaseVersion {
version: "1.2.3".to_owned(),
},