feat(release): stage GitWorkshop and next node updates

This commit is contained in:
archipelago
2026-09-09 18:15:21 -04:00
parent 973356df16
commit f5c0ba85cd
97 changed files with 5716 additions and 1327 deletions
+16 -10
View File
@@ -48,7 +48,12 @@ podman_rootless() {
port_is_listening() {
local port="$1"
ss -ltn 2>/dev/null | awk '{print $4}' | grep -Eq "(^|:)$port$"
local protocol="${2:-tcp}"
case "$protocol" in
tcp) ss -ltn 2>/dev/null ;;
udp) ss -lun 2>/dev/null ;;
*) return 1 ;;
esac | awk '{print $4}' | grep -Eq "(^|:)$port$"
}
run_fix() {
@@ -564,18 +569,19 @@ fix_missing_rootless_ports() {
local fixed=false
local name
for name in $containers; do
local ports
ports=$(podman_rootless inspect "$name" --format '{{range $p,$bindings := .NetworkSettings.Ports}}{{if $bindings}}{{range $bindings}}{{.HostPort}}{{"\n"}}{{end}}{{end}}{{end}}' 2>/dev/null | sort -u)
[ -n "$ports" ] || continue
local bindings
bindings=$(podman_rootless inspect "$name" --format '{{range $p,$bindings := .NetworkSettings.Ports}}{{if $bindings}}{{range $bindings}}{{printf "%s %s\n" $p .HostPort}}{{end}}{{end}}{{end}}' 2>/dev/null | sort -u)
[ -n "$bindings" ] || continue
local missing=()
local port
for port in $ports; do
[ -n "$port" ] || continue
if ! port_is_listening "$port"; then
missing+=("$port")
local container_binding host_port protocol
while read -r container_binding host_port; do
[ -n "$container_binding" ] && [ -n "$host_port" ] || continue
protocol="${container_binding##*/}"
if ! port_is_listening "$host_port" "$protocol"; then
missing+=("$host_port/$protocol")
fi
done
done <<< "$bindings"
if [ ${#missing[@]} -gt 0 ]; then
log "Restarting $name: missing rootlessport listener(s): ${missing[*]}"
+1 -1
View File
@@ -214,7 +214,7 @@ run_smoke_tests() {
# Test 8: Container doctor runs cleanly
TESTS=$((TESTS + 1))
local doctor_exit
ssh $SSH_OPTS "$SSH_HOST" "sudo /home/archipelago/archy/scripts/container-doctor.sh --local 2>&1 | tail -1"
ssh $SSH_OPTS "$SSH_HOST" "sudo /opt/archipelago/scripts/container-doctor.sh --local 2>&1 | tail -1"
doctor_exit=$?
if [ $doctor_exit -eq 0 ]; then
pass "container-doctor.sh: clean exit"
+38 -3
View File
@@ -18,7 +18,7 @@ from typing import Any
import yaml
SYNC_FIELDS = ("title", "version", "description", "dockerImage", "category", "tier", "icon", "repoUrl")
SYNC_FIELDS = ("title", "version", "description", "dockerImage", "category", "tier", "icon", "repoUrl", "maintainerNpub")
def load_manifests(apps_dir: Path) -> dict[str, dict[str, Any]]:
@@ -43,15 +43,20 @@ def metadata(app: dict[str, Any]) -> dict[str, Any]:
def manifest_catalog_values(app: dict[str, Any]) -> dict[str, str]:
meta = metadata(app)
container = app.get("container") if isinstance(app.get("container"), dict) else {}
build = container.get("build") if isinstance(container.get("build"), dict) else {}
values = {
"title": app.get("name"),
"version": app.get("version"),
"description": app.get("description"),
"dockerImage": container.get("image"),
# The install RPC still requires an image-shaped identifier before it
# dispatches to the manifest orchestrator. For build-source apps this
# is the local tag the orchestrator creates, not a registry image.
"dockerImage": container.get("image") or build.get("tag"),
"category": app.get("category") or meta.get("category"),
"tier": meta.get("tier"),
"icon": meta.get("icon"),
"repoUrl": meta.get("repo") or meta.get("repoUrl") or meta.get("source"),
"maintainerNpub": meta.get("maintainer_npub"),
}
return {key: str(value) for key, value in values.items() if value is not None and str(value).strip()}
@@ -98,6 +103,19 @@ def manifest_opens_in_new_tab(app: dict[str, Any]) -> bool:
return launch.get("open_in_new_tab") is True
def manifest_requires_host_frame(app: dict[str, Any]) -> bool:
"""Return whether an app must remain inside the dashboard document.
The Android companion normally promotes app sessions to its native browser
overlay. Apps that consume a parent-frame integration (for example the
NIP-07 signer bridge) must stay embedded instead.
"""
launch = metadata(app).get("launch")
if not isinstance(launch, dict):
return False
return launch.get("requires_host_frame") is True
def ts_string(value: str) -> str:
return json.dumps(value, ensure_ascii=True)
@@ -106,6 +124,7 @@ def render_app_session_config(manifests: dict[str, dict[str, Any]]) -> str:
ports: dict[str, int] = {}
titles: dict[str, str] = {}
new_tab_apps: list[str] = []
host_frame_apps: list[str] = []
for app_id, app in sorted(manifests.items()):
name = app.get("name")
if isinstance(name, str) and name.strip():
@@ -113,8 +132,17 @@ def render_app_session_config(manifests: dict[str, dict[str, Any]]) -> str:
port = manifest_launch_port(app)
if port:
ports[app_id] = port
if manifest_opens_in_new_tab(app):
opens_in_new_tab = manifest_opens_in_new_tab(app)
requires_host_frame = manifest_requires_host_frame(app)
if opens_in_new_tab and requires_host_frame:
raise ValueError(
f"{app_id}: metadata.launch.open_in_new_tab and "
"requires_host_frame cannot both be true"
)
if opens_in_new_tab:
new_tab_apps.append(app_id)
if requires_host_frame:
host_frame_apps.append(app_id)
lines = [
"/** Generated by scripts/generate-app-catalog.py. Do not edit manually. */",
@@ -137,6 +165,13 @@ def render_app_session_config(manifests: dict[str, dict[str, Any]]) -> str:
])
for app_id in new_tab_apps:
lines.append(f" {ts_string(app_id)},")
lines.extend([
"])",
"",
"export const GENERATED_HOST_FRAME_APPS = new Set<string>([",
])
for app_id in host_frame_apps:
lines.append(f" {ts_string(app_id)},")
lines.extend(["])", ""])
return "\n".join(lines)
+13
View File
@@ -36,6 +36,7 @@ source "$ROOT/scripts/image-versions.sh"
set +a
UPDATED="$(date -u +%Y-%m-%d)" OUT="$OUT" APPS_DIR="$ROOT/apps" \
PUBLIC_CATALOG="$ROOT/app-catalog/catalog.json" \
EMBED_MANIFESTS="${EMBED_MANIFESTS:-1}" python3 - <<'PY'
import glob
import json, os
@@ -263,6 +264,18 @@ catalog = {
"apps": dict(sorted(apps.items())),
}
# Storefront composition is release data, not node-OS UI code. Copy it from
# the public catalog into the signed registry artifact so popular ordering and
# promotions can change independently after the fleet supports this schema.
# The legacy `featured` block remains for older dashboards.
public_catalog_path = os.environ.get("PUBLIC_CATALOG")
if public_catalog_path:
with open(public_catalog_path, encoding="utf-8") as fh:
public_catalog = json.load(fh)
for key in ("featured", "storefront"):
if key in public_catalog:
catalog[key] = public_catalog[key]
with open(os.environ["OUT"], "w") as f:
json.dump(catalog, f, indent=2)
f.write("\n")
+18
View File
@@ -25,9 +25,27 @@ location /app/uptime-kuma/ {
proxy_hide_header X-Frame-Options;
proxy_hide_header Content-Security-Policy;
}
# GitWorkshop follows the dashboard origin; the app gate keeps the route
# session-authenticated before it reaches the loopback-only container.
location /app/archipelago-source/ {
proxy_pass http://127.0.0.2:8337/;
proxy_http_version 1.1;
proxy_set_header Host $http_host;
proxy_set_header Cookie $http_cookie;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-Prefix /app/archipelago-source;
proxy_hide_header X-Frame-Options;
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-Content-Type-Options "nosniff" always;
proxy_read_timeout 300s;
}
location /app/gitea/ {
proxy_pass http://127.0.0.1:3001/;
proxy_http_version 1.1;
proxy_request_buffering off;
client_max_body_size 10G;
proxy_set_header Host $http_host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
+29 -5
View File
@@ -38,6 +38,20 @@ ok() { echo -e "${GREEN}[$(date '+%H:%M:%S')] OK${NC} $*" | tee -a "$LOG_FILE";
err() { echo -e "${RED}[$(date '+%H:%M:%S')] ERROR${NC} $*" | tee -a "$LOG_FILE"; }
warn(){ echo -e "${YELLOW}[$(date '+%H:%M:%S')] WARN${NC} $*" | tee -a "$LOG_FILE"; }
ensure_ngit_runtime() {
local installer="$REPO_DIR/image-recipe/configs/install-ngit.sh"
if [ ! -f "$installer" ]; then
warn "Pinned ngit installer missing; Nostr source cloning remains unavailable"
return 0
fi
log "Checking pinned ngit source-contribution runtime..."
if sudo sh "$installer" >>"$LOG_FILE" 2>&1; then
ok "ngit source-contribution runtime ready"
else
warn "Unable to install ngit; HTTP source access remains available"
fi
}
cleanup() {
rm -f "$LOCK_FILE"
}
@@ -142,6 +156,13 @@ for pkg in python3-venv binutils libpython3.13; do
fi
done
# Retry the pinned runtime on already-current nodes without making them rebuild
# the backend. Nodes updating from an older checkout run it again after pull,
# when the installer first becomes available.
if [ -f "$REPO_DIR/image-recipe/configs/install-ngit.sh" ]; then
ensure_ngit_runtime
fi
# Fetch latest
log "Fetching from origin..."
git fetch origin main --quiet 2>>"$LOG_FILE"
@@ -188,6 +209,8 @@ git pull origin main --ff-only 2>>"$LOG_FILE" || {
exit 1
}
ensure_ngit_runtime
NEW_VERSION=$(git rev-parse --short HEAD)
log "Now at: $NEW_VERSION"
@@ -321,17 +344,18 @@ fi
UI_DOCKER_DEST="/opt/archipelago/docker"
sudo mkdir -p "$UI_DOCKER_DEST"
UI_REBUILD_LIST=""
# fips-ui and fedimint-ui are synced but NOT added to UI_REBUILD_LIST below:
# container-specs.sh has no spec for either (and their container names break
# fips-ui, fedimint-ui, and archipelago-source are synced but NOT added to
# UI_REBUILD_LIST below:
# container-specs.sh has no spec for these apps (and their container names break
# the archy-<ui> assumption — the FIPS one is plain `fips-ui`). Their rebuilds
# come from elsewhere — the daemon's companion installer for fedimint-ui, the
# orchestrator's build context for fips-ui — but BOTH read
# orchestrator's build context for fips-ui and archipelago-source — but all read
# /opt/archipelago/docker/<ui>, and nothing was ever updating that directory.
# So source edits to those two trees reached nodes through no path at all:
# So source edits to these trees previously reached nodes through no path at all:
# their nginx kept listening on 0.0.0.0 and served the Guardian and FIPS
# screens unauthenticated on every interface (found by scanning a test node
# from outside, 2026-08-05 — the in-node audit could not see them).
for ui in bitcoin-ui lnd-ui electrs-ui fips-ui fedimint-ui; do
for ui in bitcoin-ui lnd-ui electrs-ui fips-ui fedimint-ui archipelago-source; do
src="$REPO_DIR/docker/$ui"
dst="$UI_DOCKER_DEST/$ui"
[ -d "$src" ] || continue