huskies: merge 1072
This commit is contained in:
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user