Skip to main content

ossctl_core/release/adapters/
homebrew.rs

1//! Homebrew distribution adapter: `homebrew-tap` and `homebrew-core`.
2//!
3//! Updates a Homebrew formula (a custom tap, or a `homebrew-core` bump PR) so
4//! `brew install` resolves the new version. A tap/core is not observable through
5//! the [`RegistryQuery`](crate::ports::RegistryQuery) port, so `verify` returns
6//! [`VerifyOutcome::Unknown`] **explicitly** rather than being excused from the
7//! contract (ADR-0002 §1) — an honest "cannot check", never a false `Missing`.
8
9use std::time::Duration;
10
11use crate::contract::schema::Adapter;
12use crate::protocol::release::{
13    BuildArtifacts, DryRunReport, PlannedCommand, PublishReceipt, VerifyOutcome,
14};
15
16use super::{make_receipt, run_all, AdapterError, AdapterTarget, EffectCtx, ReleaseAdapter};
17
18/// The homebrew distribution adapter, operating as `homebrew-tap` or
19/// `homebrew-core`.
20pub struct HomebrewAdapter {
21    adapter: Adapter,
22}
23
24impl HomebrewAdapter {
25    /// Construct for a resolved homebrew adapter identity.
26    #[must_use]
27    pub fn new(adapter: Adapter) -> Self {
28        debug_assert!(matches!(
29            adapter,
30            Adapter::HomebrewTap | Adapter::HomebrewCore
31        ));
32        Self { adapter }
33    }
34}
35
36impl ReleaseAdapter for HomebrewAdapter {
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        Ok(DryRunReport {
47            adapter: self.adapter,
48            planned_commands: vec![PlannedCommand::new(
49                "brew",
50                &["audit", "--strict", &t.package],
51            )],
52            notes: vec![
53                "a formula is a downstream bump of an already-published artifact; \
54                 there is no separate build step"
55                    .to_string(),
56            ],
57        })
58    }
59
60    fn build(
61        &self,
62        _ctx: &EffectCtx<'_>,
63        _t: &AdapterTarget,
64    ) -> Result<BuildArtifacts, AdapterError> {
65        // Homebrew has no build phase of its own — it repackages an existing
66        // release artifact. Return an empty manifest rather than shelling out.
67        Ok(BuildArtifacts {
68            adapter: self.adapter,
69            artifacts: vec![],
70            notes: vec!["homebrew has no build phase (formula update only)".to_string()],
71        })
72    }
73
74    fn publish(
75        &self,
76        ctx: &EffectCtx<'_>,
77        t: &AdapterTarget,
78    ) -> Result<PublishReceipt, AdapterError> {
79        // PER-TARGET IRREVERSIBLE (opens/merges a formula bump).
80        // `bump-formula-pr` carries the release tarball URL threaded in via
81        // `ctx.artifacts.source_tarball` (the coordinator resolves the URL from the
82        // `origin` remote). `sha256` is currently `None` — the coordinator cannot
83        // produce a correct digest before the tag exists (see its `source_tarball`
84        // docs), so `--sha256` is omitted and `brew` derives it from `--url`. When
85        // a digest is present it is passed through. Options precede the formula name.
86        let mut args: Vec<&str> = match self.adapter {
87            Adapter::HomebrewCore => vec!["bump-formula-pr", "--no-fork"],
88            _ => vec!["bump-formula-pr"],
89        };
90        if let Some(tarball) = &ctx.artifacts.source_tarball {
91            args.push("--url");
92            args.push(tarball.url.as_str());
93            if let Some(sha256) = &tarball.sha256 {
94                args.push("--sha256");
95                args.push(sha256.as_str());
96            }
97        }
98        // `--` terminates options so a formula name is never parsed as a flag.
99        args.push("--");
100        args.push(t.package.as_str());
101        run_all(ctx, &[PlannedCommand::new("brew", &args)])?;
102        Ok(make_receipt(ctx, t, None, None))
103    }
104
105    fn verify(
106        &self,
107        _ctx: &EffectCtx<'_>,
108        _receipt: &PublishReceipt,
109    ) -> Result<VerifyOutcome, AdapterError> {
110        // A tap/core formula is not observable through RegistryQuery; report the
111        // honest "cannot check" rather than a false Missing (ADR-0002 §1).
112        Ok(VerifyOutcome::Unknown)
113    }
114
115    fn timeout(&self) -> Duration {
116        Duration::from_secs(600)
117    }
118}