huskies: merge 1072

This commit is contained in:
dave
2026-05-15 01:21:38 +00:00
parent ae69cd50b1
commit 1506141155
4 changed files with 236 additions and 12 deletions
+87 -10
View File
@@ -217,7 +217,13 @@ async fn migrate_json_stores_to_sqlite(huskies_dir: &Path) {
}
/// Set up the server log file, node identity keypair, pipeline DB, and CRDT state.
pub(crate) async fn init_subsystems(app_state: &Arc<SessionState>, cwd: &Path) {
///
/// When `is_agent` is `true` the pipeline database is opened at an isolated
/// temporary path (or at `HUSKIES_DB_PATH` if that env-var is set) so that the
/// headless build agent never touches the production `.huskies/pipeline.db`.
/// This prevents feature-branch migrations from being applied to the shared
/// database and bricking the next server restart.
pub(crate) async fn init_subsystems(app_state: &Arc<SessionState>, cwd: &Path, is_agent: bool) {
// Enable persistent server log file now that the project root is known.
if let Some(ref root) = *app_state.project_root.lock().unwrap() {
let log_dir = root.join(".huskies").join("logs");
@@ -242,20 +248,91 @@ pub(crate) async fn init_subsystems(app_state: &Arc<SessionState>, cwd: &Path) {
}
}
// Initialise the SQLite pipeline shadow-write database and CRDT state layer.
// Clone the path out before the await so we don't hold the MutexGuard across
// an await point.
let pipeline_db_path = app_state
.project_root
.lock()
.unwrap()
.as_ref()
.map(|root| root.join(".huskies").join("pipeline.db"));
// Resolve the pipeline DB path.
//
// Priority order:
// 1. HUSKIES_DB_PATH env var (operator override, any mode)
// 2. Agent mode: process-local temp file so the production DB is never touched
// 3. Default: {project_root}/.huskies/pipeline.db
let pipeline_db_path: Option<PathBuf> = if let Ok(env_path) = std::env::var("HUSKIES_DB_PATH") {
let p = PathBuf::from(&env_path);
crate::slog!("[db] HUSKIES_DB_PATH override: {}", p.display());
Some(p)
} else if is_agent {
// Headless agent: use an isolated temp DB so that any migrations compiled
// into this binary (e.g. from a feature branch) are never applied to the
// production database. The temp file is process-unique and harmless to
// leave behind after the agent exits.
let pid = std::process::id();
let temp_path = std::env::temp_dir().join(format!("huskies-agent-{pid}.db"));
crate::slog!(
"[db] Agent mode: using isolated DB at {} (not touching production pipeline.db)",
temp_path.display()
);
Some(temp_path)
} else {
// Server mode: use the project-local production database.
app_state
.project_root
.lock()
.unwrap()
.as_ref()
.map(|root| root.join(".huskies").join("pipeline.db"))
};
if let Some(ref db_path) = pipeline_db_path {
if let Err(e) = db::init(db_path).await {
crate::slog!("[db] Failed to initialise pipeline.db: {e}");
} else {
// ── Migration drift self-check (server mode only) ─────────────────────
//
// In server mode, detect whether the live database contains migrations
// that were applied by a newer binary (e.g. a feature-branch agent that
// ran before the feature was merged). If so, log each unknown migration
// and exit with a clear actionable message. This is the root cause of
// the 2026-05-14 21:07 production outage where the server came up but
// the CRDT never initialised.
if !is_agent && let Some(pool) = db::get_shared_pool() {
let drift = db::check_schema_drift(pool).await;
if !drift.is_empty() {
for m in &drift {
crate::slog!(
"[db] UNKNOWN migration {} ('{}') applied at {} \
is not in the compiled-in set",
m.version,
m.description,
m.installed_on,
);
}
eprintln!();
eprintln!(
"error: pipeline.db contains {} migration(s) that are not \
recognised by this binary:",
drift.len()
);
for m in &drift {
eprintln!(
" \u{2022} migration {} ('{}') applied at {}",
m.version, m.description, m.installed_on
);
}
eprintln!();
eprintln!(
"This means the database was previously opened by a newer \
version of huskies."
);
eprintln!(
"To fix: rebuild huskies from the latest source (the branch \
that added these migrations) and restart."
);
eprintln!(
"Do NOT start the old binary against this database — it will \
behave incorrectly."
);
std::process::exit(1);
}
}
// One-shot migration: move any existing JSON store files into SQLite.
let huskies_dir = db_path.parent().unwrap_or(db_path);
migrate_json_stores_to_sqlite(huskies_dir).await;