diff --git a/apps/bitcoin-core/manifest.yml b/apps/bitcoin-core/manifest.yml index c8619f7e..5e9db91a 100644 --- a/apps/bitcoin-core/manifest.yml +++ b/apps/bitcoin-core/manifest.yml @@ -38,15 +38,8 @@ app: RPC_CONF="/tmp/rpc.conf"; umask 077; { echo "rpcuser=$RPC_USER"; echo "rpcpassword=$RPC_PASS"; } > "$RPC_CONF"; - # A stray bitcoin.conf in the datadir is FATAL when -conf points - # elsewhere: bitcoind refuses to start with "contains a bitcoin.conf - # file which is ignored", and the app crash-loops (100.82.34.38, - # 2026-08-05 — Exited(1) every few seconds). Our -conf carries the - # RPC credentials and the flags below are the authoritative config, - # so the datadir file is legacy debris; say so out loud rather than - # failing, and let bitcoind start. if [ -f /home/bitcoin/.bitcoin/bitcoin.conf ]; then - echo "archipelago: ignoring legacy /home/bitcoin/.bitcoin/bitcoin.conf; RPC config comes from $RPC_CONF and the flags below" >&2; + echo "archipelago: ignoring legacy datadir bitcoin.conf; RPC config comes from $RPC_CONF" >&2; fi; RPC_TXRELAY_AUTH="$(printenv BITCOIN_RPC_TXRELAY_RPCAUTH || true)"; DISK_GB_VALUE="$(printenv DISK_GB || true)"; diff --git a/apps/bitcoin-knots/manifest.yml b/apps/bitcoin-knots/manifest.yml index cdc492c5..f74c6052 100644 --- a/apps/bitcoin-knots/manifest.yml +++ b/apps/bitcoin-knots/manifest.yml @@ -38,15 +38,8 @@ app: RPC_CONF="/tmp/rpc.conf"; umask 077; { echo "rpcuser=$RPC_USER"; echo "rpcpassword=$RPC_PASS"; } > "$RPC_CONF"; - # A stray bitcoin.conf in the datadir is FATAL when -conf points - # elsewhere: bitcoind refuses to start with "contains a bitcoin.conf - # file which is ignored", and the app crash-loops (100.82.34.38, - # 2026-08-05 — Exited(1) every few seconds). Our -conf carries the - # RPC credentials and the flags below are the authoritative config, - # so the datadir file is legacy debris; say so out loud rather than - # failing, and let bitcoind start. if [ -f /home/bitcoin/.bitcoin/bitcoin.conf ]; then - echo "archipelago: ignoring legacy /home/bitcoin/.bitcoin/bitcoin.conf; RPC config comes from $RPC_CONF and the flags below" >&2; + echo "archipelago: ignoring legacy datadir bitcoin.conf; RPC config comes from $RPC_CONF" >&2; fi; RPC_TXRELAY_AUTH="$(printenv BITCOIN_RPC_TXRELAY_RPCAUTH || true)"; DISK_GB_VALUE="$(printenv DISK_GB || true)"; diff --git a/scripts/check-manifest-shell.py b/scripts/check-manifest-shell.py new file mode 100755 index 00000000..67414a68 --- /dev/null +++ b/scripts/check-manifest-shell.py @@ -0,0 +1,86 @@ +#!/usr/bin/env python3 +"""Syntax-check the shell embedded in apps/*/manifest.yml. + +A manifest can carry a whole startup script in `container.custom_args` / +`entrypoint`. Nothing validated it, so a broken one shipped through the +signed catalog and only failed on the node — as a container that exits +instantly and an app that vanishes from the UI. + +Two checks, both learned from v1.7.124 (bitcoin-knots / bitcoin-core): + +1. `sh -n` the snippet. The break was `sh: Syntax error: "fi" unexpected`, + which no YAML parse and no Rust test could have caught. + +2. Reject `#` inside the snippet. These are YAML **folded** scalars (`>-`), + where `#` is NOT a comment — it is literal text that reaches the shell, + and because folding joins lines with spaces it comments out the rest of + the folded line. That is exactly how an `if ... then` was swallowed while + its more-indented body survived, leaving an orphan `fi`. Put explanations + above the `- >-` line, where YAML really does treat them as comments. +""" + +from __future__ import annotations + +import glob +import os +import subprocess +import sys +import tempfile + +import yaml + +# Long enough to be a script rather than a flag. +MIN_SCRIPT_LEN = 60 + + +def snippets(path: str): + with open(path, encoding="utf-8") as fh: + data = yaml.safe_load(fh) + container = ((data or {}).get("app") or {}).get("container") or {} + for key in ("custom_args", "entrypoint"): + value = container.get(key) + if not isinstance(value, list): + continue + for i, part in enumerate(value): + if isinstance(part, str) and len(part) >= MIN_SCRIPT_LEN: + yield f"{key}[{i}]", part + + +def main() -> int: + failures = [] + checked = 0 + for path in sorted(glob.glob("apps/*/manifest.yml")): + app = os.path.basename(os.path.dirname(path)) + try: + found = list(snippets(path)) + except Exception as exc: # noqa: BLE001 — report, don't crash the gate + failures.append(f"{app}: manifest does not parse: {exc}") + continue + for where, script in found: + checked += 1 + if "#" in script: + failures.append( + f"{app} {where}: contains '#'. In a folded YAML scalar that is not a " + f"comment — it reaches the shell and comments out the rest of the " + f"folded line. Move the explanation above the '- >-' line." + ) + with tempfile.NamedTemporaryFile("w", suffix=".sh", delete=False) as tmp: + tmp.write(script) + tmp_path = tmp.name + try: + proc = subprocess.run( + ["sh", "-n", tmp_path], capture_output=True, text=True, check=False + ) + finally: + os.unlink(tmp_path) + if proc.returncode != 0: + failures.append(f"{app} {where}: {proc.stderr.strip()}") + + for f in failures: + print(f"MANIFEST-SHELL {f}", file=sys.stderr) + print(f'{{"snippets_checked": {checked}, "failures": {len(failures)}}}') + return 1 if failures else 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/tests/release/run.sh b/tests/release/run.sh index 5c577d33..6d047a00 100755 --- a/tests/release/run.sh +++ b/tests/release/run.sh @@ -62,6 +62,7 @@ summary() { # ── 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 "manifest-shell" python3 scripts/check-manifest-shell.py stage "catalog-drift" python3 scripts/check-app-catalog-drift.py --release --strict # Every release must surface its CHANGELOG entry in the Settings "What's New" # modal. The modal hardcodes a block per version and has drifted behind before