aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorValentin Popov <valentin@popov.link>2026-06-25 03:41:44 +0300
committerValentin Popov <valentin@popov.link>2026-06-25 10:45:33 +0300
commit5950c62cec76f86578817b54e45c7bf5d52add67 (patch)
tree1e37336351a179970ae94d4a4d4260116215d8b0
parent247f86aa0997a0a6b1a209f38b90e4580c842e1b (diff)
downloadfparkan-5950c62cec76f86578817b54e45c7bf5d52add67.tar.xz
fparkan-5950c62cec76f86578817b54e45c7bf5d52add67.zip
ci: tighten supply-chain fallback policy
-rw-r--r--fixtures/acceptance/coverage.tsv2
-rw-r--r--xtask/src/main.rs61
2 files changed, 61 insertions, 2 deletions
diff --git a/fixtures/acceptance/coverage.tsv b/fixtures/acceptance/coverage.tsv
index a39009a..14d4132 100644
--- a/fixtures/acceptance/coverage.tsv
+++ b/fixtures/acceptance/coverage.tsv
@@ -11,7 +11,7 @@ S0-ARCH-003 covered cargo xtask policy rejects platform/render adapter dependenc
S0-ARCH-004 covered cargo xtask policy scans workspace-owned Rust/TOML for unsafe constructs and workspace lints forbid unsafe_code
S0-ARCH-005 covered cargo xtask policy rejects Python source files, Python shebangs, and Python CI workflow steps while allowing docs requirements.txt
S0-ARCH-006 covered cargo xtask policy rejects non-fparkan package directories under crates/
-S0-ARCH-007 covered cargo xtask ci runs fmt, policy, workspace test, clippy, rustdoc warnings, cargo-deny or built-in supply-chain fallback, and strict acceptance audit
+S0-ARCH-007 covered cargo xtask ci runs fmt, policy, workspace test, clippy, rustdoc warnings, cargo-deny with reviewed deny.toml, and strict acceptance audit; built-in supply-chain fallback is opt-in local-only and forbidden when CI is set
S0-ARCH-008 covered cargo xtask policy rejects moving Rust toolchains and workspace rust-version drift
S0-ARCH-009 covered .github/workflows/ci.yml runs a pinned MSRV backend-neutral crate job
S0-ARCH-010 covered cargo xtask acceptance audit emits commit_sha, rust_toolchain, and msrv metadata into the JSON artifact
diff --git a/xtask/src/main.rs b/xtask/src/main.rs
index 242f25f..e4023ce 100644
--- a/xtask/src/main.rs
+++ b/xtask/src/main.rs
@@ -38,6 +38,7 @@ const CI_ACCEPTANCE_ROADMAP: &str = "fixtures/acceptance/stage_0_roadmap.md";
const CI_ACCEPTANCE_COVERAGE: &str = "fixtures/acceptance/coverage.tsv";
const CI_ACCEPTANCE_REPORT: &str = "target/fparkan/acceptance/stage-0-audit.json";
const STAGE_PACKAGE_MANIFEST: &str = "fixtures/acceptance/stage_packages.toml";
+const SUPPLY_CHAIN_POLICY_CONFIG: &str = "deny.toml";
const REQUIRED_NATIVE_SMOKE_PLATFORMS: &[&str] = &["macos"];
const APPROVED_REGISTRY_SOURCE: &str = "registry+https://github.com/rust-lang/crates.io-index";
const SUPPLY_CHAIN_BANNED_PACKAGES: &[&str] = &["native-tls", "openssl", "openssl-sys"];
@@ -192,6 +193,7 @@ fn run_cargo_fmt_check() -> Result<(), String> {
}
fn run_cargo_deny() -> Result<(), String> {
+ validate_supply_chain_policy_config(&workspace_relative_path(SUPPLY_CHAIN_POLICY_CONFIG))?;
let cargo_deny = std::env::var_os("CARGO_DENY").unwrap_or_else(|| "cargo-deny".into());
let version_output = match Command::new(&cargo_deny).arg("--version").output() {
Ok(output) => output,
@@ -238,11 +240,15 @@ fn run_cargo_deny() -> Result<(), String> {
const PINNED_CARGO_DENY_VERSION: &str = "0.19.9";
fn handle_cargo_deny_fallback(reason: &str) -> Result<(), String> {
- if std::env::var_os(ALLOW_SUPPLY_CHAIN_FALLBACK_ENV).is_some() {
+ if allow_supply_chain_fallback() {
eprintln!(
"{reason}; running built-in supply-chain policy fallback because {ALLOW_SUPPLY_CHAIN_FALLBACK_ENV} is set"
);
run_builtin_supply_chain_policy(Path::new("."))
+ } else if std::env::var_os(ALLOW_SUPPLY_CHAIN_FALLBACK_ENV).is_some() && ci_env_active() {
+ Err(format!(
+ "{reason}; {ALLOW_SUPPLY_CHAIN_FALLBACK_ENV} is for local developer convenience only and is forbidden when CI is set"
+ ))
} else {
Err(format!(
"{reason}; install cargo-deny {PINNED_CARGO_DENY_VERSION} or explicitly opt into the fallback with {ALLOW_SUPPLY_CHAIN_FALLBACK_ENV}=1"
@@ -250,6 +256,32 @@ fn handle_cargo_deny_fallback(reason: &str) -> Result<(), String> {
}
}
+fn validate_supply_chain_policy_config(path: &Path) -> Result<(), String> {
+ if path.is_file() {
+ Ok(())
+ } else {
+ Err(format!(
+ "reviewed supply-chain policy config is missing: {}",
+ path.display()
+ ))
+ }
+}
+
+fn allow_supply_chain_fallback() -> bool {
+ std::env::var_os(ALLOW_SUPPLY_CHAIN_FALLBACK_ENV).is_some() && !ci_env_active()
+}
+
+fn ci_env_active() -> bool {
+ ci_env_value_is_active(std::env::var("CI").ok().as_deref())
+}
+
+fn ci_env_value_is_active(value: Option<&str>) -> bool {
+ value.is_some_and(|value| {
+ let trimmed = value.trim();
+ !trimmed.is_empty() && trimmed != "0" && !trimmed.eq_ignore_ascii_case("false")
+ })
+}
+
fn run_builtin_supply_chain_policy(root: &Path) -> Result<(), String> {
let mut failures = Vec::new();
validate_workspace_license(root, &mut failures)?;
@@ -2748,6 +2780,33 @@ source = "git+https://example.invalid/repo"
}
#[test]
+ fn supply_chain_policy_config_must_exist() -> Result<(), String> {
+ let root = temp_dir("supply-chain-config");
+ fs::create_dir_all(&root).map_err(|err| err.to_string())?;
+
+ let missing = root.join("deny.toml");
+ assert!(validate_supply_chain_policy_config(&missing).is_err());
+
+ fs::write(&missing, "[graph]\nall-features = true\n").map_err(|err| err.to_string())?;
+ assert_eq!(validate_supply_chain_policy_config(&missing), Ok(()));
+
+ fs::remove_dir_all(root).map_err(|err| err.to_string())?;
+ Ok(())
+ }
+
+ #[test]
+ fn ci_env_truthy_values_are_detected() {
+ assert!(ci_env_value_is_active(Some("true")));
+ assert!(ci_env_value_is_active(Some("1")));
+ assert!(ci_env_value_is_active(Some("yes")));
+ assert!(!ci_env_value_is_active(None));
+ assert!(!ci_env_value_is_active(Some("")));
+ assert!(!ci_env_value_is_active(Some("0")));
+ assert!(!ci_env_value_is_active(Some("false")));
+ assert!(!ci_env_value_is_active(Some(" FALSE ")));
+ }
+
+ #[test]
fn detects_forbidden_domain_dependencies() {
assert!(!is_forbidden_domain_dependency("fparkan-render-vulkan"));
assert!(is_forbidden_domain_dependency("sdl2"));