diff --git a/apps/btcpay-server/manifest.yml b/apps/btcpay-server/manifest.yml index 703a9f0c..705feed1 100644 --- a/apps/btcpay-server/manifest.yml +++ b/apps/btcpay-server/manifest.yml @@ -1,11 +1,11 @@ app: id: btcpay-server name: BTCPay Server - version: 2.3.9 + version: 2.4.2 description: Self-hosted Bitcoin payment processor. Accept Bitcoin payments without intermediaries. container: - image: docker.io/btcpayserver/btcpayserver:2.3.9 + image: docker.io/btcpayserver/btcpayserver:2.4.2 pull_policy: if-not-present network: archy-net secret_env: diff --git a/scripts/check-app-catalog-drift.py b/scripts/check-app-catalog-drift.py index afe59b96..0d943ece 100644 --- a/scripts/check-app-catalog-drift.py +++ b/scripts/check-app-catalog-drift.py @@ -52,12 +52,49 @@ LEGACY_STACK_CATALOG_IDS = { def load_catalog(path: Path) -> dict[str, dict[str, Any]]: + """Load either catalog shape into {app_id: app-fields}. + + Two formats exist and only one used to be understood here: + + * app-catalog/catalog.json — `apps` is a LIST of entries carrying `id`. + * releases/app-catalog.json — `apps` is a DICT keyed by app id, and each + entry wraps the app's full manifest under `manifest.app` (the signed + release catalog; EMBED_MANIFESTS has been on since 2026-06-23). + + The signed release catalog is the one nodes actually resolve apps through, + so a drift checker that only parsed the list form was checking the file + that governs nothing and crashing on the file that governs everything. + """ with path.open("r", encoding="utf-8") as fh: data = json.load(fh) apps = data.get("apps", []) - if not isinstance(apps, list): - raise ValueError(f"{path}: expected .apps to be a list") - return {str(app.get("id", "")): app for app in apps if isinstance(app, dict) and app.get("id")} + + if isinstance(apps, list): + return { + str(app.get("id", "")): app + for app in apps + if isinstance(app, dict) and app.get("id") + } + + if isinstance(apps, dict): + out: dict[str, dict[str, Any]] = {} + for app_id, entry in apps.items(): + if not isinstance(entry, dict): + continue + manifest = entry.get("manifest") + if isinstance(manifest, dict) and isinstance(manifest.get("app"), dict): + # Embedded manifest: compare against the same fields the disk + # manifests expose, plus the entry's own version. + app = dict(manifest["app"]) + else: + app = {} + app.setdefault("id", app_id) + if entry.get("version") is not None: + app["version"] = entry["version"] + out[str(app_id)] = app + return out + + raise ValueError(f"{path}: expected .apps to be a list or an object") def load_manifests(apps_dir: Path) -> dict[str, dict[str, Any]]: