Skip to main content

max / makenotwork

4.5 KB · 142 lines History Blame Raw
1 //! `bento-release-macos` — drive one macOS release through the in-session
2 //! `ops-agent` and pull the signed DMG back.
3 //!
4 //! Usage: `bento-release-macos --config driver.toml [--version 0.4.1]`
5 //!
6 //! This is the thin driver (launchplan §A decision (a)); the full bentod/TUI is
7 //! deferred. It is transport-agnostic via `ops-exec` — in production it points
8 //! at the Mac's `ops-agent` (`AgentRpc`), the only context where codesign can
9 //! use the Developer ID key.
10
11 use anyhow::{Context, Result};
12 use bento_driver::{ReleasePlan, StdoutSink, run_release};
13 use ops_exec::{AgentRpc, CapabilitySet, Executor};
14 use serde::Deserialize;
15 use std::path::PathBuf;
16
17 #[derive(Debug, Deserialize)]
18 struct DriverConfig {
19 agent: AgentSection,
20 plan: PlanSection,
21 /// Where on THIS host (fw13) to pull the signed DMG.
22 local: LocalSection,
23 }
24
25 #[derive(Debug, Deserialize)]
26 struct AgentSection {
27 /// e.g. `http://mbp.tailnet:8765`
28 base_url: String,
29 /// Audit label, e.g. `mbp`.
30 host_label: String,
31 }
32
33 #[derive(Debug, Deserialize)]
34 struct PlanSection {
35 app: String,
36 version: String,
37 repo_path: String,
38 env_file: String,
39 dist_root: String,
40 /// Optional; defaults to `<product>_<version>_aarch64.dmg`.
41 dmg_name: Option<String>,
42 /// Product name used for the default DMG name, e.g. `GoingsOn`.
43 product_name: String,
44 }
45
46 #[derive(Debug, Deserialize)]
47 struct LocalSection {
48 dest_dir: PathBuf,
49 }
50
51 #[tokio::main]
52 async fn main() -> Result<()> {
53 tracing_subscriber::fmt()
54 .with_env_filter(
55 tracing_subscriber::EnvFilter::try_from_default_env()
56 .unwrap_or_else(|_| tracing_subscriber::EnvFilter::new("info")),
57 )
58 .init();
59
60 let args = Args::parse()?;
61 let raw = std::fs::read_to_string(&args.config)
62 .with_context(|| format!("reading {}", args.config))?;
63 let mut cfg: DriverConfig = toml::from_str(&raw).context("parsing driver config")?;
64 if let Some(v) = args.version {
65 cfg.plan.version = v;
66 }
67
68 let dmg_name = cfg.plan.dmg_name.clone().unwrap_or_else(|| {
69 ReleasePlan::default_dmg_name(&cfg.plan.product_name, &cfg.plan.version)
70 });
71 let plan = ReleasePlan {
72 app: cfg.plan.app.clone(),
73 version: cfg.plan.version.clone(),
74 repo_path: cfg.plan.repo_path.clone(),
75 env_file: cfg.plan.env_file.clone(),
76 dist_root: cfg.plan.dist_root.clone(),
77 dmg_name: dmg_name.clone(),
78 };
79
80 // The driver's caller-side grant: it may build/sign/notarize/staple and
81 // observe the gatekeeper verdict. The agent re-checks against its own grant.
82 let exec = AgentRpc::new(
83 cfg.agent.base_url.clone(),
84 cfg.agent.host_label.clone(),
85 CapabilitySet::from_tokens(["build", "sign", "notarize", "staple"], ["gatekeeper"]),
86 );
87
88 // Fail fast if the agent isn't reachable / in-session.
89 let health = exec.health().await.context(
90 "agent /health failed — is ops-agent running in the Aqua session on the build host?",
91 )?;
92 tracing::info!(actuate = ?health.actuate, "agent reachable");
93
94 println!("==> releasing {} {} via {}", plan.app, plan.version, cfg.agent.host_label);
95 let mut sink = StdoutSink;
96 let outcome = run_release(&exec, &plan, &mut sink).await?;
97 if !outcome.gatekeeper_accepted {
98 anyhow::bail!(
99 "Gatekeeper did NOT accept {} — not pulling. Check the notarization log.",
100 outcome.dmg_remote
101 );
102 }
103 println!("\n==> Gatekeeper accepted; pulling DMG");
104
105 tokio::fs::create_dir_all(&cfg.local.dest_dir).await.ok();
106 let local_dmg = cfg.local.dest_dir.join(&dmg_name);
107 exec.pull(
108 std::path::Path::new(&outcome.dmg_remote),
109 &local_dmg,
110 &Default::default(),
111 )
112 .await
113 .context("pulling the signed DMG back")?;
114
115 println!("==> done: {}", local_dmg.display());
116 Ok(())
117 }
118
119 struct Args {
120 config: String,
121 version: Option<String>,
122 }
123
124 impl Args {
125 fn parse() -> Result<Self> {
126 let mut config = None;
127 let mut version = None;
128 let mut it = std::env::args().skip(1);
129 while let Some(a) = it.next() {
130 match a.as_str() {
131 "--config" | "-c" => config = it.next(),
132 "--version" | "-v" => version = it.next(),
133 other => anyhow::bail!("unexpected arg `{other}` (usage: --config <toml> [--version <ver>])"),
134 }
135 }
136 Ok(Self {
137 config: config.context("--config <driver.toml> is required")?,
138 version,
139 })
140 }
141 }
142