feat(release): stage GitWorkshop and next node updates
This commit is contained in:
+16
-10
@@ -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[*]}"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user