diff --git a/tools/ci/kill-by-name-check.sh b/tools/ci/kill-by-name-check.sh index 02f97e79c..694871c8e 100755 --- a/tools/ci/kill-by-name-check.sh +++ b/tools/ci/kill-by-name-check.sh @@ -14,6 +14,11 @@ # .json, .jsonl, .out, .err, .pid, .csv, .md, .toml, .yml) is flagged: a file name is a redirect or an argument, and a # redirect is never on a command line. # 3. `ps ... | grep ` without the bracket form is flagged (the same self-match). +# 4. (8 October 2026, 12:4x UK: twice in one day a merge gate died to signal 15 from another lane's kill pattern) `killall` with any +# pattern, and `pkill`/`pgrep` (with or without -f, -x) whose pattern is a generic tool or a gate's name (bash, sh, node, cargo, +# python, ssh, git, pre-push, merge-to-master, build-remote, remote-run, gate, igneum-gate, igneum-merge) are flagged: such a +# pattern can match a gate or a merge run of another lane. A gate is killed by its pid file only +# (.git/igneum-gate.pid, .git/igneum-merge.pid under the worktree: `kill "$(cut -d' ' -f1 )"`). # # tools/ci/kill-by-name-check.sh # exit 1 with file:line and the reason # tools/ci/kill-by-name-check.sh --self-test # fires on each banned shape, passes each allowed one @@ -24,6 +29,20 @@ check_line() { # -> prints the reason, returns 1, when the line carrie code="${line%%#*}" # a comment is not code (a line that starts with // or * is a comment too) [[ "$code" =~ ^[[:space:]]*(//|\*|/\*) ]] && return 0 [[ "$line" =~ ^[[:space:]]*(//|\*|/\*) ]] && return 0 + # rule 4: killall, and any pkill/pgrep whose pattern could match a gate or a merge run of another lane (kills are by pid file only) + if [[ "$code" =~ (^|[^A-Za-z0-9_./-])killall[[:space:]]+(-[A-Za-z0-9]+[[:space:]]+)*(\"[^\"]*\"|\'[^\']*\'|[^[:space:]|;\)]+) ]]; then + echo "killall ${BASH_REMATCH[3]}: killall matches by name across every lane's processes; a gate or a merge run dies with it; kill by pid file only"; return 1 + fi + local gp="$code" + while [[ "$gp" =~ (^|[^A-Za-z0-9_./-])(pgrep|pkill)([[:space:]]+-[A-Za-z0-9]+)*[[:space:]]+(\"[^\"]*\"|\'[^\']*\'|[^[:space:]|;\)-][^[:space:]|;\)]*) ]]; do + local gpat="${BASH_REMATCH[4]}"; gpat="${gpat#\"}"; gpat="${gpat%\"}"; gpat="${gpat#\'}"; gpat="${gpat%\'}" + local core="${gpat#^}"; core="$(printf '%s' "$core" | sed -E 's/^\[([A-Za-z])\]/\1/')" # the bracket form [p]re-push reads as pre-push + case "$core" in + bash|sh|zsh|node|cargo|python|python3|ssh|git|perl|gate|igneum|*pre-push*|*merge-to-master*|*build-remote*|*remote-run*|*igneum-gate*|*igneum-merge*) + echo "pkill/pgrep on a pattern that can match a gate or a merge run of another lane ($gpat): kill by pid file only (.git/igneum-gate.pid, .git/igneum-merge.pid)"; return 1 ;; + esac + gp="${gp#*${BASH_REMATCH[2]}}" + done # every pgrep/pkill -f on the line, not only the first (`pkill -f "[i]gneum-prove-host"; pkill -f prove-shard.sh` hid its second) local rest="$code" m while [[ "$rest" =~ (^|[^A-Za-z0-9_./-])(pgrep|pkill)([[:space:]]+-[A-Za-z0-9]+)*[[:space:]]+-[A-Za-z]*f[A-Za-z]*[[:space:]]+(\"[^\"]*\"|\'[^\']*\'|[^[:space:]|;\)]+) ]]; do @@ -52,6 +71,14 @@ check_line() { # -> prints the reason, returns 1, when the line carrie if [ "${1:-}" = "--self-test" ]; then fails=0 bad=( + 'killall node' + 'killall -9 igneum-miner' + 'pkill -f bash' + 'pkill -x node' + 'pkill -f "[p]re-push.sh"' + 'pgrep -f merge-to-master' + 'pkill -f "^igneum-gate:"' + 'pkill cargo' 'pkill -f igneum-roll.log' 'pkill -f "fleet-wave-3.log" || true' 'pgrep -f igneumd >/dev/null && exit 0' @@ -65,6 +92,10 @@ if [ "${1:-}" = "--self-test" ]; then "pgrep -fl 'igneumd --' | grep -v Wallet" ) good=( + 'kill "$(cut -d" " -f1 .git/igneum-gate.pid)"' + 'pkill -F /srv/x/run.pid' + 'pkill -P "$keeper"' + 'pkill -x igneumd' 'pgrep -f "[i]gneumd" >/dev/null' "pkill -f '[n]ode tools/fleet/wave.mjs'" 'pgrep -x igneumd' diff --git a/tools/ci/merge-to-master.sh b/tools/ci/merge-to-master.sh index 2cd5412d3..54d5263ea 100755 --- a/tools/ci/merge-to-master.sh +++ b/tools/ci/merge-to-master.sh @@ -19,6 +19,12 @@ # unless --fixes-master, none pushes the branch, pending waits then goes set -euo pipefail ROOT=$(git rev-parse --show-toplevel); cd "$ROOT" +# kill-proof by rule (8 October 2026): a unique title per run and a pid file under the worktree's git directory, removed at exit; a merge +# run is stopped by `kill "$(cut -d' ' -f1 .git/igneum-merge.pid)"` and never by a name pattern (tools/ci/kill-by-name-check.sh) +if [ -z "${IGNEUM_MERGE_TITLE:-}" ] && [ "${IGNEUM_GATE_NO_TITLE:-0}" != 1 ]; then export IGNEUM_MERGE_TITLE="igneum-merge:$$-$(date +%s)"; exec -a "$IGNEUM_MERGE_TITLE" bash "$0" "$@"; fi +MERGE_PID_FILE="$(git rev-parse --git-dir)/igneum-merge.pid" +printf '%s %s %s\n' "$$" "$(date -u +%Y-%m-%dT%H:%M:%SZ)" "${IGNEUM_MERGE_TITLE:-untitled}" > "$MERGE_PID_FILE" 2>/dev/null || true +trap 'rm -f "$MERGE_PID_FILE"' EXIT . tools/ci/gh-env.sh # every gh call here reads Igneum's own gh directory, never the founder's (8 October 2026) BRANCH="$(git rev-parse --abbrev-ref HEAD)"; TRIES=6; CI_WAIT_MIN="${CI_WAIT_MIN:-25}"; FIXES_MASTER=0; SELF_TEST=0; REMOTE="${MERGE_REMOTE:-origin}" while [ $# -gt 0 ]; do case "$1" in --tries) TRIES="$2"; shift 2 ;; --ci-wait) CI_WAIT_MIN="$2"; shift 2 ;; --fixes-master) FIXES_MASTER=1; shift ;; --remote) REMOTE="$2"; shift 2 ;; --self-test) SELF_TEST=1; shift ;; -*) echo "unknown option $1" >&2; exit 2 ;; *) BRANCH="$1"; shift ;; esac; done diff --git a/tools/ci/pre-push.sh b/tools/ci/pre-push.sh index 487069142..48e3f533d 100755 --- a/tools/ci/pre-push.sh +++ b/tools/ci/pre-push.sh @@ -27,8 +27,19 @@ cd "$(git rev-parse --show-toplevel)" || exit 1 unset GIT_DIR GIT_WORK_TREE GIT_INDEX_FILE GIT_PREFIX GIT_COMMON_DIR GIT_OBJECT_DIRECTORY GIT_ALTERNATE_OBJECT_DIRECTORIES GIT_QUARANTINE_PATH GIT_PUSH_OPTION_COUNT MODE="${1:-local}"; MODE="${MODE#--}" GATE_ROOT="$(pwd -P)" +# Kill-proof by rule (8 October 2026, 12:4x UK: twice in a day a gate died to signal 15 from another lane's kill pattern): every gate run +# carries a unique process title (igneum-gate:-, set by re-exec through `exec -a`) and writes its pid, start and mode to +# .git/igneum-gate.pid under the worktree (removed at exit); a gate is stopped by that file only (`kill "$(cut -d' ' -f1 )"`), and +# tools/ci/kill-by-name-check.sh refuses any pkill/pgrep/killall pattern that could match a gate. IGNEUM_GATE_NO_TITLE=1 skips the re-exec. +if [ -z "${IGNEUM_GATE_TITLE:-}" ] && [ "${IGNEUM_GATE_NO_TITLE:-0}" != 1 ] && [ "$MODE" != self-test ]; then + export IGNEUM_GATE_TITLE="igneum-gate:$$-$(date +%s)" + exec -a "$IGNEUM_GATE_TITLE" bash "$0" "$@" +fi +GATE_PID_FILE="$(git rev-parse --git-dir 2>/dev/null || echo .git)/igneum-gate.pid" +printf '%s %s %s %s\n' "$$" "$(date -u +%Y-%m-%dT%H:%M:%SZ)" "$MODE" "${IGNEUM_GATE_TITLE:-untitled}" > "$GATE_PID_FILE" 2>/dev/null || true +[ "${IGNEUM_GATE_HOLD:-0}" = 1 ] && sleep 4 # the self-test's window to read the file and the title . "$GATE_ROOT/tools/ci/gh-env.sh" # every gh call under the gate reads Igneum's own gh directory, never the founder's (8 October 2026) # the readers below are called from fixture repositories in the self-test, so by absolute path -RED=0; N=0; LOG="$(mktemp)"; trap 'rm -rf "$LOG" "${SITE_TMP:-}"' EXIT +RED=0; N=0; LOG="$(mktemp)"; trap 'rm -rf "$LOG" "${SITE_TMP:-}"; rm -f "$GATE_PID_FILE"' EXIT T0=$(date +%s) run() { @@ -271,6 +282,14 @@ case "$MODE" in MODE=ci GITHUB_ACTIONS= site_in_place && { echo "self-test failed: --ci outside GitHub Actions chose the in-place build"; fails=1; } MODE=ci GITHUB_ACTIONS=true site_in_place || { echo "self-test failed: --ci inside GitHub Actions did not choose the in-place build"; fails=1; } declare -f tree_checks | grep -q 'never_push_checks' || { echo "self-test failed: the full gate does not run the never-push checks"; fails=1; } + # kill-proof: a running gate writes its pid, start and mode to .git/igneum-gate.pid and removes it at exit; the title re-exec is in the script + pf="$(mktemp -d)"; ( cd "$pf" && git init -q -b master . ) + # a gate that is held open (IGNEUM_GATE_HOLD=1 sleeps 3 s after writing the file) names its own pid and mode in the file; the file is gone after it + ( cd "$pf"; IGNEUM_GATE_HOLD=1 bash "$GATE_ROOT/tools/ci/pre-push.sh" --list >/dev/null 2>&1 & p=$!; sleep 2; f="$pf/.git/igneum-gate.pid" + if [ ! -f "$f" ]; then echo "self-test failed: no pid file at $f while the gate runs"; else read -r pid start mode title < "$f"; [ "$pid" = "$p" ] || echo "self-test failed: the pid file names $pid, the gate is $p"; [ "$mode" = list ] || echo "self-test failed: the pid file's mode is $mode"; case "$title" in igneum-gate:*) ;; *) echo "self-test failed: the pid file carries no title: $title" ;; esac; [ "$(ps -o command= -p "$p" | cut -d' ' -f1)" = "$title" ] || echo "self-test failed: the process title is $(ps -o command= -p "$p" | cut -d' ' -f1), the file says $title"; fi + wait $p 2>/dev/null; [ -f "$f" ] && echo "self-test failed: the pid file outlived the gate"; true ) | grep 'self-test failed' && fails=1 + rm -rf "$pf" + grep -q 'exec -a "\$IGNEUM_GATE_TITLE" bash "\$0" "\$@"' "$0" || { echo "self-test failed: the gate does not re-exec under its unique title"; fails=1; } # the overlap sweep's wall clock runs the command with GNU timeout where it exists and plainly where it does not (bash 3.2 under set -u included) [ "$(wall_clock 5 /bin/echo clocked 2>&1)" = clocked ] || { echo "self-test failed: wall_clock did not run its command"; fails=1; } [ "$(PATH=/nonexistent wall_clock 5 /bin/echo plain 2>&1)" = plain ] || { echo "self-test failed: wall_clock without a timeout binary did not run its command plainly"; fails=1; }