fix: harden OTA updates, AIUI desktop gap, LND no-proxy
- update.rs: post-OTA probe falls back to http://127.0.0.1/ on connect error (nginx binds :80, not :443) so good updates are no longer rolled back; recover stuck update_in_progress; avoid ETXTBSY on running binary - LND: REST client bypasses proxy, GET newaddress p2wkh, wallet readiness/unlock after restart - Dashboard.vue: chat route back to plain h-full (desktop bottom-gap fix) - vite.config.ts: dev-only /aiui proxy - tests/release/run.sh: release gate harness (static+frontend+backend) - CHANGELOG: v1.7.89-alpha notes Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
495b90782a
commit
c49e8fcacd
@ -2,9 +2,11 @@
|
||||
|
||||
## v1.7.89-alpha (2026-06-12)
|
||||
|
||||
- Restored the established AIUI mobile layout by hiding the Archipelago close pill and removing the newly added back button on phones.
|
||||
- Embedded AIUI once again receives `hideClose=true`, and its previous 16px mobile spacing allowance is restored without changing shared Mesh, dashboard, or app-session layouts.
|
||||
- Validation passed with `git diff --check`, `npm run type-check`, and the focused frontend route tests.
|
||||
- The AI assistant looks the way it always did again: no extra back button or close button on phones, and the desktop view fills the whole screen without a gap at the bottom.
|
||||
- System updates are much more reliable: updates that previously got stuck partway or failed to install now complete cleanly, and a failed update can no longer block all future updates.
|
||||
- After an update, the system now checks itself correctly on every node type, so working updates are no longer mistakenly undone.
|
||||
- Generating a Bitcoin receive address works again on nodes where a network proxy previously got in the way.
|
||||
- The Lightning wallet now recovers and unlocks itself properly after restarts.
|
||||
|
||||
## v1.7.88-alpha (2026-06-12)
|
||||
|
||||
|
||||
2
core/Cargo.lock
generated
2
core/Cargo.lock
generated
@ -80,7 +80,7 @@ checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61"
|
||||
|
||||
[[package]]
|
||||
name = "archipelago"
|
||||
version = "1.7.88-alpha"
|
||||
version = "1.7.89-alpha"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"archipelago-container",
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "archipelago"
|
||||
version = "1.7.88-alpha"
|
||||
version = "1.7.89-alpha"
|
||||
edition = "2021"
|
||||
description = "Archipelago Bitcoin Node OS - Native backend"
|
||||
authors = ["Archipelago Team"]
|
||||
|
||||
@ -38,6 +38,7 @@ impl RpcHandler {
|
||||
let macaroon_hex = hex::encode(&macaroon_bytes);
|
||||
|
||||
let client = reqwest::Client::builder()
|
||||
.no_proxy()
|
||||
.timeout(std::time::Duration::from_secs(10))
|
||||
.danger_accept_invalid_certs(true)
|
||||
.build()
|
||||
@ -180,6 +181,7 @@ impl RpcHandler {
|
||||
let macaroon_hex = hex::encode(&macaroon_bytes);
|
||||
|
||||
let client = reqwest::Client::builder()
|
||||
.no_proxy()
|
||||
.danger_accept_invalid_certs(true)
|
||||
.timeout(std::time::Duration::from_secs(10))
|
||||
.build()
|
||||
|
||||
@ -63,6 +63,7 @@ impl RpcHandler {
|
||||
let macaroon_bytes = read_lnd_admin_macaroon().await?;
|
||||
let macaroon_hex = hex::encode(&macaroon_bytes);
|
||||
let client = reqwest::Client::builder()
|
||||
.no_proxy()
|
||||
.timeout(std::time::Duration::from_secs(15))
|
||||
.danger_accept_invalid_certs(true)
|
||||
.build()
|
||||
|
||||
@ -530,6 +530,7 @@ impl RpcHandler {
|
||||
// Call LND REST API to initialize wallet with derived entropy.
|
||||
// LND must be running but NOT yet initialized (no existing wallet).
|
||||
let client = reqwest::Client::builder()
|
||||
.no_proxy()
|
||||
.timeout(std::time::Duration::from_secs(30))
|
||||
.danger_accept_invalid_certs(true)
|
||||
.build()
|
||||
|
||||
@ -76,7 +76,7 @@ pub async fn ensure_wallet_initialized() -> Result<()> {
|
||||
let admin_macaroon = "/var/lib/archipelago/lnd/data/chain/bitcoin/mainnet/admin.macaroon";
|
||||
let wallet_db = "/var/lib/archipelago/lnd/data/chain/bitcoin/mainnet/wallet.db";
|
||||
if file_exists_as_root(wallet_db).await {
|
||||
if file_exists_as_root(admin_macaroon).await {
|
||||
if file_exists_as_root(admin_macaroon).await && lnd_getinfo_ready(admin_macaroon).await {
|
||||
return Ok(());
|
||||
}
|
||||
unlock_existing_wallet().await?;
|
||||
@ -127,6 +127,7 @@ async fn unlock_existing_wallet() -> Result<()> {
|
||||
|
||||
async fn unlock_existing_wallet_via_rest() -> Result<()> {
|
||||
let client = reqwest::Client::builder()
|
||||
.no_proxy()
|
||||
.timeout(std::time::Duration::from_secs(20))
|
||||
.danger_accept_invalid_certs(true)
|
||||
.build()
|
||||
@ -204,6 +205,7 @@ struct InitWalletRequest {
|
||||
|
||||
async fn init_wallet_via_rest() -> Result<()> {
|
||||
let client = reqwest::Client::builder()
|
||||
.no_proxy()
|
||||
.timeout(std::time::Duration::from_secs(20))
|
||||
.danger_accept_invalid_certs(true)
|
||||
.build()
|
||||
@ -305,12 +307,12 @@ async fn decode_lnd_unlocker_response<T: for<'de> Deserialize<'de>>(
|
||||
anyhow::bail!("LND REST {path} returned {status}: {text}")
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
async fn lnd_getinfo_ready(admin_macaroon: &str) -> bool {
|
||||
let Ok(macaroon) = read_file_as_root(admin_macaroon).await else {
|
||||
return false;
|
||||
};
|
||||
let Ok(client) = reqwest::Client::builder()
|
||||
.no_proxy()
|
||||
.timeout(std::time::Duration::from_secs(5))
|
||||
.danger_accept_invalid_certs(true)
|
||||
.build()
|
||||
|
||||
@ -3220,11 +3220,11 @@ app:
|
||||
- /data/.filebrowser.json
|
||||
volumes:
|
||||
- type: bind
|
||||
source: /tmp/filebrowser-srv
|
||||
source: /var/lib/archipelago/filebrowser-srv
|
||||
target: /srv
|
||||
options: [rw]
|
||||
- type: bind
|
||||
source: /tmp/filebrowser-data
|
||||
source: /var/lib/archipelago/filebrowser-data
|
||||
target: /data
|
||||
options: [rw]
|
||||
"#;
|
||||
@ -3244,7 +3244,7 @@ app:
|
||||
secret_file: bitcoin-rpc-password
|
||||
volumes:
|
||||
- type: bind
|
||||
source: /tmp/lnd
|
||||
source: /var/lib/archipelago/lnd
|
||||
target: /root/.lnd
|
||||
"#;
|
||||
AppManifest::parse(yaml).unwrap()
|
||||
|
||||
@ -367,12 +367,20 @@ async fn probe_frontend_once() -> Result<()> {
|
||||
.context("build probe client")?;
|
||||
// Prefer HTTPS since that's the failure mode we're catching (nginx
|
||||
// 500 on the PWA). HTTP usually redirects to HTTPS and would mask
|
||||
// the bug.
|
||||
let resp = client
|
||||
.get("https://127.0.0.1/")
|
||||
.send()
|
||||
.await
|
||||
.context("probe GET https://127.0.0.1/")?;
|
||||
// the bug. BUT not every node binds 443 on loopback (.116 serves
|
||||
// plain HTTP; 443 there belongs to tailscale) — on a *connect*
|
||||
// error, fall back to HTTP so a healthy node isn't "verified" into
|
||||
// a rollback. An HTTP error status stays fatal on whichever scheme
|
||||
// answered.
|
||||
let resp = match client.get("https://127.0.0.1/").send().await {
|
||||
Ok(resp) => resp,
|
||||
Err(e) if e.is_connect() => client
|
||||
.get("http://127.0.0.1/")
|
||||
.send()
|
||||
.await
|
||||
.context("probe GET http://127.0.0.1/ (https not bound on loopback)")?,
|
||||
Err(e) => return Err(e).context("probe GET https://127.0.0.1/"),
|
||||
};
|
||||
let status = resp.status();
|
||||
if status.is_success() || status.is_redirection() {
|
||||
return Ok(());
|
||||
@ -1078,6 +1086,17 @@ pub async fn apply_update(data_dir: &Path) -> Result<()> {
|
||||
let current_binary = Path::new("/usr/local/bin/archipelago");
|
||||
if current_binary.exists() {
|
||||
let backup_path = backup_dir.join("archipelago");
|
||||
// A leftover backup from an earlier rollback can be root-owned
|
||||
// (rollback used to chown it in place), and fs::copy O_TRUNCs the
|
||||
// existing file — EACCES as the service user, wedging every apply
|
||||
// (seen on .116, v1.7.86 OTA). Unlink first; the dir is
|
||||
// service-owned so unlink works even when the file isn't ours.
|
||||
if backup_path.exists() {
|
||||
if let Err(e) = fs::remove_file(&backup_path).await {
|
||||
tracing::warn!(error = %e, "unlink of stale binary backup failed, retrying via host_sudo");
|
||||
let _ = host_sudo(&["rm", "-f", &backup_path.to_string_lossy()]).await;
|
||||
}
|
||||
}
|
||||
fs::copy(current_binary, &backup_path)
|
||||
.await
|
||||
.context("Failed to backup current binary")?;
|
||||
@ -1415,21 +1434,38 @@ pub async fn rollback_update(data_dir: &Path) -> Result<()> {
|
||||
|
||||
let backup_binary = backup_dir.join("archipelago");
|
||||
if backup_binary.exists() {
|
||||
// Use host_sudo + mv so we escape the archipelago service's
|
||||
// ProtectSystem=strict mount namespace. A plain fs::copy or
|
||||
// `sudo cp` from inside the service hits EROFS on /usr/local/bin,
|
||||
// which would silently orphan the rollback — exactly the
|
||||
// opposite of what auto-rollback is for. Pattern matches
|
||||
// apply_update()'s binary swap above.
|
||||
// Same two namespace gotchas as apply_update()'s binary swap:
|
||||
// `cp` straight onto the running binary is O_TRUNC and fails
|
||||
// ETXTBSY (exit 1 — exactly what broke the .116 rollback), and
|
||||
// plain sudo inherits ProtectSystem=strict, so everything goes
|
||||
// through host_sudo. Copy to a temp name on the same filesystem,
|
||||
// fix ownership on the TEMP file (never the stored backup — an
|
||||
// in-place chown is what later wedged apply_update), then mv,
|
||||
// which is an atomic rename and tolerates a busy destination.
|
||||
let backup_str = backup_binary.to_string_lossy().to_string();
|
||||
let _ = host_sudo(&["chmod", "0755", &backup_str]).await;
|
||||
let _ = host_sudo(&["chown", "root:root", &backup_str]).await;
|
||||
let status = host_sudo(&["cp", &backup_str, "/usr/local/bin/archipelago"])
|
||||
let tmp = format!(
|
||||
"/usr/local/bin/.archipelago.rollback.{}",
|
||||
chrono::Utc::now().timestamp_millis()
|
||||
);
|
||||
let copy = host_sudo(&["cp", &backup_str, &tmp])
|
||||
.await
|
||||
.context("Failed to stage backup binary via host_sudo")?;
|
||||
if !copy.success() {
|
||||
anyhow::bail!(
|
||||
"cp backup binary to {} failed (exit {:?})",
|
||||
tmp,
|
||||
copy.code()
|
||||
);
|
||||
}
|
||||
let _ = host_sudo(&["chmod", "0755", &tmp]).await;
|
||||
let _ = host_sudo(&["chown", "root:root", &tmp]).await;
|
||||
let status = host_sudo(&["mv", &tmp, "/usr/local/bin/archipelago"])
|
||||
.await
|
||||
.context("Failed to restore backup binary via host_sudo")?;
|
||||
if !status.success() {
|
||||
let _ = host_sudo(&["rm", "-f", &tmp]).await;
|
||||
anyhow::bail!(
|
||||
"cp backup binary into /usr/local/bin failed (exit {:?})",
|
||||
"mv backup binary into /usr/local/bin failed (exit {:?})",
|
||||
status.code()
|
||||
);
|
||||
}
|
||||
|
||||
4
neode-ui/package-lock.json
generated
4
neode-ui/package-lock.json
generated
@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "neode-ui",
|
||||
"version": "1.7.88-alpha",
|
||||
"version": "1.7.89-alpha",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "neode-ui",
|
||||
"version": "1.7.88-alpha",
|
||||
"version": "1.7.89-alpha",
|
||||
"dependencies": {
|
||||
"@types/dompurify": "^3.0.5",
|
||||
"@vue-leaflet/vue-leaflet": "^0.10.1",
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "neode-ui",
|
||||
"private": true,
|
||||
"version": "1.7.88-alpha",
|
||||
"version": "1.7.89-alpha",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"start": "./start-dev.sh",
|
||||
|
||||
@ -86,8 +86,8 @@
|
||||
<div
|
||||
v-if="route.path === '/dashboard/chat' || route.path === '/dashboard/mesh'"
|
||||
:class="[
|
||||
'h-full dashboard-scroll-panel mobile-scroll-pad',
|
||||
route.path === '/dashboard/mesh' ? 'mesh-dashboard-panel' : '',
|
||||
'h-full',
|
||||
route.path === '/dashboard/mesh' ? 'dashboard-scroll-panel mobile-scroll-pad mesh-dashboard-panel' : '',
|
||||
mobileTabPaddingTop ? 'overflow-y-auto' : ''
|
||||
]"
|
||||
:style="{ paddingTop: mobileTabPaddingTop ? (mobileTabPaddingTop + 16) + 'px' : undefined }"
|
||||
|
||||
@ -150,6 +150,12 @@ export default defineConfig({
|
||||
changeOrigin: true,
|
||||
secure: false,
|
||||
},
|
||||
// Serve the node's deployed AIUI same-origin like production (set VITE_AIUI_URL=/aiui/)
|
||||
'/aiui': {
|
||||
target: process.env.AIUI_PROXY_TARGET || 'http://127.0.0.1:80',
|
||||
changeOrigin: true,
|
||||
secure: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
build: {
|
||||
|
||||
104
tests/release/run.sh
Executable file
104
tests/release/run.sh
Executable file
@ -0,0 +1,104 @@
|
||||
#!/bin/bash
|
||||
# Release gate harness — seed of the full-system test harness.
|
||||
#
|
||||
# Ties together the checks that already exist in this repo (catalog drift,
|
||||
# release manifest, lifecycle bats, vitest, cargo tests) plus live-node
|
||||
# smoke probes, so "is this release OK?" is one command instead of folklore.
|
||||
#
|
||||
# Usage:
|
||||
# tests/release/run.sh # static + frontend + backend stages
|
||||
# tests/release/run.sh --quick # static + frontend unit only
|
||||
# tests/release/run.sh --with-build # also production-build the frontend
|
||||
# # and verify the dist version changed
|
||||
# tests/release/run.sh --manifest # also validate releases/manifest.json
|
||||
# # (run AFTER create-release staged it)
|
||||
# tests/release/run.sh --live [URL] # also smoke-probe a running node
|
||||
# # (default http://127.0.0.1)
|
||||
#
|
||||
# Flags compose. Exits non-zero on the first failing stage.
|
||||
#
|
||||
# CAUTION (.116 and other dev nodes): full `cargo test -p archipelago` has
|
||||
# hung tool PTYs here before — every cargo invocation below is wrapped in
|
||||
# `timeout` and scoped to focused module filters.
|
||||
|
||||
set -u
|
||||
REPO="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
|
||||
cd "$REPO"
|
||||
|
||||
QUICK=0 WITH_BUILD=0 MANIFEST=0 LIVE=0 LIVE_URL="http://127.0.0.1"
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--quick) QUICK=1 ;;
|
||||
--with-build) WITH_BUILD=1 ;;
|
||||
--manifest) MANIFEST=1 ;;
|
||||
--live) LIVE=1; [[ "${2:-}" == http* ]] && { LIVE_URL="$2"; shift; } ;;
|
||||
*) echo "unknown flag: $1" >&2; exit 2 ;;
|
||||
esac
|
||||
shift
|
||||
done
|
||||
|
||||
PASS=() FAIL=()
|
||||
stage() { # stage <name> <cmd...>
|
||||
local name="$1"; shift
|
||||
echo
|
||||
echo "=== [$name] $*"
|
||||
if "$@"; then
|
||||
echo "=== [$name] PASS"
|
||||
PASS+=("$name")
|
||||
else
|
||||
echo "=== [$name] FAIL (exit $?)"
|
||||
FAIL+=("$name")
|
||||
summary 1
|
||||
fi
|
||||
}
|
||||
summary() {
|
||||
echo
|
||||
echo "──────── release gate summary ────────"
|
||||
printf 'PASS: %s\n' "${PASS[@]:-none}"
|
||||
[[ ${#FAIL[@]} -gt 0 ]] && printf 'FAIL: %s\n' "${FAIL[@]}"
|
||||
exit "${1:-0}"
|
||||
}
|
||||
|
||||
# ── Stage 1: static ──────────────────────────────────────────────────
|
||||
stage "git-diff-check" git diff --check
|
||||
stage "cargo-fmt" timeout 240 cargo fmt --manifest-path core/Cargo.toml --all --check
|
||||
stage "catalog-drift" python3 scripts/check-app-catalog-drift.py
|
||||
if [[ $MANIFEST -eq 1 ]]; then
|
||||
stage "release-manifest" scripts/check-release-manifest.sh
|
||||
fi
|
||||
|
||||
# ── Stage 2: frontend ────────────────────────────────────────────────
|
||||
stage "ui-type-check" bash -c 'cd neode-ui && npm run --silent type-check'
|
||||
stage "ui-unit-tests" bash -c 'cd neode-ui && npx vitest run --silent 2>&1 | tail -4; exit ${PIPESTATUS[0]}'
|
||||
|
||||
if [[ $WITH_BUILD -eq 1 ]]; then
|
||||
# npm run build can fail silently (vue-tsc EACCES burned us before) —
|
||||
# require the packaged output to actually contain the current version.
|
||||
VERSION=$(grep -m1 '^version' core/archipelago/Cargo.toml | cut -d'"' -f2)
|
||||
stage "ui-build" bash -c 'cd neode-ui && npm run build'
|
||||
stage "ui-dist-version" bash -c "grep -rqo '${VERSION}' web/dist/neode-ui/assets/*.js"
|
||||
fi
|
||||
|
||||
[[ $QUICK -eq 1 ]] && summary 0
|
||||
|
||||
# ── Stage 3: backend ─────────────────────────────────────────────────
|
||||
stage "cargo-check" timeout 580 cargo check --manifest-path core/Cargo.toml -p archipelago
|
||||
# Focused suites for the subsystems this release train touched:
|
||||
# update:: — OTA download/apply/rollback/probe (v1.7.89 hardening)
|
||||
# lnd — receive address + wallet readiness (v1.7.85–.89)
|
||||
# container::image_versions — image pinning / false-update detection
|
||||
# scanner — RAII in-flight guard (v1.7.84)
|
||||
# 1500s: the non-incremental test-profile compile alone takes ~9 min on the
|
||||
# .116 ThinkPad; 580s expires mid-compile (exit 124) before a single test runs.
|
||||
stage "cargo-test-weekly" timeout 1500 env CARGO_INCREMENTAL=0 \
|
||||
cargo test --manifest-path core/Cargo.toml -p archipelago -- \
|
||||
update:: lnd container::image_versions scanner
|
||||
|
||||
# ── Stage 4: live node smoke ─────────────────────────────────────────
|
||||
if [[ $LIVE -eq 1 ]]; then
|
||||
stage "live-frontend" bash -c "curl -skf -o /dev/null '$LIVE_URL/' || curl -skf -o /dev/null '${LIVE_URL/http:/https:}/'"
|
||||
stage "live-aiui" curl -sf -o /dev/null "$LIVE_URL/aiui/"
|
||||
stage "live-rpc" bash -c "curl -s -X POST '$LIVE_URL/rpc/v1' -H 'Content-Type: application/json' -d '{\"jsonrpc\":\"2.0\",\"id\":1,\"method\":\"update.status\",\"params\":{}}' | grep -qE '\"(result|error)\"'"
|
||||
fi
|
||||
|
||||
summary 0
|
||||
Loading…
x
Reference in New Issue
Block a user