fix: fail preflight when a gate tool is killed instead of reporting it clean (#633)

A tool whose child dies by signal can still exit 0 through go tool, so
govulncheck printed 'ok' after being OOM-killed and preflight passed
without a vulnerability scan having run. Route lint, govulncheck and
nilaway through scripts/run-gate.sh, which fails on a non-zero exit or
on signal-death output.
This commit is contained in:
Josh Siviour
2026-08-02 08:15:16 +10:00
committed by GitHub
parent d89ac24af2
commit 6ce8b9ff18
3 changed files with 129 additions and 3 deletions
+3 -3
View File
@@ -259,7 +259,7 @@ quickinstall: install-system install-go install-aws
lint:
@echo "Running golangci-lint..."
$(_Q)golangci-lint run ./...
$(_Q)scripts/run-gate.sh golangci-lint golangci-lint run ./...
@echo " golangci-lint ok"
fix:
@@ -267,13 +267,13 @@ fix:
govulncheck:
@echo "Running govulncheck..."
$(_Q)go tool govulncheck ./...
$(_Q)scripts/run-gate.sh govulncheck go tool govulncheck ./...
@echo " govulncheck ok"
# NilAway — advisory nil-panic analysis. Not in preflight due to false positives
nilaway:
@echo "Running nilaway..."
$(_Q)go tool nilaway -include-pkgs=github.com/mulgadc/spinifex -exclude-test-files ./...
$(_Q)scripts/run-gate.sh nilaway go tool nilaway -include-pkgs=github.com/mulgadc/spinifex -exclude-test-files ./...
@echo " nilaway ok"
# Build release tarballs — use distro-ARCH for single arch, distro for both
+91
View File
@@ -0,0 +1,91 @@
// Package preflightgate tests the preflight gate wrapper. It exists as a Go
// test so `go test ./...` exercises it; the repo's shell suites are not yet
// wired into CI.
package preflightgate
import (
"errors"
"os/exec"
"path/filepath"
"runtime"
"strings"
"testing"
)
// scriptPath resolves scripts/run-gate.sh relative to this test file, so the
// test does not depend on the working directory.
func scriptPath(t *testing.T) string {
t.Helper()
_, thisFile, _, ok := runtime.Caller(0)
if !ok {
t.Fatal("runtime.Caller failed")
}
return filepath.Join(filepath.Dir(thisFile), "..", "..", "scripts", "run-gate.sh")
}
func runGate(t *testing.T, args ...string) (string, int) {
t.Helper()
cmd := exec.Command(scriptPath(t), args...)
out, err := cmd.CombinedOutput()
if err == nil {
return string(out), 0
}
var exitErr *exec.ExitError
if errors.As(err, &exitErr) {
return string(out), exitErr.ExitCode()
}
t.Fatalf("running run-gate.sh: %v", err)
return "", -1
}
func TestRunGate_CleanRunPasses(t *testing.T) {
out, code := runGate(t, "govulncheck", "echo", "No vulnerabilities found.")
if code != 0 {
t.Fatalf("expected exit 0 for a clean run, got %d\n%s", code, out)
}
if !strings.Contains(out, "No vulnerabilities found.") {
t.Errorf("tool output should be forwarded, got:\n%s", out)
}
}
func TestRunGate_NonZeroExitFails(t *testing.T) {
out, code := runGate(t, "lint", "sh", "-c", "echo 3 issues.; exit 1")
if code == 0 {
t.Fatalf("expected failure for a non-zero tool exit, got 0\n%s", out)
}
if !strings.Contains(out, "lint FAILED") {
t.Errorf("failure should name the gate, got:\n%s", out)
}
}
// The defect this guards: a signal-killed scanner exiting 0 was reported clean,
// so preflight passed without a vulnerability scan having run.
func TestRunGate_SignalKilledWithZeroExitFails(t *testing.T) {
for _, sig := range []string{"killed", "terminated", "aborted"} {
t.Run(sig, func(t *testing.T) {
out, code := runGate(t, "govulncheck", "sh", "-c",
"echo 'go tool govulncheck: signal: "+sig+"'; exit 0")
if code == 0 {
t.Fatalf("expected failure when the tool died by signal, got 0\n%s", out)
}
if !strings.Contains(out, "did NOT complete") {
t.Errorf("failure should say the gate did not complete, got:\n%s", out)
}
})
}
}
// "signal" appearing in ordinary findings must not fail an otherwise clean run.
func TestRunGate_SignalWordInOutputDoesNotFail(t *testing.T) {
out, code := runGate(t, "lint", "echo", "checked signal handling in 12 files")
if code != 0 {
t.Fatalf("expected exit 0, got %d\n%s", code, out)
}
}
func TestRunGate_RequiresLabelAndCommand(t *testing.T) {
out, code := runGate(t, "govulncheck")
if code != 2 {
t.Fatalf("expected usage exit 2, got %d\n%s", code, out)
}
}
+35
View File
@@ -0,0 +1,35 @@
#!/usr/bin/env bash
# Runs a preflight gate and fails unless it actually completed.
# A tool whose child is killed by a signal can still exit 0 through `go tool`,
# which would let a security or lint gate pass without having run.
set -uo pipefail
if [ "$#" -lt 2 ]; then
echo "usage: run-gate.sh <label> <command> [args...]" >&2
exit 2
fi
label=$1
shift
output=$("$@" 2>&1)
rc=$?
if [ -n "$output" ]; then
printf '%s\n' "$output"
fi
if [ "$rc" -ne 0 ]; then
echo " ${label} FAILED (exit ${rc})" >&2
exit 1
fi
# A zero exit alongside a signal death means the gate never produced a verdict,
# so it must not be reported as clean.
if printf '%s' "$output" | grep -qE 'signal: (killed|terminated|aborted|segmentation fault)'; then
echo " ${label} did NOT complete: the tool was killed by a signal, so this is not a clean result" >&2
echo " re-run it with more memory available before trusting a pass" >&2
exit 1
fi
exit 0