From ce3e64e2d50504966ef120b87427eba3f9f7c7d6 Mon Sep 17 00:00:00 2001 From: Dorian Date: Wed, 1 Apr 2026 16:18:58 +0100 Subject: [PATCH] =?UTF-8?q?feat:=20complete=20OS=20update=20pipeline=20?= =?UTF-8?q?=E2=80=94=20extraction,=20notifications,=20CI=20publishing?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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) --- .gitea/workflows/build-iso-dev.yml | 79 ++++++++++++++++++++++++++++++ core/archipelago/src/server.rs | 12 ++++- core/archipelago/src/update.rs | 59 +++++++++++++++------- 3 files changed, 129 insertions(+), 21 deletions(-) diff --git a/.gitea/workflows/build-iso-dev.yml b/.gitea/workflows/build-iso-dev.yml index 3be3b212..14186319 100644 --- a/.gitea/workflows/build-iso-dev.yml +++ b/.gitea/workflows/build-iso-dev.yml @@ -143,6 +143,85 @@ jobs: echo "SHA256: $(sha256sum "$DEST" | cut -d' ' -f1)" fi + - name: Publish release artifacts and manifest + run: | + VERSION=$(grep '^version' core/archipelago/Cargo.toml | head -1 | sed 's/.*"\(.*\)".*/\1/') + DATE=$(date +%Y-%m-%d) + RELEASE_DIR="/var/lib/archipelago/filebrowser/Builds/releases/v${VERSION}" + sudo mkdir -p "$RELEASE_DIR" + + # Copy backend binary + BINARY="core/target/release/archipelago" + if [ -f "$BINARY" ]; then + sudo cp "$BINARY" "$RELEASE_DIR/archipelago" + sudo chmod 755 "$RELEASE_DIR/archipelago" + echo "Backend: $(du -h "$RELEASE_DIR/archipelago" | cut -f1)" + fi + + # Create frontend archive + if [ -d "web/dist/neode-ui" ]; then + FRONTEND_ARCHIVE="$RELEASE_DIR/archipelago-frontend-${VERSION}.tar.gz" + sudo tar -czf "$FRONTEND_ARCHIVE" -C web/dist neode-ui + echo "Frontend: $(du -h "$FRONTEND_ARCHIVE" | cut -f1)" + fi + + # Generate manifest with SHA256 hashes + BACKEND_HASH=$(sha256sum "$RELEASE_DIR/archipelago" 2>/dev/null | awk '{print $1}') + BACKEND_SIZE=$(stat -c%s "$RELEASE_DIR/archipelago" 2>/dev/null || echo 0) + FRONTEND_NAME="archipelago-frontend-${VERSION}.tar.gz" + FRONTEND_HASH=$(sha256sum "$RELEASE_DIR/$FRONTEND_NAME" 2>/dev/null | awk '{print $1}') + FRONTEND_SIZE=$(stat -c%s "$RELEASE_DIR/$FRONTEND_NAME" 2>/dev/null || echo 0) + + # Build download base URL (FileBrowser serves from /Builds/) + HOST=$(hostname -I 2>/dev/null | awk '{print $1}') + BASE_URL="http://${HOST:-192.168.1.228}:8083/Builds/releases/v${VERSION}" + + # Get changelog from recent commits + CHANGELOG=$(git log --oneline -10 --format='%s' | python3 -c " + import sys, json + lines = [l.strip() for l in sys.stdin if l.strip()] + print(json.dumps(lines[:10])) + " 2>/dev/null || echo '["Update to version '"$VERSION"'"]') + + python3 -c " + import json + manifest = { + 'version': '$VERSION', + 'release_date': '$DATE', + 'changelog': $CHANGELOG, + 'components': [] + } + if '$BACKEND_HASH': + manifest['components'].append({ + 'name': 'archipelago', + 'current_version': '$VERSION', + 'new_version': '$VERSION', + 'download_url': '$BASE_URL/archipelago', + 'sha256': '$BACKEND_HASH', + 'size_bytes': $BACKEND_SIZE + }) + if '$FRONTEND_HASH': + manifest['components'].append({ + 'name': '$FRONTEND_NAME', + 'current_version': '$VERSION', + 'new_version': '$VERSION', + 'download_url': '$BASE_URL/$FRONTEND_NAME', + 'sha256': '$FRONTEND_HASH', + 'size_bytes': $FRONTEND_SIZE + }) + print(json.dumps(manifest, indent=2)) + " | sudo tee "$RELEASE_DIR/manifest.json" > /dev/null + + # Also copy manifest to repo releases/ dir for git-based serving + cp "$RELEASE_DIR/manifest.json" releases/manifest.json 2>/dev/null || true + + sudo chown -R 1000:1000 "$RELEASE_DIR" + echo "" + echo "Release manifest:" + cat "$RELEASE_DIR/manifest.json" + echo "" + echo "Artifacts published to: $RELEASE_DIR" + - name: Build report if: always() continue-on-error: true diff --git a/core/archipelago/src/server.rs b/core/archipelago/src/server.rs index 9180c90f..ab90355e 100644 --- a/core/archipelago/src/server.rs +++ b/core/archipelago/src/server.rs @@ -471,7 +471,14 @@ async fn scan_and_update_packages( let tor_changed = tor_addr != current_data.server_info.tor_address; let first_scan = !current_data.server_info.status_info.containers_scanned; - if packages_changed || tor_changed || first_scan { + // Check if update scheduler has found an available update + let update_available = crate::update::load_state(std::path::Path::new("/var/lib/archipelago")) + .await + .map(|s| s.available_update.is_some()) + .unwrap_or(false); + let update_changed = update_available != current_data.server_info.status_info.updated; + + if packages_changed || tor_changed || first_scan || update_changed { let mut data = current_data; if !packages.is_empty() { data.package_data = packages; @@ -479,8 +486,9 @@ async fn scan_and_update_packages( data.server_info.tor_address = tor_addr.clone(); data.server_info.node_address = tor_addr.as_ref().map(|t| identity.node_address(t)); data.server_info.status_info.containers_scanned = true; + data.server_info.status_info.updated = update_available; state.update_data(data).await; - debug!("📦 State changed (packages={}, tor={}, first_scan={}), broadcasting update", packages_changed, tor_changed, first_scan); + debug!("📦 State changed (packages={}, tor={}, first_scan={}, update={}), broadcasting update", packages_changed, tor_changed, first_scan, update_changed); } Ok(()) diff --git a/core/archipelago/src/update.rs b/core/archipelago/src/update.rs index 63469418..96e75127 100644 --- a/core/archipelago/src/update.rs +++ b/core/archipelago/src/update.rs @@ -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"); } }