diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 5745224e7..c895324b2 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -95,6 +95,8 @@ jobs: run: bash tools/ci/commit-string-check.sh --self-test - name: build server remote checkout self-test (the stale-overlay class of 6 October 2026) run: bash infra/build-server/remote-run.sh --self-test + - name: no shell assignment hides behind a trailing comment (the swallowed-defaults class of 6 October 2026) + run: bash tools/ci/defaults-line-check.sh --self-test && bash tools/ci/defaults-line-check.sh - name: no secret file names and no 64-hex secrets in the tree (self-test first, then the tree) run: bash tools/ci/no-secrets-check.sh --self-test && bash tools/ci/no-secrets-check.sh - name: faucet unit tests (validation, the daily limits, the signed transaction; keccak, RLP and secp256k1 vectors) diff --git a/tools/build-remote.sh b/tools/build-remote.sh index cee55ccec..30cffa0a7 100755 --- a/tools/build-remote.sh +++ b/tools/build-remote.sh @@ -64,7 +64,7 @@ while [ $# -gt 0 ]; do esac done [ "${CARGO_ARGS[0]:-}" = cargo ] && CARGO_ARGS=("${CARGO_ARGS[@]:1}") -CARGO_ARGS_GIVEN=""; [ "${#CARGO_ARGS[@]}" -gt 0 ] && CARGO_ARGS_GIVEN=1 +CARGO_ARGS_GIVEN=""; [ -n "${CARGO_ARGS[*]:-}" ] && CARGO_ARGS_GIVEN=1 bs_host bs_context @@ -106,16 +106,16 @@ fi # defaults per crate case "$BS_KIND:$BS_CRATE_REL" in node:*) - [ "${#CARGO_ARGS[@]}" -gt 0 ] || CARGO_ARGS=(build --release -p kaspad -p igneum-miner --features kaspad/igneum-pow) + [ -n "${CARGO_ARGS[*]:-}" ] || CARGO_ARGS=(build --release -p kaspad -p igneum-miner --features kaspad/igneum-pow) [ -n "$ARTEFACTS" ] || ARTEFACTS="$TARGET_DIR/release/igneumd $TARGET_DIR/release/igneum-miner" ;; repo:app/igneum-app) - [ "${#CARGO_ARGS[@]}" -gt 0 ] || CARGO_ARGS=(build --release) + [ -n "${CARGO_ARGS[*]:-}" ] || CARGO_ARGS=(build --release) [ -n "$ARTEFACTS" ] || ARTEFACTS="$TARGET_DIR/release/igneum-app $TARGET_DIR/release/igneum-ota-sign $TARGET_DIR/release/igneum-prove-verify" ;; repo:proving/igneum-prove) - [ "${#CARGO_ARGS[@]}" -gt 0 ] || CARGO_ARGS=(build --release) + [ -n "${CARGO_ARGS[*]:-}" ] || CARGO_ARGS=(build --release) [ -n "$ARTEFACTS" ] || ARTEFACTS="$TARGET_DIR/release/igneum-prove-host $TARGET_DIR/release/igneum-prove-export" ;; *) - [ "${#CARGO_ARGS[@]}" -gt 0 ] || CARGO_ARGS=(build --release) ;; + [ -n "${CARGO_ARGS[*]:-}" ] || CARGO_ARGS=(build --release) ;; esac case "${CARGO_ARGS[0]}" in build) ;; *) [ -n "${ARTEFACTS_SET:-}" ] || { FETCH=0; ARTEFACTS=""; } ;; esac # test, check, clippy: nothing to fetch [ -n "$OUT" ] || OUT="$BS_CRATE/target-remote" diff --git a/tools/ci/defaults-line-check.sh b/tools/ci/defaults-line-check.sh new file mode 100755 index 000000000..80f8302e4 --- /dev/null +++ b/tools/ci/defaults-line-check.sh @@ -0,0 +1,57 @@ +#!/usr/bin/env bash +# The swallowed-defaults class (6 October 2026, twice in one evening): a comment appended to a line of shell assignments +# (`JOBS=...; # ... OUT=""; CARGO_ARGS=()`) turns every assignment after the `#` into comment text; the script still parses, +# bash -n and shellcheck say nothing, and the first use of the undeclared array dies under the Mac's bash 3.2 with +# "unbound variable" (build-remote.sh at 19:5x UTC, cross-remote.sh at 21:xx UTC: the shipper's default cross-build never ran +# and its chain kept the previous exes). Rule: no `#` comment on a line that carries shell assignments after it. +# The scan (python3, which every CI host has) first drops quoted strings, ${...} expansions, $# and [^#] so a `#` inside them +# is not a comment, then flags a line where a comment start (start of line or whitespace, then #) is followed by `NAME=`. +# +# tools/ci/defaults-line-check.sh # exit 1 with file:line on a hit +# tools/ci/defaults-line-check.sh --self-test # fires on the swallowed shape, passes clean ones (including ${a#b} and sed s#x#y#) +set -euo pipefail +cd "$(dirname "$0")/../.." +scan() { # file names as arguments (python3 - takes its script from stdin, so stdin cannot carry the list), file:line per hit, exit 1 if any + python3 - "$@" <<'PY' +import re, sys +strip = [ + (re.compile(r"\$\{[^}]*\}"), ""), # ${var#pat}, ${var%pat}, ${!i} + (re.compile(r"\$#"), ""), # the argument count + (re.compile(r"\[\^#\]"), ""), # a bracket expression excluding # + (re.compile(r"'[^']*'"), "''"), # single-quoted strings + (re.compile(r'"(?:[^"\\]|\\.)*"'), '""'), # double-quoted strings + (re.compile(r"\\#"), ""), # an escaped # +] +# the swallowed shape is an assignment LIST after the comment start: `NAME=...;` or `NAME=()`; a prose mention such as +# "(access public|private, private only in NET_MODE=private)" or "OTA_SKIP=1 (the default now)" has neither +hit = re.compile(r"(^|\s)#.*\s[A-Za-z_][A-Za-z0-9_]*=(\(\)|[^;()]*;)") +bad = 0 +for path in sys.argv[1:]: + try: lines = open(path, encoding="utf-8", errors="replace").read().split("\n") + except OSError: continue + for n, line in enumerate(lines, 1): + s = line + for rx, rep in strip: s = rx.sub(rep, s) + if s.lstrip().startswith("#"): continue # a whole-line comment may say anything + if hit.search(s): + print(f"defaults-line: {path}:{n}: a comment swallows assignments after it: {line.strip()[:140]}"); bad = 1 +sys.exit(bad) +PY +} +if [ "${1:-}" = --self-test ]; then + t=$(mktemp -d); trap 'rm -rf "$t"' EXIT + printf '%s\n' 'JOBS="${JOBS:-}"; # empty = the box decides OUT=""; CARGO_ARGS=()' > "$t/bad.sh" + printf '%s\n' 'JOBS="${JOBS:-}"; OUT=""; CARGO_ARGS=() # one line, nothing assigned after the comment' \ + 'ver=${tarball#node-}; ver=${ver%-linux-x64.tar.xz}' \ + "epoch=\$(sed -n 's/^#define X \"\\(.*\\)\"/\\1/p' \"\$ph\"); day=\$(sed -n 's/^#define Y/x/p')" \ + 'rel="${a#"$TARGET_DIR"/}"; dest="$OUT/$rel"' \ + 'for ((i=1;i<=$#;i++)); do fee="${!i}"; done' \ + "A=\"\$(find . | sed 's#^\\./##' | sort)\"; B=1" \ + 'hetzner_create_one() { # name type location [role] [access] (access public|private, private only in NET_MODE=private)' \ + 'if [ "${OTA_SKIP:-}" = 0 ]; then SIGN=1; fi # the old spelling; OTA_SKIP=1 (the default now) leaves the manifest alone' \ + '# a whole-line comment with OUT=""; CARGO_ARGS=() in it' > "$t/good.sh" + if scan "$t/bad.sh" >/dev/null; then echo "defaults-line self-test: the swallowed shape did NOT fire"; exit 1; fi + if scan "$t/good.sh"; then echo "defaults-line self-test: fires on the swallowed shape, passes the clean shapes"; exit 0; else echo "defaults-line self-test: a clean shape fired"; exit 1; fi +fi +# shellcheck disable=SC2046 # paths in this tree carry no spaces +if scan $(git ls-files 'tools/*.sh' 'tools/**/*.sh' 'infra/**/*.sh' 'proto-cuda/**/*.sh' 'packaging/**/*.sh'); then echo "defaults-line: no assignment hides behind a comment"; else exit 1; fi diff --git a/tools/cross-remote.sh b/tools/cross-remote.sh index cf5509601..431137abe 100755 --- a/tools/cross-remote.sh +++ b/tools/cross-remote.sh @@ -37,7 +37,10 @@ BS_TOOL=cross-remote . "$HERE/../infra/build-server/lib.sh" TARGET=x86_64-pc-windows-gnu -JOBS="${JOBS:-}"; # empty = the box decides: 90 alone, 45 beside another slot holder (remote-run.sh, main's ruling 6 Oct 2026) OUT=""; COMPARE=""; TARGET_DIR="target"; CARGO_ARGS=() +# JOBS empty = the box decides: 90 alone, 45 beside another slot holder (remote-run.sh, main's ruling 6 Oct 2026). One line, no +# trailing comment: a `#` on this line once swallowed every assignment after it, CARGO_ARGS was never declared and the default +# cross-build died with "unbound variable" under the Mac's bash 3.2 (the shipper, 6 Oct 2026, 21:xx UTC) +JOBS="${JOBS:-}"; OUT=""; COMPARE=""; TARGET_DIR="target"; CARGO_ARGS=() while [ $# -gt 0 ]; do case "$1" in --jobs) JOBS="$2"; shift 2 ;; @@ -55,10 +58,10 @@ bs_host bs_context case "$BS_KIND:$BS_CRATE_REL" in node:*) - [ "${#CARGO_ARGS[@]}" -gt 0 ] || CARGO_ARGS=(build --release -p kaspad -p igneum-miner --features igneum-pow --target "$TARGET") + [ -n "${CARGO_ARGS[*]:-}" ] || CARGO_ARGS=(build --release -p kaspad -p igneum-miner --features igneum-pow --target "$TARGET") EXES="igneumd.exe igneum-miner.exe" ;; repo:app/igneum-app) - [ "${#CARGO_ARGS[@]}" -gt 0 ] || CARGO_ARGS=(build --release --target "$TARGET") + [ -n "${CARGO_ARGS[*]:-}" ] || CARGO_ARGS=(build --release --target "$TARGET") EXES="igneum-app.exe igneum-ota-sign.exe igneum-prove-verify.exe" ;; *) bs_die "cross-remote builds the fork (run from a vendor/igneum-node* worktree) or app/igneum-app, not $BS_CRATE_REL" ;; esac