Demo images / Build & push demo images (push) Failing after 2m22s
Replaces the registry host across 86 files: 309 references, covering all 40 app manifests, the orchestrator and container crates, the release and catalog scripts, both demo-images workflows, the ISO builder, demo-deploy, and the frontend marketplace data. Verified the domain actually serves the registry before rewriting anything, rather than assuming the web host implies the registry: - TLS verifies clean, HTTP/2 on the web root - an anonymous token grants a manifest fetch (HTTP 200) with no credentials - skopeo inspect --no-creds resolves an image and lists its tags That last check is the one that matters: an outside developer with no account can now pull, which was the functional blocker for publishing at all. Plain-HTTP references become HTTPS in the same pass, so OTA downloads stop crossing the network in the clear. Deliberately NOT rewritten: - The public FIPS anchor on port 8444. It is a functional network endpoint every node dials to bootstrap the mesh — closer to Bitcoin Core's hardcoded seeds than to leaked infrastructure. The domain does resolve to the same host, so it could become a hostname, but that adds a DNS dependency to the path used precisely when things are broken. Worth a deliberate decision, not a side effect of this change. - The companion APK on port 2100. The domain returns 404 for that path, so rewriting it would swap a working URL for a broken one. The Releases page does serve (200), which is where the plan already wants those binaries. - releases/app-catalog.json, releases/manifest.json and release-manifest.json. These carry `signature` and `signed_by`; editing their contents invalidates the signature and the fleet refuses artifacts that fail verification. They were rewritten in a first pass and reverted — they must be regenerated and re-signed through the signing ceremony instead, which needs the mnemonic. So the catalog still advertises the old host until that ceremony runs. Nodes resolve images through the signed catalog, not the on-disk manifests, so this commit alone does not change what a node pulls. Verified: archipelago-container 75/75; every manifest still parses with a top-level app block; no signed artifact modified. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
243 lines
10 KiB
Bash
Executable File
243 lines
10 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
# Generate releases/app-catalog.json — the REMOTE per-app version catalog that
|
|
# decouples app updates from the binary OTA (see
|
|
# core/.../container/app_catalog.rs and docs/dht-distribution-design.md).
|
|
#
|
|
# Nodes fetch this file over HTTP from the OVH origin (same host as the OTA
|
|
# manifest), compare each app's catalog version against the running container
|
|
# tag, and light up the per-app "Update" button — no node release required.
|
|
#
|
|
# The app_id -> image-variable mapping below MIRRORS
|
|
# core/archipelago/src/container/image_versions.rs (image_var_for_app +
|
|
# containers_for_stack). image_versions.rs is the canonical mapping; keep this in
|
|
# sync when you add an app there.
|
|
#
|
|
# Usage:
|
|
# scripts/generate-app-catalog.sh [output-path]
|
|
# EMBED_MANIFESTS=0 scripts/generate-app-catalog.sh # version/image only (legacy)
|
|
# # then publish: push releases/app-catalog.json to the OVH gitea (raw URL).
|
|
#
|
|
# EMBED_MANIFESTS (default ON, 2026-06-23): embed each app's full
|
|
# apps/<id>/manifest.yml into its catalog entry's `manifest` field, so nodes
|
|
# install from the signed registry alone (no OTA-shipped disk manifest). Consumed
|
|
# by container::app_catalog + the orchestrator's load_manifests overlay
|
|
# (origin-wins, disk = fallback). See docs/registry-manifest-design.md. The
|
|
# migration window is over — every regen now embeds; set EMBED_MANIFESTS=0 only
|
|
# to reproduce the old version/image-only catalog.
|
|
set -euo pipefail
|
|
|
|
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
|
OUT="${1:-$ROOT/releases/app-catalog.json}"
|
|
|
|
# Export every *_IMAGE var (and ARCHY_REGISTRY) so python can read them.
|
|
set -a
|
|
# shellcheck disable=SC1091
|
|
source "$ROOT/scripts/image-versions.sh"
|
|
set +a
|
|
|
|
UPDATED="$(date -u +%Y-%m-%d)" OUT="$OUT" APPS_DIR="$ROOT/apps" \
|
|
EMBED_MANIFESTS="${EMBED_MANIFESTS:-1}" python3 - <<'PY'
|
|
import glob
|
|
import json, os
|
|
|
|
try:
|
|
import yaml
|
|
except ImportError:
|
|
yaml = None
|
|
|
|
def img(var):
|
|
v = os.environ.get(var)
|
|
return v if v else None
|
|
|
|
def tag(image):
|
|
# version = tag after the LAST colon that follows the last slash
|
|
if not image:
|
|
return None
|
|
tail = image.rsplit('/', 1)[-1]
|
|
return tail.rsplit(':', 1)[1] if ':' in tail else 'latest'
|
|
|
|
# Single-container apps: app_id -> primary image variable.
|
|
SINGLE = {
|
|
"bitcoin-knots": "BITCOIN_KNOTS_IMAGE",
|
|
"lnd": "LND_IMAGE",
|
|
"electrumx": "ELECTRUMX_IMAGE",
|
|
"bitcoin-ui": "BITCOIN_UI_IMAGE",
|
|
"lnd-ui": "LND_UI_IMAGE",
|
|
"electrs-ui": "ELECTRS_UI_IMAGE",
|
|
"homeassistant": "HOMEASSISTANT_IMAGE",
|
|
"grafana": "GRAFANA_IMAGE",
|
|
"uptime-kuma": "UPTIME_KUMA_IMAGE",
|
|
"jellyfin": "JELLYFIN_IMAGE",
|
|
"photoprism": "PHOTOPRISM_IMAGE",
|
|
"ollama": "OLLAMA_IMAGE",
|
|
"vaultwarden": "VAULTWARDEN_IMAGE",
|
|
"nextcloud": "NEXTCLOUD_IMAGE",
|
|
"searxng": "SEARXNG_IMAGE",
|
|
"cryptpad": "CRYPTPAD_IMAGE",
|
|
"filebrowser": "FILEBROWSER_IMAGE",
|
|
"nginx-proxy-manager": "NPM_IMAGE",
|
|
"portainer": "PORTAINER_IMAGE",
|
|
"tailscale": "TAILSCALE_IMAGE",
|
|
"fedimint": "FEDIMINT_IMAGE",
|
|
"fedimint-gateway": "FEDIMINT_GATEWAY_IMAGE",
|
|
"nostr-rs-relay": "NOSTR_RS_RELAY_IMAGE",
|
|
"nostr-vpn": "NOSTR_VPN_IMAGE",
|
|
"fips": "FIPS_IMAGE",
|
|
"routstr": "ROUTSTR_IMAGE",
|
|
"adguardhome": "ADGUARDHOME_IMAGE",
|
|
}
|
|
|
|
# Stack apps: app_id -> {container_name: image variable}. The FIRST entry is the
|
|
# primary (its version drives the badge); it is also emitted as `image`.
|
|
STACK = {
|
|
"indeedhub": {
|
|
"indeedhub": "INDEEDHUB_IMAGE",
|
|
"indeedhub-api": "INDEEDHUB_API_IMAGE",
|
|
"indeedhub-ffmpeg": "INDEEDHUB_FFMPEG_IMAGE",
|
|
},
|
|
"immich": {
|
|
"immich_server": "IMMICH_SERVER_IMAGE",
|
|
"immich_postgres": "IMMICH_POSTGRES_IMAGE",
|
|
"immich_redis": "REDIS_IMAGE",
|
|
},
|
|
"penpot": {
|
|
"penpot-frontend": "PENPOT_FRONTEND_IMAGE",
|
|
"penpot-backend": "PENPOT_BACKEND_IMAGE",
|
|
"penpot-exporter": "PENPOT_EXPORTER_IMAGE",
|
|
"penpot-postgres": "PENPOT_POSTGRES_IMAGE",
|
|
"penpot-valkey": "PENPOT_VALKEY_IMAGE",
|
|
},
|
|
"mempool": {
|
|
"archy-mempool-web": "MEMPOOL_WEB_IMAGE",
|
|
"mempool-api": "MEMPOOL_BACKEND_IMAGE",
|
|
"archy-mempool-db": "MARIADB_IMAGE",
|
|
},
|
|
"btcpay": {
|
|
"btcpay-server": "BTCPAY_IMAGE",
|
|
"archy-nbxplorer": "NBXPLORER_IMAGE",
|
|
"archy-btcpay-db": "BTCPAY_POSTGRES_IMAGE",
|
|
},
|
|
}
|
|
|
|
apps = {}
|
|
for app_id, var in SINGLE.items():
|
|
image = img(var)
|
|
if image:
|
|
apps[app_id] = {"version": tag(image), "image": image}
|
|
|
|
for app_id, comps in STACK.items():
|
|
images = {name: img(var) for name, var in comps.items() if img(var)}
|
|
if not images:
|
|
continue
|
|
primary_name = next(iter(comps)) # first listed = primary
|
|
primary_image = img(comps[primary_name])
|
|
entry = {"version": tag(primary_image)}
|
|
if primary_image:
|
|
entry["image"] = primary_image
|
|
entry["images"] = images
|
|
apps[app_id] = entry
|
|
|
|
# Opt-in (EMBED_MANIFESTS): embed each app's full manifest so nodes install from
|
|
# the registry alone. The whole manifest document is embedded under `manifest`
|
|
# (top-level `app:` preserved) — that is exactly what the Rust side deserializes
|
|
# into an AppManifest. Apps not already in SINGLE/STACK get a new entry whose
|
|
# version comes from the manifest. A bad embed is harmless: the node validates and
|
|
# falls back to its disk manifest.
|
|
embedded = 0
|
|
apps_dir = os.environ.get("APPS_DIR")
|
|
if os.environ.get("EMBED_MANIFESTS") and apps_dir:
|
|
if yaml is None:
|
|
raise SystemExit("EMBED_MANIFESTS set but PyYAML is not available")
|
|
for path in sorted(glob.glob(os.path.join(apps_dir, "*", "manifest.yml"))):
|
|
with open(path) as fh:
|
|
data = yaml.safe_load(fh)
|
|
if not isinstance(data, dict) or not isinstance(data.get("app"), dict):
|
|
continue
|
|
app = data["app"]
|
|
app_id = app.get("id")
|
|
if not app_id:
|
|
continue
|
|
entry = apps.setdefault(str(app_id), {})
|
|
entry.setdefault("version", str(app.get("version", "")) or "0")
|
|
entry["manifest"] = data
|
|
embedded += 1
|
|
|
|
# Multi-version support (docs/bitcoin-multi-version-design.md §3 Phase 1):
|
|
# curated, bounded `versions[]` a runner may install or switch to. The entry
|
|
# marked default:true MUST equal the app's top-level catalog `version` (the
|
|
# manifest version for embedded apps) so selecting it un-pins / tracks latest.
|
|
#
|
|
# ONLY list versions whose tagged image is actually published to the registry —
|
|
# an unbuilt tag 404s on install. Extend each list as scripts/build-bitcoin-
|
|
# image.sh (Phase 0) publishes more tagged images, e.g.:
|
|
# {"version": "30.0", "image": f"{REGISTRY}/bitcoin:30.0"},
|
|
# {"version": "27.2", "image": f"{REGISTRY}/bitcoin:27.2", "deprecated": True, "eol": "2026-12-31"},
|
|
REGISTRY = os.environ.get("ARCHY_REGISTRY", "source.archipelago-foundation.org/lfg2025")
|
|
VERSIONS = {
|
|
# Curated Core set (latest patch per major, current → 25). Images built +
|
|
# verified (SHA-256 + OpenPGP, fail-closed) and pushed by
|
|
# scripts/build-bitcoin-image.sh. `28.4.0` is the default (== the manifest's
|
|
# top-level version) so existing/new installs are undisturbed; runners switch
|
|
# up to 31.0 (e.g. for BIP-110 signalling) or down to 25.2 from the app's
|
|
# "Version & Updates" card. Add the next release by building its image then
|
|
# prepending it here.
|
|
"bitcoin-core": [
|
|
{"version": "latest", "image": f"{REGISTRY}/bitcoin:latest", "default": True},
|
|
{"version": "31.0", "image": f"{REGISTRY}/bitcoin:31.0"},
|
|
{"version": "30.2", "image": f"{REGISTRY}/bitcoin:30.2"},
|
|
{"version": "29.3", "image": f"{REGISTRY}/bitcoin:29.3"},
|
|
{"version": "29.2", "image": f"{REGISTRY}/bitcoin:29.2"},
|
|
{"version": "28.4.0", "image": f"{REGISTRY}/bitcoin:28.4"},
|
|
{"version": "27.2", "image": f"{REGISTRY}/bitcoin:27.2"},
|
|
{"version": "26.2", "image": f"{REGISTRY}/bitcoin:26.2", "deprecated": True},
|
|
{"version": "25.2", "image": f"{REGISTRY}/bitcoin:25.2", "deprecated": True},
|
|
],
|
|
# Knots: a real tagged build is now published, so it's selectable + pinnable
|
|
# in the Knots app interface. `latest` is the default (== the manifest's
|
|
# floating tag) so selecting it un-pins / tracks latest — this MUST match the
|
|
# top-level catalog version (L167-168) or the card can't reach "latest" and
|
|
# selecting the highlighted default would instead pin+recreate. Pinning
|
|
# 29.3.knots20260508 moves a runner off the floating tag.
|
|
# `latest` is the default and points at the NEWEST published dated image
|
|
# (not the bare :latest tag) so "Always use the latest version" installs the
|
|
# newest build on fixed-binary nodes, while UNPINNED nodes still resolve via
|
|
# the manifest's floating :latest tag (kept on the legacy image until the
|
|
# entrypoint-render fix is fleet-deployed — see
|
|
# docs/bitcoin-version-bulletproof-rollout.md).
|
|
"bitcoin-knots": [
|
|
{"version": "latest",
|
|
"image": f"{REGISTRY}/bitcoin-knots:29.3.knots20260508", "default": True},
|
|
{"version": "29.3.knots20260508",
|
|
"image": f"{REGISTRY}/bitcoin-knots:29.3.knots20260508"},
|
|
{"version": "29.3.knots20260507",
|
|
"image": f"{REGISTRY}/bitcoin-knots:29.3.knots20260507"},
|
|
{"version": "29.3.knots20260210",
|
|
"image": f"{REGISTRY}/bitcoin-knots:29.3.knots20260210"},
|
|
{"version": "29.2.knots20251110",
|
|
"image": f"{REGISTRY}/bitcoin-knots:29.2.knots20251110"},
|
|
],
|
|
}
|
|
for app_id, versions in VERSIONS.items():
|
|
if app_id in apps and versions:
|
|
apps[app_id]["versions"] = versions
|
|
# The default/latest entry MUST equal the app's top-level catalog
|
|
# `version` (commit 169ff2e2) so selecting the highlighted default
|
|
# un-pins / tracks latest instead of pinning+recreating. Enforce it here
|
|
# rather than relying on the manifest version matching.
|
|
default_entry = next((v for v in versions if v.get("default")), None)
|
|
if default_entry:
|
|
apps[app_id]["version"] = default_entry["version"]
|
|
|
|
catalog = {
|
|
"schema": 1,
|
|
"updated": os.environ["UPDATED"],
|
|
"apps": dict(sorted(apps.items())),
|
|
}
|
|
|
|
with open(os.environ["OUT"], "w") as f:
|
|
json.dump(catalog, f, indent=2)
|
|
f.write("\n")
|
|
suffix = f" (embedded {embedded} manifests)" if embedded else ""
|
|
print(f"Wrote {os.environ['OUT']} with {len(apps)} apps{suffix}")
|
|
PY
|