From 4d1bd063e8e4c511b450c768bd245562bd73575f Mon Sep 17 00:00:00 2001 From: Dorian Date: Mon, 20 Apr 2026 12:33:10 -0400 Subject: [PATCH] release(v1.7.6-alpha): robust apply_update + manifest-override env var MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit apply_update frontend swap Transient EROFS on .198 (filesystem hiccup — root FS mounts with errors=remount-ro so a fleeting glitch can bounce /opt to RO for a moment) caught the pre-cleanup `rm -rf web-ui.new web-ui.bak` mid- stride and aborted the apply. Rewrote the swap to use a timestamped staging dir (web-ui.new.) and a timestamped old-copy path so nothing needs to be rm'd before the extract. After the new tree is mv'd into place, the previous rollback copy is rotated aside with a . suffix (best-effort) and this apply's old copy becomes the new web-ui.bak. If the final mv fails, the staged old is restored so nginx keeps serving. handle_update_check manifest override handle_update_check takes the git path whenever ~/archy/.git exists. On the dev box (.116) that meant the Pull & Rebuild button was always the only option even though the manifest-path OTA was already wired via ARCHIPELAGO_UPDATE_URL. Now: if that env var is set, we skip the git detection entirely and use the manifest path. The regular fleet (no env var, no repo) hits the manifest branch naturally; beta dev nodes (repo + no env var) still get Pull & Rebuild; dev nodes with the env var explicitly set can finally test the manifest OTA end-to-end. Artefacts: archipelago 356e78cc…91a6dd 40372288 archipelago-frontend-1.7.6-alpha.tar.gz 4fb79664…0172e9 76984615 (reused) Co-Authored-By: Claude Opus 4.7 (1M context) --- core/Cargo.lock | 2 +- core/archipelago/Cargo.toml | 2 +- core/archipelago/src/api/rpc/update.rs | 10 +++- core/archipelago/src/update.rs | 76 ++++++++++++++++++-------- 4 files changed, 63 insertions(+), 27 deletions(-) diff --git a/core/Cargo.lock b/core/Cargo.lock index a6699b34..2df3e3c1 100644 --- a/core/Cargo.lock +++ b/core/Cargo.lock @@ -80,7 +80,7 @@ checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61" [[package]] name = "archipelago" -version = "1.7.5-alpha" +version = "1.7.6-alpha" dependencies = [ "anyhow", "archipelago-container", diff --git a/core/archipelago/Cargo.toml b/core/archipelago/Cargo.toml index 30f2a017..0616123d 100644 --- a/core/archipelago/Cargo.toml +++ b/core/archipelago/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "archipelago" -version = "1.7.5-alpha" +version = "1.7.6-alpha" edition = "2021" description = "Archipelago Bitcoin Node OS - Native backend" authors = ["Archipelago Team"] diff --git a/core/archipelago/src/api/rpc/update.rs b/core/archipelago/src/api/rpc/update.rs index 0db8b0fd..277f27b6 100644 --- a/core/archipelago/src/api/rpc/update.rs +++ b/core/archipelago/src/api/rpc/update.rs @@ -6,12 +6,18 @@ impl RpcHandler { /// Check for available system updates. /// Tries git-based check first (if repo exists), falls back to manifest-based. pub(super) async fn handle_update_check(&self) -> Result { - // Try git-based check first (preferred for beta nodes) + // Manifest override: when ARCHIPELAGO_UPDATE_URL is explicitly set, + // the operator wants OTA via manifest — typically a dev box where + // ~/archy/.git exists but isn't the intended update surface. + // Without this short-circuit the dev box always advertises "Pull + // & Rebuild" and can never exercise the manifest OTA path. + let manifest_override = std::env::var("ARCHIPELAGO_UPDATE_URL").is_ok(); + let repo_dir = std::path::PathBuf::from( std::env::var("HOME").unwrap_or_else(|_| "/home/archipelago".to_string()), ) .join("archy"); - if repo_dir.join(".git").exists() { + if !manifest_override && repo_dir.join(".git").exists() { if let Ok(git_status) = self.git_check_update(&repo_dir).await { return Ok(git_status); } diff --git a/core/archipelago/src/update.rs b/core/archipelago/src/update.rs index c3e3cc2d..3cef79a0 100644 --- a/core/archipelago/src/update.rs +++ b/core/archipelago/src/update.rs @@ -305,45 +305,47 @@ pub async fn apply_update(data_dir: &Path) -> Result<()> { info!(name = %name, "Backend binary applied"); } _ if name.contains("frontend") && name.ends_with(".tar.gz") => { - // The tarball contents are the *inside* of web-ui/ — root - // entries are `./test-aiui.html`, `./assets/`, etc. Extract - // into a sibling staging dir, then swap atomically so - // nginx never sees a half-written tree. - let new_dir = "/opt/archipelago/web-ui.new"; + // Tarball contents are the *inside* of web-ui/ (root entries + // `./test-aiui.html`, `./assets/`, ...). Extract into a + // uniquely-named staging dir, then mv into place. No `rm + // -rf` pre-cleanup — that's what hit transient EROFS on + // .198 and aborted the apply mid-flight. + let ts = chrono::Utc::now().timestamp_millis(); + let staging_new = format!("/opt/archipelago/web-ui.new.{}", ts); + let staging_old = format!("/opt/archipelago/web-ui.old.{}", ts); + let web_ui = "/opt/archipelago/web-ui"; let backup_path = "/opt/archipelago/web-ui.bak"; - // Wipe any previous attempt's staging / backup dirs. - let _ = tokio::process::Command::new("sudo") - .args(["rm", "-rf", new_dir, backup_path]) - .status() - .await; + let mk = tokio::process::Command::new("sudo") - .args(["mkdir", "-p", new_dir]) + .args(["mkdir", "-p", &staging_new]) .status() .await .context("Failed to create frontend staging dir")?; if !mk.success() { - anyhow::bail!("mkdir {} failed", new_dir); + anyhow::bail!("mkdir {} failed", staging_new); } - // Extract INTO the staging dir — tar's ./ entries land at - // the right place (web-ui.new/assets/... etc.). - let status = tokio::process::Command::new("sudo") - .args(["tar", "-xzf", &src.to_string_lossy(), "-C", new_dir]) + let extract = tokio::process::Command::new("sudo") + .args(["tar", "-xzf", &src.to_string_lossy(), "-C", &staging_new]) .status() .await .with_context(|| format!("Failed to extract {}", name))?; - if !status.success() { + if !extract.success() { + // Best-effort cleanup of the partial extraction. + let _ = tokio::process::Command::new("sudo") + .args(["rm", "-rf", &staging_new]) + .status() + .await; anyhow::bail!("tar extraction failed for {}", name); } - // Ownership: match what first-boot + the ISO expect. let _ = tokio::process::Command::new("sudo") - .args(["chown", "-R", "archipelago:archipelago", new_dir]) + .args(["chown", "-R", "archipelago:archipelago", &staging_new]) .status() .await; - // Atomic-ish swap: move old aside, new into place. - let web_ui = "/opt/archipelago/web-ui"; + + // Swap: mv current web-ui aside, then mv new into place. if Path::new(web_ui).exists() { let mv_old = tokio::process::Command::new("sudo") - .args(["mv", web_ui, backup_path]) + .args(["mv", web_ui, &staging_old]) .status() .await .context("Failed to rotate old web-ui")?; @@ -352,13 +354,41 @@ pub async fn apply_update(data_dir: &Path) -> Result<()> { } } let mv_new = tokio::process::Command::new("sudo") - .args(["mv", new_dir, web_ui]) + .args(["mv", &staging_new, web_ui]) .status() .await .context("Failed to swap new web-ui into place")?; if !mv_new.success() { + // Roll back the rename so nginx keeps serving. + if Path::new(&staging_old).exists() { + let _ = tokio::process::Command::new("sudo") + .args(["mv", &staging_old, web_ui]) + .status() + .await; + } anyhow::bail!("failed to move new web-ui into place"); } + + // Rotate previous rollback aside (best-effort) and install + // this apply's old copy as the new rollback. + if Path::new(&staging_old).exists() { + if Path::new(backup_path).exists() { + // Tag the previous backup with its own ts so it + // doesn't collide; best-effort cleanup. + let _ = tokio::process::Command::new("sudo") + .args([ + "mv", + backup_path, + &format!("{}.{}", backup_path, ts), + ]) + .status() + .await; + } + let _ = tokio::process::Command::new("sudo") + .args(["mv", &staging_old, backup_path]) + .status() + .await; + } info!(name = %name, "Frontend archive extracted to /opt/archipelago/web-ui"); } _ => {