Skip to main content

ossctl_core/release/adapters/
cargo.rs

1//! Rust ecosystem adapter: `cargo-publish` (crates.io) and `cargo-dist`.
2//!
3//! `cargo-publish` publishes a crate to crates.io via `cargo publish`.
4//! `cargo-dist` plans and builds distributable binaries locally (`dist`), but
5//! its *upload* is the CI release workflow — so its publish body is
6//! [`AdapterError::Unsupported`] from this host rather than a fabricated receipt
7//! for a build-only command. `verify` (for `cargo-publish`) reconciles against
8//! crates.io through [`RegistryQuery`](crate::ports::RegistryQuery) via the
9//! adapter's default path.
10
11use std::time::Duration;
12
13use crate::contract::schema::Adapter;
14use crate::protocol::release::{BuildArtifacts, DryRunReport, PlannedCommand, PublishReceipt};
15
16use super::{make_receipt, run_all, AdapterError, AdapterTarget, EffectCtx, ReleaseAdapter};
17
18/// The rust release adapter, operating as either `cargo-publish` or `cargo-dist`.
19pub struct CargoAdapter {
20    adapter: Adapter,
21}
22
23impl CargoAdapter {
24    /// Construct for a resolved rust adapter identity (`cargo-publish` /
25    /// `cargo-dist`).
26    #[must_use]
27    pub fn new(adapter: Adapter) -> Self {
28        debug_assert!(matches!(
29            adapter,
30            Adapter::CargoPublish | Adapter::CargoDist
31        ));
32        Self { adapter }
33    }
34}
35
36impl ReleaseAdapter for CargoAdapter {
37    fn adapter(&self) -> Adapter {
38        self.adapter
39    }
40
41    fn dry_run(
42        &self,
43        _ctx: &EffectCtx<'_>,
44        t: &AdapterTarget,
45    ) -> Result<DryRunReport, AdapterError> {
46        let planned_commands = match self.adapter {
47            Adapter::CargoDist => vec![PlannedCommand::new(
48                "dist",
49                &["plan", "--output-format=json"],
50            )],
51            _ => vec![PlannedCommand::new(
52                "cargo",
53                &["publish", "-p", &t.package, "--dry-run"],
54            )],
55        };
56        Ok(DryRunReport {
57            adapter: self.adapter,
58            planned_commands,
59            notes: vec![],
60        })
61    }
62
63    fn build(
64        &self,
65        ctx: &EffectCtx<'_>,
66        t: &AdapterTarget,
67    ) -> Result<BuildArtifacts, AdapterError> {
68        // `dist build` emits per-platform tarballs/installers, not a `.crate`;
69        // name the artifact set to match what each identity actually produces.
70        let (cmds, artifacts) = match self.adapter {
71            Adapter::CargoDist => (
72                vec![PlannedCommand::new("dist", &["build"])],
73                vec!["dist/".to_string()],
74            ),
75            _ => (
76                vec![PlannedCommand::new("cargo", &["package", "-p", &t.package])],
77                vec![format!("{}-{}.crate", t.package, t.version)],
78            ),
79        };
80        run_all(ctx, &cmds)?;
81        // SKELETON: a production build parses the exact packaged `.crate` /
82        // `dist-manifest.json` paths out of the command output; here we name the
83        // expected artifact set deterministically.
84        Ok(BuildArtifacts {
85            adapter: self.adapter,
86            artifacts,
87            notes: vec![],
88        })
89    }
90
91    fn publish(
92        &self,
93        ctx: &EffectCtx<'_>,
94        t: &AdapterTarget,
95    ) -> Result<PublishReceipt, AdapterError> {
96        // cargo-dist uploads via the CI release workflow, not from this host —
97        // `dist build` only builds. Report that honestly rather than returning a
98        // receipt for a publish that did not happen.
99        if matches!(self.adapter, Adapter::CargoDist) {
100            return Err(AdapterError::Unsupported {
101                adapter: self.adapter,
102                operation: "publish",
103            });
104        }
105        // PER-TARGET IRREVERSIBLE — drives the real `cargo publish` through the
106        // injected runner (the port is the safety seam under test). No
107        // `--no-verify`: a resume that enters publish without re-running build
108        // must still let cargo verify the package before it lands.
109        run_all(
110            ctx,
111            &[PlannedCommand::new("cargo", &["publish", "-p", &t.package])],
112        )?;
113        // SKELETON: a production publish parses the crates.io checksum from the
114        // `cargo publish` output for `digest`; the canonical URL is well-known.
115        let remote_url = Some(format!(
116            "https://crates.io/crates/{}/{}",
117            t.package, t.version
118        ));
119        Ok(make_receipt(ctx, t, None, remote_url))
120    }
121
122    fn timeout(&self) -> Duration {
123        Duration::from_secs(600)
124    }
125}