Your browser sucks, JavaScript disabled! Gunnar's console needs JavaScript and WebAssembly. Git over SSH on port 2222 does not.

nordisk/gunnar

xtask/tests/publishable.rs

//! **Exactly the declared crates in this workspace may reach a public registry.**
//!
//! Until 2026-10-22 that was one crate, gunnar-forge. The owner's order of
//! 2026-10-22 (`cargo install gunnar` installs a startable server) put the
//! `gunnar` crate and its chain on the list: [`THE_CHAIN`] below. Every other
//! member stays `publish = false`, and a crate that joins or leaves the list
//! has to change this file to do it.
//!
//! The intent was written down twice and enforced nowhere.
//! `crates/gunnar-forge/Cargo.toml` spends thirty lines under the heading "WHY
//! THIS CRATE PUBLISHES AND ITS SIBLINGS DO NOT", and the constellation
//! descriptor says `release = false` for the repository — and neither of those
//! is a thing cargo reads. Until 2026-09-11 no manifest in `crates/` carried a
//! `publish` key at all, so `cargo publish -p gunnar-server` typed in the wrong
//! directory would have put the appliance's server half on crates.io. That is
//! not an undoable mistake: a publish can only be yanked, and a yanked crate is
//! still downloadable by anything that names the version.
//!
//! # The answer is cargo's, not a second reading of the manifests
//!
//! `cargo metadata` is what `cargo publish` itself resolves, inheritance and
//! all. A test that re-read the `.toml` files would be a second implementation
//! of `publish.workspace = true`, and the two could disagree in exactly the
//! case that matters — which is the shape of guard this repository keeps
//! finding in other people's code.
//!
//! In `cargo metadata`'s schema `publish` is `null` for "any registry" and `[]`
//! for "none". The list of allowed registries never appears here, because a
//! third state would mean somebody had introduced a private registry and that
//! is a decision this test should notice.

use std::process::Command;

use xtask::json::Json;

/// The `cargo install gunnar` chain: `gunnar` and every workspace crate it can
/// reach through any feature (owner order 2026-10-22). Sorted.
const THE_CHAIN: &[&str] = &[
    "gunnar",
    // `cargo install gunnar-cli`: the command-line tool, one of the three products
    // (GUN-PRODUCTS, 2026-10-30).
    "gunnar-cli",
    "gunnar-client",
    "gunnar-common",
    "gunnar-core",
    "gunnar-crypt",
    "gunnar-grpc",
    "gunnar-http",
    "gunnar-lfs",
    "gunnar-pairing-door",
    "gunnar-policy",
    "gunnar-proto",
    "gunnar-recipe",
    "gunnar-repair",
    "gunnar-server",
    "gunnar-ssh",
    "gunnar-store",
    "gunnar-wire",
];

/// The one crate that may go to a public registry, and the reason, in one
/// place. `gunnar-ui` is a separate repository and needs it: without a registry
/// copy, building it requires a `gunnar` checkout parked at exactly
/// `../../../gunnar`, which is a build instruction no clone can carry.
const THE_EXCEPTION: &str = "gunnar-forge";

fn workspace_root() -> std::path::PathBuf {
    std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
        .parent()
        .expect("xtask/ has a parent")
        .to_path_buf()
}

/// `(name, may_publish)` for every workspace member, as cargo resolves it.
fn members() -> Vec<(String, bool)> {
    let cargo = std::env::var("CARGO").unwrap_or_else(|_| "cargo".to_string());
    let out = Command::new(cargo)
        .args(["metadata", "--no-deps", "--format-version", "1"])
        .current_dir(workspace_root())
        .output()
        .expect("run `cargo metadata`");
    assert!(
        out.status.success(),
        "cargo metadata failed: {}",
        String::from_utf8_lossy(&out.stderr)
    );
    let text = String::from_utf8(out.stdout).expect("cargo metadata is utf-8");
    let json = Json::parse(&text).expect("cargo metadata is json");
    let packages = json.get("packages").expect("metadata has packages").arr();
    assert!(
        !packages.is_empty(),
        "cargo metadata reported no packages, so every assertion below is vacuous"
    );
    packages
        .iter()
        .map(|package| {
            let name = package
                .get("name")
                .and_then(Json::str)
                .expect("a package has a name")
                .to_owned();
            // `null` — absent from the object, or present as Null — is "any
            // registry". Anything else is a list, and an EMPTY list is the only
            // other value this workspace may hold.
            let publish = package.get("publish");
            let may = match publish {
                None | Some(Json::Null) => true,
                Some(Json::Arr(allowed)) => {
                    assert!(
                        allowed.is_empty(),
                        "{name} names specific registries ({allowed:?}); this test only knows \
                         `publish = false` and `publish = true`, and a private registry is a \
                         decision that should not arrive silently"
                    );
                    false
                }
                Some(other) => panic!("{name} has an unreadable `publish` value: {other:?}"),
            };
            (name, may)
        })
        .collect()
}

/// **One crate may publish, by name, and every other member may not.**
///
/// Both directions in one test on purpose. A workspace where nothing could
/// publish would satisfy "gunnar-server may not" perfectly and would also have
/// broken the one release this repository actually makes — and the day somebody
/// silences THAT by flipping the workspace default back, this fails on the
/// other half rather than going quiet.
#[test]
fn only_the_declared_members_may_reach_a_public_registry() {
    let members = members();
    assert!(
        members.len() >= 15,
        "cargo metadata reported only {} members; this workspace has more, so the scan is \
         not seeing the crates it is about",
        members.len()
    );

    let mut publishable: Vec<&str> = members
        .iter()
        .filter(|(_, may)| *may)
        .map(|(name, _)| name.as_str())
        .collect();
    publishable.sort_unstable();
    let mut expected: Vec<&str> = THE_CHAIN.to_vec();
    expected.push(THE_EXCEPTION);
    expected.sort_unstable();
    assert_eq!(
        publishable, expected,
        "the set of publishable crates is not exactly the `cargo install gunnar` chain plus \
         {THE_EXCEPTION}. A crate that can publish and should not is one `cargo publish -p` \
         from being on crates.io permanently; a chain crate that cannot publish breaks \
         `cargo install gunnar`"
    );

    // `gunnar-cli` left this check on 2026-10-30 (GUN-PRODUCTS): it is a product now.
    let (_, may) = members
        .iter()
        .find(|(member, _)| member == "xtask")
        .unwrap_or_else(|| panic!("xtask is not a workspace member any more"));
    assert!(!may, "xtask may be published to a public registry");
    // The two DETACHED workspaces (2026-10-22) are outside `cargo metadata`'s view
    // of the root, so their own manifests are read: each must say `publish = false`.
    for detached in ["crates/gunnar-front", "crates/gunnar-monetize-guest"] {
        let text = std::fs::read_to_string(workspace_root().join(detached).join("Cargo.toml"))
            .unwrap_or_else(|e| panic!("{detached}/Cargo.toml: {e}"));
        assert!(
            text.lines().any(|l| l.trim() == "publish = false"),
            "{detached} may be published to a public registry"
        );
    }
}