feat: complete OS update pipeline — extraction, notifications, CI publishing
Some checks failed
Build Archipelago ISO / build-iso (push) Has been cancelled
Container Orchestration Tests / unit-tests (push) Has been cancelled
Container Orchestration Tests / smoke-tests (push) Has been cancelled
Build Archipelago ISO (dev) / build-iso (push) Has been cancelled

- update.rs: extract frontend .tar.gz archives during apply (was TODO/skip)
- update.rs: back up current frontend before extraction, set binary perms
- server.rs: periodic scan reads update_state.json, sets status_info.updated
  flag and broadcasts via WebSocket so frontend gets notified automatically
- build-iso-dev.yml: publish binary + frontend archive + manifest.json with
  SHA256 hashes to /Builds/releases/v{version}/ after each build

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Dorian
2026-04-01 16:18:58 +01:00
parent 919faf54ca
commit ce3e64e2d5
3 changed files with 129 additions and 21 deletions

View File

@@ -260,27 +260,48 @@ pub async fn apply_update(data_dir: &Path) -> Result<()> {
let name = entry.file_name().to_string_lossy().to_string();
let src = entry.path();
// Map component names to destinations
let dest = match name.as_str() {
"archipelago" => Some(Path::new("/usr/local/bin/archipelago").to_path_buf()),
_ => {
// For frontend or config files, determine destination
if name.ends_with(".tar.gz") || name.ends_with(".zip") {
// Archive — extract to appropriate location
debug!(name = %name, "Skipping archive (manual extraction needed)");
None
} else {
debug!(name = %name, "Unknown component, skipping");
None
match name.as_str() {
"archipelago" => {
let dest = Path::new("/usr/local/bin/archipelago");
fs::copy(&src, dest)
.await
.with_context(|| format!("Failed to apply {}", name))?;
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
std::fs::set_permissions(dest, std::fs::Permissions::from_mode(0o755))
.context("Failed to set binary permissions")?;
}
info!(name = %name, "Backend binary applied");
}
_ if name.contains("frontend") && name.ends_with(".tar.gz") => {
let web_ui_dir = Path::new("/opt/archipelago/web-ui");
// Back up current frontend
let frontend_backup = backup_dir.join("web-ui-backup.tar.gz");
if web_ui_dir.exists() {
let status = tokio::process::Command::new("tar")
.args(["-czf", &frontend_backup.to_string_lossy(), "-C", "/opt/archipelago", "web-ui"])
.status()
.await
.context("Failed to backup frontend")?;
if status.success() {
info!("Frontend backed up");
}
}
// Extract new frontend
let status = tokio::process::Command::new("tar")
.args(["-xzf", &src.to_string_lossy(), "-C", "/opt/archipelago"])
.status()
.await
.with_context(|| format!("Failed to extract {}", name))?;
if !status.success() {
anyhow::bail!("tar extraction failed for {}", name);
}
info!(name = %name, "Frontend archive extracted to /opt/archipelago/web-ui");
}
_ => {
debug!(name = %name, "Unknown component, skipping");
}
};
if let Some(dest_path) = dest {
fs::copy(&src, &dest_path)
.await
.with_context(|| format!("Failed to apply {}", name))?;
info!(name = %name, dest = %dest_path.display(), "Component applied");
}
}