Skip to main content

max / makenotwork

5.1 KB · 154 lines History Blame Raw
1 //! Build orchestration: resolve a sha to a worktree, read the server version,
2 //! shell out to `cargo build --release`, record a `versions` row.
3 //!
4 //! Runs as a tokio task spawned from `POST /rebuild`; the HTTP request
5 //! returns the version id immediately and the task drives the rest.
6
7 use crate::config::Config;
8 use crate::deploy;
9 use crate::gates::{self, GateCtx};
10 use crate::git;
11 use crate::topology::Topology;
12 use anyhow::{Context, Result};
13 use chrono::Utc;
14 use sqlx::SqlitePool;
15 use std::path::{Path, PathBuf};
16 use std::sync::Arc;
17 use tokio::process::Command;
18
19 #[derive(Debug, Clone)]
20 pub struct BuildArtifact {
21 pub version: String,
22 pub git_sha: String,
23 pub worktree: PathBuf,
24 pub binary_path: PathBuf,
25 }
26
27 pub async fn run(
28 pool: SqlitePool,
29 cfg: Arc<Config>,
30 topo: Arc<Topology>,
31 sha: String,
32 ) -> Result<BuildArtifact> {
33 let worktree = cfg.workdir.join(&sha);
34 let bare = PathBuf::from(&topo.repo.bare_path);
35 git::checkout_worktree(&bare, &sha, &worktree).await?;
36
37 let server_dir = worktree.join("server");
38 let version = read_pkg_version(&server_dir.join("Cargo.toml")).await
39 .with_context(|| format!("reading version from {}/Cargo.toml", server_dir.display()))?;
40
41 tracing::info!(sha = %sha, version = %version, dir = %server_dir.display(), "cargo build --release start");
42 let started = std::time::Instant::now();
43 let out = Command::new("cargo")
44 .arg("build")
45 .arg("--release")
46 .current_dir(&server_dir)
47 .output()
48 .await
49 .context("spawning cargo build")?;
50 let elapsed_s = started.elapsed().as_secs();
51 if !out.status.success() {
52 tracing::error!(sha = %sha, version = %version, elapsed_s, "cargo build --release failed");
53 } else {
54 tracing::info!(sha = %sha, version = %version, elapsed_s, "cargo build --release ok");
55 }
56 anyhow::ensure!(
57 out.status.success(),
58 "cargo build --release failed:\n{}",
59 tail(&out.stderr, 4_000),
60 );
61
62 let binary_path = server_dir.join("target/release/server");
63 anyhow::ensure!(
64 binary_path.exists(),
65 "expected binary at {} after build",
66 binary_path.display(),
67 );
68
69 sqlx::query(
70 "INSERT OR IGNORE INTO versions (version, git_sha, built_at, artifact_path)
71 VALUES (?, ?, ?, ?)",
72 )
73 .bind(&version)
74 .bind(&sha)
75 .bind(Utc::now().to_rfc3339())
76 .bind(binary_path.to_string_lossy().as_ref())
77 .execute(&pool)
78 .await?;
79
80 Ok(BuildArtifact { version, git_sha: sha, worktree, binary_path })
81 }
82
83 /// Full MM-tier pipeline: build, deploy the binary into MM's release_root,
84 /// run MM's configured gates against the worktree, set tier_state.mm if all
85 /// pass. Errors propagate back to the spawned task and get logged.
86 pub async fn build_and_run_mm(
87 pool: SqlitePool,
88 cfg: Arc<Config>,
89 topo: Arc<Topology>,
90 sha: String,
91 ) -> Result<()> {
92 let art = run(pool.clone(), cfg.clone(), topo.clone(), sha).await?;
93
94 // Stage the binary in MM's release_root so future gates and the MM
95 // self-deploy point at a stable path, not the worktree's target/.
96 let mm_release_root = &cfg.release_root;
97 let staged = deploy::deploy_local(mm_release_root, &art.version, &art.binary_path).await?;
98 let staged_bin = staged.join("server");
99 sqlx::query("UPDATE versions SET artifact_path = ? WHERE version = ?")
100 .bind(staged_bin.to_string_lossy().as_ref())
101 .bind(&art.version)
102 .execute(&pool)
103 .await?;
104
105 // Find the MM tier's gate list. MM is conventionally named "mm".
106 let mm = topo.tiers.iter().find(|t| t.name == "mm")
107 .context("topology has no `mm` tier")?;
108
109 let ctx = GateCtx {
110 pool: pool.clone(),
111 cfg: cfg.clone(),
112 tier: "mm".to_string(),
113 version: art.version.clone(),
114 worktree: art.worktree.clone(),
115 };
116 let ok = gates::run_all(&ctx, &mm.gates).await?;
117
118 if ok {
119 let prev: Option<String> = sqlx::query_scalar(
120 "SELECT current_version FROM tier_state WHERE tier = 'mm'",
121 )
122 .fetch_optional(&pool).await?.flatten();
123 sqlx::query(
124 "UPDATE tier_state SET previous_version = ?, current_version = ?, burn_in_started_at = ?
125 WHERE tier = 'mm'",
126 )
127 .bind(prev)
128 .bind(&art.version)
129 .bind(Utc::now().to_rfc3339())
130 .execute(&pool)
131 .await?;
132 tracing::info!(version = %art.version, "MM pipeline green; ready to promote to next tier");
133 } else {
134 tracing::warn!(version = %art.version, "MM pipeline red; not advancing tier_state");
135 }
136 Ok(())
137 }
138
139 async fn read_pkg_version(cargo_toml: &Path) -> Result<String> {
140 let raw = tokio::fs::read_to_string(cargo_toml).await?;
141 let parsed: toml::Value = toml::from_str(&raw)?;
142 let v = parsed
143 .get("package")
144 .and_then(|p| p.get("version"))
145 .and_then(|v| v.as_str())
146 .context("package.version not found")?;
147 Ok(v.to_string())
148 }
149
150 fn tail(buf: &[u8], max: usize) -> String {
151 let s = String::from_utf8_lossy(buf);
152 if s.len() <= max { s.into_owned() } else { s[s.len() - max..].to_string() }
153 }
154