feat: complete OS update pipeline — extraction, notifications, CI publishing
- 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:
parent
919faf54ca
commit
ce3e64e2d5
@ -143,6 +143,85 @@ jobs:
|
|||||||
echo "SHA256: $(sha256sum "$DEST" | cut -d' ' -f1)"
|
echo "SHA256: $(sha256sum "$DEST" | cut -d' ' -f1)"
|
||||||
fi
|
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
|
- name: Build report
|
||||||
if: always()
|
if: always()
|
||||||
continue-on-error: true
|
continue-on-error: true
|
||||||
|
|||||||
@ -471,7 +471,14 @@ async fn scan_and_update_packages(
|
|||||||
let tor_changed = tor_addr != current_data.server_info.tor_address;
|
let tor_changed = tor_addr != current_data.server_info.tor_address;
|
||||||
let first_scan = !current_data.server_info.status_info.containers_scanned;
|
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;
|
let mut data = current_data;
|
||||||
if !packages.is_empty() {
|
if !packages.is_empty() {
|
||||||
data.package_data = packages;
|
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.tor_address = tor_addr.clone();
|
||||||
data.server_info.node_address = tor_addr.as_ref().map(|t| identity.node_address(t));
|
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.containers_scanned = true;
|
||||||
|
data.server_info.status_info.updated = update_available;
|
||||||
state.update_data(data).await;
|
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(())
|
Ok(())
|
||||||
|
|||||||
@ -260,27 +260,48 @@ pub async fn apply_update(data_dir: &Path) -> Result<()> {
|
|||||||
let name = entry.file_name().to_string_lossy().to_string();
|
let name = entry.file_name().to_string_lossy().to_string();
|
||||||
let src = entry.path();
|
let src = entry.path();
|
||||||
|
|
||||||
// Map component names to destinations
|
match name.as_str() {
|
||||||
let dest = match name.as_str() {
|
"archipelago" => {
|
||||||
"archipelago" => Some(Path::new("/usr/local/bin/archipelago").to_path_buf()),
|
let dest = Path::new("/usr/local/bin/archipelago");
|
||||||
_ => {
|
fs::copy(&src, dest)
|
||||||
// For frontend or config files, determine destination
|
.await
|
||||||
if name.ends_with(".tar.gz") || name.ends_with(".zip") {
|
.with_context(|| format!("Failed to apply {}", name))?;
|
||||||
// Archive — extract to appropriate location
|
#[cfg(unix)]
|
||||||
debug!(name = %name, "Skipping archive (manual extraction needed)");
|
{
|
||||||
None
|
use std::os::unix::fs::PermissionsExt;
|
||||||
} else {
|
std::fs::set_permissions(dest, std::fs::Permissions::from_mode(0o755))
|
||||||
debug!(name = %name, "Unknown component, skipping");
|
.context("Failed to set binary permissions")?;
|
||||||
None
|
|
||||||
}
|
}
|
||||||
|
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");
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user