@boyingliu01/xp-gate 0.31.1 → 0.31.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/adapter-common.sh +41 -3
- package/adapters/gate-3.sh +8 -1
- package/adapters/gate-8.sh +67 -6
- package/adapters/go.sh +18 -1
- package/adapters/iac.sh +18 -1
- package/adapters/powershell.sh +37 -1
- package/adapters/python.sh +18 -4
- package/gate-3.sh +10 -1
- package/gate-8.sh +67 -6
- package/hooks/adapter-common.sh +41 -3
- package/hooks/gate-3.sh +10 -1
- package/hooks/gate-8.sh +67 -6
- package/hooks/lib/coverage-total.sh +20 -5
- package/hooks/lib/gate-5c-alignment.sh +145 -0
- package/hooks/lib/jscpd-run.sh +40 -1
- package/hooks/lib/native-path.sh +106 -0
- package/hooks/lib/prepush-score.py +143 -0
- package/hooks/lib/sprint-gate-report.sh +23 -0
- package/hooks/lib/toolchain-path.sh +370 -0
- package/hooks/lib/validate-code-walkthrough.cjs +224 -23
- package/hooks/pre-commit +371 -163
- package/hooks/pre-push +192 -74
- package/hooks/sprint-gate.sh +14 -3
- package/lib/__tests__/check-alignment-cli.test.mjs +20 -3
- package/lib/__tests__/check-alignment-reasons.test.mjs +91 -0
- package/lib/__tests__/doctor-delphi.test.js +46 -0
- package/lib/__tests__/doctor-exec-retry.test.js +20 -10
- package/lib/__tests__/doctor-hook-signatures.test.js +65 -0
- package/lib/__tests__/doctor-path-equivalence.test.js +12 -1
- package/lib/__tests__/doctor.test.js +7 -1
- package/lib/__tests__/hook-health-path.test.js +9 -1
- package/lib/__tests__/issue-566-sync-hooks-backup.test.js +221 -0
- package/lib/__tests__/issue-583-hook-deploy-log.test.js +121 -0
- package/lib/__tests__/issue-586-principles-cmdline.test.js +183 -0
- package/lib/__tests__/issue-589-check-all-gate3.test.js +190 -0
- package/lib/__tests__/issue-590-archlint-vacuous-pass.test.js +138 -0
- package/lib/__tests__/shared-paths-lazy-home.test.js +67 -0
- package/lib/arch.js +261 -44
- package/lib/check-alignment-runner.cjs +23 -0
- package/lib/check.js +4 -1
- package/lib/doctor.js +194 -49
- package/lib/gate-runner.js +265 -58
- package/lib/mattpocock-license-verdict.json +1 -1
- package/lib/principles.js +228 -6
- package/lib/shared-paths.js +53 -19
- package/mock-policy/AGENTS.md +3 -3
- package/mutation/AGENTS.md +3 -3
- package/mutation/__tests__/gremlins-runner.test.ts +3 -3
- package/package.json +1 -1
- package/plugins/claude-code/.claude-plugin/plugin.json +1 -1
- package/plugins/claude-code/skills/delphi-review/AGENTS.md +3 -3
- package/plugins/claude-code/skills/delphi-review/SKILL.md +50 -0
- package/plugins/claude-code/skills/sprint-flow/AGENTS.md +3 -3
- package/plugins/claude-code/skills/test-specification-alignment/AGENTS.md +3 -3
- package/plugins/dsh/package.json +1 -1
- package/plugins/dsh/skills/delphi-review/AGENTS.md +3 -3
- package/plugins/dsh/skills/delphi-review/SKILL.md +50 -0
- package/plugins/dsh/skills/sprint-flow/AGENTS.md +3 -3
- package/plugins/dsh/skills/test-specification-alignment/AGENTS.md +3 -3
- package/plugins/dsh/src/delphi-config-env-diagnostics.test.ts +210 -0
- package/plugins/dsh/src/delphi-config.ts +151 -9
- package/plugins/opencode/package.json +1 -1
- package/plugins/opencode/skills/delphi-review/AGENTS.md +3 -3
- package/plugins/opencode/skills/delphi-review/SKILL.md +50 -0
- package/plugins/opencode/skills/sprint-flow/AGENTS.md +3 -3
- package/plugins/opencode/skills/test-specification-alignment/AGENTS.md +3 -3
- package/plugins/qoder/plugin.json +1 -1
- package/plugins/qoder/skills/delphi-review/AGENTS.md +3 -3
- package/plugins/qoder/skills/delphi-review/SKILL.md +50 -0
- package/plugins/qoder/skills/sprint-flow/AGENTS.md +3 -3
- package/plugins/qoder/skills/test-specification-alignment/AGENTS.md +3 -3
- package/principles/AGENTS.md +3 -3
- package/principles/__tests__/issue-587-java-member-count.test.ts +279 -0
- package/principles/__tests__/issue-588-lsp-java-false-positives.test.ts +213 -0
- package/principles/adapters/java.ts +127 -11
- package/principles/rules/__tests__/solid/issue-588-lsp-base-contract.test.ts +252 -0
- package/principles/rules/__tests__/solid/lsp.test.ts +26 -22
- package/principles/rules/solid/lsp.ts +348 -23
- package/scripts/__tests__/sync-package-content.test.js +32 -1
- package/scripts/sync-package-content.js +6 -1
- package/skills/delphi-review/AGENTS.md +3 -3
- package/skills/delphi-review/SKILL.md +50 -0
- package/skills/sprint-flow/AGENTS.md +3 -3
- package/skills/test-specification-alignment/AGENTS.md +3 -3
- package/sprint-gate.sh +14 -3
package/adapter-common.sh
CHANGED
|
@@ -155,19 +155,57 @@ require_tool() {
|
|
|
155
155
|
local tool_name="$1"
|
|
156
156
|
local gate_name="${2:-Gate}"
|
|
157
157
|
local install_hint="${3:-}"
|
|
158
|
-
|
|
158
|
+
|
|
159
|
+
# #593: on Windows the hook runs under git-bash, which does not see the
|
|
160
|
+
# toolchains installed on the Windows side (%APPDATA%\npm, the node dir,
|
|
161
|
+
# pip --user Scripts, ~/bin). Bootstrapping before probing turns "not on this
|
|
162
|
+
# shell's PATH" back into "found", instead of reporting an installed tool as
|
|
163
|
+
# missing. lib/toolchain-path.sh is sourced by pre-commit before this runs; the
|
|
164
|
+
# guard keeps require_tool usable from adapters that load on their own.
|
|
165
|
+
if command -v bootstrap_toolchain_path >/dev/null 2>&1; then
|
|
166
|
+
bootstrap_toolchain_path
|
|
167
|
+
fi
|
|
168
|
+
|
|
169
|
+
# #580: on WSL (appendWindowsPath=false) and in git-bash, `command -v node`
|
|
170
|
+
# returns 1 while node.exe sits in the same directory and runs. One root cause
|
|
171
|
+
# gave Gate 1 a silent SKIP, Gate 1 a false "Biome check failed" and Gate 8 a
|
|
172
|
+
# false "gitleaks not installed" — an environment fault reported as a code
|
|
173
|
+
# verdict in all three. tool_is_available also counts the .exe suffix and the
|
|
174
|
+
# XP_GATE_<TOOL>_BIN override.
|
|
175
|
+
if command -v tool_is_available >/dev/null 2>&1 && tool_is_available "$tool_name"; then
|
|
176
|
+
return 0
|
|
177
|
+
fi
|
|
178
|
+
|
|
159
179
|
if command -v "$tool_name" >/dev/null 2>&1; then
|
|
160
180
|
return 0
|
|
161
181
|
fi
|
|
162
|
-
|
|
182
|
+
|
|
183
|
+
if command -v "${tool_name}.exe" >/dev/null 2>&1; then
|
|
184
|
+
return 0
|
|
185
|
+
fi
|
|
186
|
+
|
|
163
187
|
if command -v npx >/dev/null 2>&1 && npx --no-install "$tool_name" --version >/dev/null 2>&1; then
|
|
164
188
|
return 0
|
|
165
189
|
fi
|
|
166
|
-
|
|
190
|
+
|
|
191
|
+
# #593: monorepos keep the tool in a subproject's node_modules/.bin, where
|
|
192
|
+
# neither PATH nor `npx` from the repo root can see it.
|
|
193
|
+
if command -v resolve_tool_path >/dev/null 2>&1 && resolve_tool_path "$tool_name" >/dev/null 2>&1; then
|
|
194
|
+
if command -v ensure_tool_dir_on_path >/dev/null 2>&1; then
|
|
195
|
+
ensure_tool_dir_on_path "$tool_name" || true
|
|
196
|
+
fi
|
|
197
|
+
return 0
|
|
198
|
+
fi
|
|
199
|
+
|
|
167
200
|
echo "❌ BLOCKED - Required tool '$tool_name' not available for $gate_name"
|
|
168
201
|
if [ -n "$install_hint" ]; then
|
|
169
202
|
echo " Install: $install_hint"
|
|
170
203
|
fi
|
|
204
|
+
# #593 point 3: "not installed" and "installed but not found" need different
|
|
205
|
+
# fixes, so name every location that was searched.
|
|
206
|
+
if command -v describe_tool_search >/dev/null 2>&1; then
|
|
207
|
+
describe_tool_search "$tool_name"
|
|
208
|
+
fi
|
|
171
209
|
echo " Per QUALITY-GATES-CODE-OF-CONDUCT.md: tool unavailable = BLOCK, not SKIP"
|
|
172
210
|
return 1
|
|
173
211
|
}
|
package/adapters/gate-3.sh
CHANGED
|
@@ -109,7 +109,14 @@ else
|
|
|
109
109
|
GATE_3_STATUS="PASS"
|
|
110
110
|
fi
|
|
111
111
|
else
|
|
112
|
-
|
|
112
|
+
# #589: "no source files to check" described an empty PROJECT when the real
|
|
113
|
+
# cause is an empty CHANGED_FILES. The old wording read like a pass.
|
|
114
|
+
if [ -n "${GATE_SCOPE:-}" ] && [ "$GATE_SCOPE" = "all" ]; then
|
|
115
|
+
echo "⏭️ SKIPPED - Complexity: no tracked source files in this repository (scope: --all, whole tree)."
|
|
116
|
+
else
|
|
117
|
+
echo "⏭️ SKIPPED - Complexity: no changed source files in this run (scope: diff)."
|
|
118
|
+
echo " Run 'xp-gate check . --all' to scan the whole tree."
|
|
119
|
+
fi
|
|
113
120
|
GATE_3_STATUS="SKIP"
|
|
114
121
|
GATE_3_REASON="no_source_files_changed"
|
|
115
122
|
fi
|
package/adapters/gate-8.sh
CHANGED
|
@@ -33,10 +33,23 @@ fi
|
|
|
33
33
|
|
|
34
34
|
GITLEAKS_CONFIG=""
|
|
35
35
|
if [ -f ".gitleaks.toml" ]; then
|
|
36
|
-
# gitleaks is a native binary and may not understand MSYS paths, so pass an
|
|
36
|
+
# gitleaks is a native binary and may not understand MSYS/WSL paths, so pass an
|
|
37
37
|
# absolute path in the host's own format when we can derive one (#449).
|
|
38
|
-
|
|
38
|
+
#
|
|
39
|
+
# #582 found this the hard way: `cygpath` exists only in Git-for-Windows/MSYS, and on
|
|
40
|
+
# WSL its absence meant the fallback handed `/mnt/d/projects/<repo>/.gitleaks.toml`
|
|
41
|
+
# to the native gitleaks.exe. It could not open the file, exited 1, and Gate 8
|
|
42
|
+
# reported "Secrets detected". The conversion now lives in a shared library so the
|
|
43
|
+
# next gate handing a path to a native binary does not re-derive it (#581).
|
|
44
|
+
#
|
|
45
|
+
# This script may also run standalone (archived/copied hook) without the library, so
|
|
46
|
+
# the converter is used when present and the inline branch kept as the fallback.
|
|
47
|
+
if declare -F to_native_path >/dev/null 2>&1; then
|
|
48
|
+
GITLEAKS_CONFIG="--config=$(to_native_path "$(pwd)/.gitleaks.toml")"
|
|
49
|
+
elif command -v cygpath >/dev/null 2>&1; then
|
|
39
50
|
GITLEAKS_CONFIG="--config=$(cygpath -w "$(pwd)/.gitleaks.toml")"
|
|
51
|
+
elif command -v wslpath >/dev/null 2>&1; then
|
|
52
|
+
GITLEAKS_CONFIG="--config=$(wslpath -w "$(pwd)/.gitleaks.toml")"
|
|
40
53
|
else
|
|
41
54
|
GITLEAKS_CONFIG="--config=$(pwd)/.gitleaks.toml"
|
|
42
55
|
fi
|
|
@@ -75,10 +88,41 @@ if [ "$GITLEAKS_EXIT" -eq 0 ]; then
|
|
|
75
88
|
echo " ✅ PASSED - No secrets detected."
|
|
76
89
|
GATE_8_STATUS="PASS"
|
|
77
90
|
elif [ "$GITLEAKS_FINDINGS" = "?" ]; then
|
|
78
|
-
#
|
|
79
|
-
#
|
|
80
|
-
|
|
81
|
-
|
|
91
|
+
# #592: exit 0 with no usable report has TWO very different causes, and the
|
|
92
|
+
# operator can only act if we tell them which one happened:
|
|
93
|
+
#
|
|
94
|
+
# (a) the scan never ran — config unreadable, FTL, bad --report-path.
|
|
95
|
+
# Nothing was checked, so this must fail closed with the tool's own
|
|
96
|
+
# words, not be filed as "no evidence about the code".
|
|
97
|
+
# (b) the scan ran and found nothing, but the tool wrote no file (reported
|
|
98
|
+
# for gitleaks 8.30.1 on WSL; measured NOT to happen in git-bash with
|
|
99
|
+
# the same version, so the code must handle both). Only stdout is left,
|
|
100
|
+
# and stdout is weaker evidence than an artifact, so it still may not
|
|
101
|
+
# produce a PASS.
|
|
102
|
+
#
|
|
103
|
+
# The order matters and is the point: artifact first, tool failure second,
|
|
104
|
+
# tool output last (#449 established the principle; #592 re-opens it).
|
|
105
|
+
if printf '%s\n' "$GITLEAKS_OUTPUT" | grep -qiE '(^|[[:space:]])(FTL|fatal error)[[:space:]:]|unable to load'; then
|
|
106
|
+
echo " ❌ BLOCKED - ENVIRONMENT: gitleaks exited 0 but reported a failure, so"
|
|
107
|
+
echo " NO secret scan took place. This is a tooling problem, not a verdict"
|
|
108
|
+
echo " about the staged code."
|
|
109
|
+
printf '%s\n' "$GITLEAKS_OUTPUT" | grep -iE 'FTL|fatal|unable' | head -3 | sed 's/^/ /'
|
|
110
|
+
echo " Fix the gitleaks config/invocation above and commit again."
|
|
111
|
+
exit 1
|
|
112
|
+
fi
|
|
113
|
+
|
|
114
|
+
if printf '%s\n' "$GITLEAKS_OUTPUT" | grep -q 'no leaks found'; then
|
|
115
|
+
# The scan demonstrably ran and found nothing, but there is no artifact to
|
|
116
|
+
# prove it beyond this line. Reporting PASS from log wording is exactly what
|
|
117
|
+
# #449 forbade, so this stays SKIP — and says WHY, which the old message did
|
|
118
|
+
# not: without that, SKIP is indistinguishable from "tool missing".
|
|
119
|
+
echo " ⚠️ gitleaks scanned and reported no leaks, but wrote no report file"
|
|
120
|
+
echo " (${GITLEAKS_REPORT} missing). No artifact means no PASS (#449)."
|
|
121
|
+
echo " ⏭️ SKIPPED - secret scanning produced no artifact (scan reported clean)."
|
|
122
|
+
else
|
|
123
|
+
echo " ⚠️ gitleaks exited 0 but produced no readable report and no scan summary"
|
|
124
|
+
echo " ⏭️ SKIPPED - secret scanning produced no evidence (not a pass)"
|
|
125
|
+
fi
|
|
82
126
|
GATE_8_STATUS="SKIP"
|
|
83
127
|
else
|
|
84
128
|
echo " ❌ BLOCKED - gitleaks reported $GITLEAKS_FINDINGS finding(s)"
|
|
@@ -88,6 +132,23 @@ if [ "$GITLEAKS_EXIT" -eq 0 ]; then
|
|
|
88
132
|
exit 1
|
|
89
133
|
fi
|
|
90
134
|
elif [ "$GITLEAKS_EXIT" -eq 1 ]; then
|
|
135
|
+
# #582: gitleaks exits 1 for BOTH "leaks found" and "I could not start" (unreadable
|
|
136
|
+
# config, bad path, IO error). Treating every exit 1 as a finding produced the
|
|
137
|
+
# loudest possible wrong answer: "Secrets detected in staged files", plus
|
|
138
|
+
# instructions to rotate a key, on a repository where nothing had been scanned --
|
|
139
|
+
# because WSL handed a POSIX path to the native binary (#582's own case).
|
|
140
|
+
#
|
|
141
|
+
# #592 established this distinction for exit 0 + FTL. The same test applies here,
|
|
142
|
+
# and the order matters as much: a scanner that could not start must never be
|
|
143
|
+
# reported as having found something, or the gate teaches people to disable it.
|
|
144
|
+
if printf '%s\n' "$GITLEAKS_OUTPUT" | grep -qiE '(^|[[:space:]])(FTL|fatal error)[[:space:]:]|unable to load|cannot find the path'; then
|
|
145
|
+
echo " ❌ BLOCKED - ENVIRONMENT: gitleaks exited 1 because it could not start, so"
|
|
146
|
+
echo " NO secret scan took place. This is a tooling problem, not a verdict"
|
|
147
|
+
echo " about the staged code."
|
|
148
|
+
printf '%s\n' "$GITLEAKS_OUTPUT" | grep -iE 'FTL|fatal|unable|cannot find' | head -3 | sed 's/^/ /'
|
|
149
|
+
echo " Fix the gitleaks config/invocation above and commit again."
|
|
150
|
+
exit 1
|
|
151
|
+
fi
|
|
91
152
|
# Secrets found — output details
|
|
92
153
|
echo "$GITLEAKS_OUTPUT"
|
|
93
154
|
echo ""
|
package/adapters/go.sh
CHANGED
|
@@ -4,7 +4,24 @@
|
|
|
4
4
|
# Tools: go vet, golangci-lint, arch-go (architecture), go test
|
|
5
5
|
|
|
6
6
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
7
|
-
|
|
7
|
+
# #597: the library can be beside the adapters or one level up, depending on how
|
|
8
|
+
# the package was installed:
|
|
9
|
+
# global install ~/.config/xp-gate/adapters/adapter-common.sh (inside)
|
|
10
|
+
# npm package src/npm-package/adapters/../adapter-common.sh (above)
|
|
11
|
+
# The old single "$SCRIPT_DIR/../adapter-common.sh" only ever matched the second,
|
|
12
|
+
# so in the global layout the library never loaded - and "2>/dev/null || true"
|
|
13
|
+
# made that indistinguishable from success, leaving every shared function
|
|
14
|
+
# undefined with no warning.
|
|
15
|
+
for _ac_candidate in \
|
|
16
|
+
"$SCRIPT_DIR/adapter-common.sh" \
|
|
17
|
+
"$SCRIPT_DIR/../adapter-common.sh"; do
|
|
18
|
+
if [ -f "$_ac_candidate" ]; then
|
|
19
|
+
# shellcheck source=/dev/null
|
|
20
|
+
source "$_ac_candidate"
|
|
21
|
+
break
|
|
22
|
+
fi
|
|
23
|
+
done
|
|
24
|
+
unset _ac_candidate
|
|
8
25
|
|
|
9
26
|
run_static_analysis() {
|
|
10
27
|
require_tool go "Go SDK" || return 1
|
package/adapters/iac.sh
CHANGED
|
@@ -5,7 +5,24 @@
|
|
|
5
5
|
# Tools: checkov (primary), hadolint (Docker), kube-score (K8s)
|
|
6
6
|
|
|
7
7
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
8
|
-
|
|
8
|
+
# #597: the library can be beside the adapters or one level up, depending on how
|
|
9
|
+
# the package was installed:
|
|
10
|
+
# global install ~/.config/xp-gate/adapters/adapter-common.sh (inside)
|
|
11
|
+
# npm package src/npm-package/adapters/../adapter-common.sh (above)
|
|
12
|
+
# The old single "$SCRIPT_DIR/../adapter-common.sh" only ever matched the second,
|
|
13
|
+
# so in the global layout the library never loaded - and "2>/dev/null || true"
|
|
14
|
+
# made that indistinguishable from success, leaving every shared function
|
|
15
|
+
# undefined with no warning.
|
|
16
|
+
for _ac_candidate in \
|
|
17
|
+
"$SCRIPT_DIR/adapter-common.sh" \
|
|
18
|
+
"$SCRIPT_DIR/../adapter-common.sh"; do
|
|
19
|
+
if [ -f "$_ac_candidate" ]; then
|
|
20
|
+
# shellcheck source=/dev/null
|
|
21
|
+
source "$_ac_candidate"
|
|
22
|
+
break
|
|
23
|
+
fi
|
|
24
|
+
done
|
|
25
|
+
unset _ac_candidate
|
|
9
26
|
|
|
10
27
|
# Detect IaC files in changed files
|
|
11
28
|
detect_iac_files() {
|
package/adapters/powershell.sh
CHANGED
|
@@ -279,12 +279,48 @@ run_static_analysis() {
|
|
|
279
279
|
exit 0
|
|
280
280
|
" 2>&1) || PSSA_RC=$?
|
|
281
281
|
else
|
|
282
|
+
# #595: the unscoped path used to be a bare `-Path . -Recurse`, which walks
|
|
283
|
+
# node_modules. In a repo with Playwright installed that analyses
|
|
284
|
+
# playwright-core/bin/*.ps1 -- vendored files that use Write-Host and WMI
|
|
285
|
+
# cmdlets by design, that `npm ci` overwrites, and that the author cannot
|
|
286
|
+
# edit. Gate 1 then blocked EVERY commit, permanently. A gate that can never
|
|
287
|
+
# pass is not strict; it is one people learn to bypass.
|
|
288
|
+
#
|
|
289
|
+
# The adapter already had the right discovery: _find_powershell_files prunes
|
|
290
|
+
# .git, node_modules, dist, coverage and the rest. The two paths disagreed
|
|
291
|
+
# only because this one never called it. Now both use it, so "what this
|
|
292
|
+
# adapter refuses to analyse" has one definition.
|
|
293
|
+
local legacy_files
|
|
294
|
+
legacy_files=$(_find_powershell_files '*.ps1')
|
|
295
|
+
if [ -z "$legacy_files" ]; then
|
|
296
|
+
# Nothing to analyse is NOT a pass: say so via the reason protocol the
|
|
297
|
+
# module-missing branch uses, so it stays visible in the gate output.
|
|
298
|
+
echo "XP_G1_REASON: no PowerShell files found outside the pruned directories"
|
|
299
|
+
return 3
|
|
300
|
+
fi
|
|
301
|
+
local legacy_array
|
|
302
|
+
legacy_array=$(_powershell_path_array $legacy_files)
|
|
282
303
|
PSSA_OUT=$("$PWSH" "${POWERSHELL_ARGS[@]}" "
|
|
283
304
|
if (-not (Get-Module -ListAvailable -Name PSScriptAnalyzer)) {
|
|
284
305
|
Write-Output 'PSSA_MODULE_MISSING'
|
|
285
306
|
exit 0
|
|
286
307
|
}
|
|
287
|
-
\$
|
|
308
|
+
\$ErrorActionPreference = 'Stop'
|
|
309
|
+
try { Import-Module PSScriptAnalyzer -ErrorAction Stop | Out-Null } catch { exit 3 }
|
|
310
|
+
\$settings = Join-Path (Get-Location) 'PSScriptAnalyzerSettings.psd1'
|
|
311
|
+
\$results = @()
|
|
312
|
+
try {
|
|
313
|
+
foreach (\$p in $legacy_array) {
|
|
314
|
+
if (Test-Path -LiteralPath \$settings) {
|
|
315
|
+
\$results += @(Invoke-ScriptAnalyzer -Path \$p -Severity Error,Warning -Settings \$settings -ErrorAction Stop)
|
|
316
|
+
} else {
|
|
317
|
+
\$results += @(Invoke-ScriptAnalyzer -Path \$p -Severity Error,Warning -ErrorAction Stop)
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
} catch {
|
|
321
|
+
Write-Output ('PSSA_RUN_ERROR: ' + \$_.Exception.Message)
|
|
322
|
+
exit 3
|
|
323
|
+
}
|
|
288
324
|
if (\$results) {
|
|
289
325
|
\$results | Format-Table -AutoSize
|
|
290
326
|
exit 1
|
package/adapters/python.sh
CHANGED
|
@@ -4,10 +4,24 @@
|
|
|
4
4
|
# Tools: mypy, ruff/flake8, pytest, import-linter (architecture)
|
|
5
5
|
|
|
6
6
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
7
|
-
#
|
|
8
|
-
#
|
|
9
|
-
#
|
|
10
|
-
|
|
7
|
+
# #597: the library can be beside the adapters or one level up, depending on how
|
|
8
|
+
# the package was installed:
|
|
9
|
+
# global install ~/.config/xp-gate/adapters/adapter-common.sh (inside)
|
|
10
|
+
# npm package src/npm-package/adapters/../adapter-common.sh (above)
|
|
11
|
+
# The old single "$SCRIPT_DIR/../adapter-common.sh" only ever matched the second,
|
|
12
|
+
# so in the global layout the library never loaded - and "2>/dev/null || true"
|
|
13
|
+
# made that indistinguishable from success, leaving every shared function
|
|
14
|
+
# undefined with no warning.
|
|
15
|
+
for _ac_candidate in \
|
|
16
|
+
"$SCRIPT_DIR/adapter-common.sh" \
|
|
17
|
+
"$SCRIPT_DIR/../adapter-common.sh"; do
|
|
18
|
+
if [ -f "$_ac_candidate" ]; then
|
|
19
|
+
# shellcheck source=/dev/null
|
|
20
|
+
source "$_ac_candidate"
|
|
21
|
+
break
|
|
22
|
+
fi
|
|
23
|
+
done
|
|
24
|
+
unset _ac_candidate
|
|
11
25
|
|
|
12
26
|
# A mypy target must exist AND actually contain .py files: a bare directory
|
|
13
27
|
# check lets through TS repos' src/ (mypy exits 2 with "There are no .py[i]
|
package/gate-3.sh
CHANGED
|
@@ -233,7 +233,16 @@ else
|
|
|
233
233
|
fi
|
|
234
234
|
fi
|
|
235
235
|
else
|
|
236
|
-
|
|
236
|
+
# #589: "no source files to check" described an empty PROJECT when the real
|
|
237
|
+
# cause is an empty CHANGED_FILES — an empty DIFF, or a `--all` run whose
|
|
238
|
+
# tree genuinely has no sources. The old wording read like a pass, which is
|
|
239
|
+
# why the reporter worked around the gate by running lizard by hand.
|
|
240
|
+
if [ -n "${GATE_SCOPE:-}" ] && [ "$GATE_SCOPE" = "all" ]; then
|
|
241
|
+
echo "⏭️ SKIPPED - Complexity: no tracked source files in this repository (scope: --all, whole tree)."
|
|
242
|
+
else
|
|
243
|
+
echo "⏭️ SKIPPED - Complexity: no changed source files in this run (scope: diff)."
|
|
244
|
+
echo " Run 'xp-gate check . --all' to scan the whole tree."
|
|
245
|
+
fi
|
|
237
246
|
GATE_3_STATUS="SKIP"
|
|
238
247
|
# Consumed by the sourcing parent (pre-commit report JSON), not here.
|
|
239
248
|
# shellcheck disable=SC2034
|
package/gate-8.sh
CHANGED
|
@@ -33,10 +33,23 @@ fi
|
|
|
33
33
|
|
|
34
34
|
GITLEAKS_CONFIG=""
|
|
35
35
|
if [ -f ".gitleaks.toml" ]; then
|
|
36
|
-
# gitleaks is a native binary and may not understand MSYS paths, so pass an
|
|
36
|
+
# gitleaks is a native binary and may not understand MSYS/WSL paths, so pass an
|
|
37
37
|
# absolute path in the host's own format when we can derive one (#449).
|
|
38
|
-
|
|
38
|
+
#
|
|
39
|
+
# #582 found this the hard way: `cygpath` exists only in Git-for-Windows/MSYS, and on
|
|
40
|
+
# WSL its absence meant the fallback handed `/mnt/d/projects/<repo>/.gitleaks.toml`
|
|
41
|
+
# to the native gitleaks.exe. It could not open the file, exited 1, and Gate 8
|
|
42
|
+
# reported "Secrets detected". The conversion now lives in a shared library so the
|
|
43
|
+
# next gate handing a path to a native binary does not re-derive it (#581).
|
|
44
|
+
#
|
|
45
|
+
# This script may also run standalone (archived/copied hook) without the library, so
|
|
46
|
+
# the converter is used when present and the inline branch kept as the fallback.
|
|
47
|
+
if declare -F to_native_path >/dev/null 2>&1; then
|
|
48
|
+
GITLEAKS_CONFIG="--config=$(to_native_path "$(pwd)/.gitleaks.toml")"
|
|
49
|
+
elif command -v cygpath >/dev/null 2>&1; then
|
|
39
50
|
GITLEAKS_CONFIG="--config=$(cygpath -w "$(pwd)/.gitleaks.toml")"
|
|
51
|
+
elif command -v wslpath >/dev/null 2>&1; then
|
|
52
|
+
GITLEAKS_CONFIG="--config=$(wslpath -w "$(pwd)/.gitleaks.toml")"
|
|
40
53
|
else
|
|
41
54
|
GITLEAKS_CONFIG="--config=$(pwd)/.gitleaks.toml"
|
|
42
55
|
fi
|
|
@@ -75,10 +88,41 @@ if [ "$GITLEAKS_EXIT" -eq 0 ]; then
|
|
|
75
88
|
echo " ✅ PASSED - No secrets detected."
|
|
76
89
|
GATE_8_STATUS="PASS"
|
|
77
90
|
elif [ "$GITLEAKS_FINDINGS" = "?" ]; then
|
|
78
|
-
#
|
|
79
|
-
#
|
|
80
|
-
|
|
81
|
-
|
|
91
|
+
# #592: exit 0 with no usable report has TWO very different causes, and the
|
|
92
|
+
# operator can only act if we tell them which one happened:
|
|
93
|
+
#
|
|
94
|
+
# (a) the scan never ran — config unreadable, FTL, bad --report-path.
|
|
95
|
+
# Nothing was checked, so this must fail closed with the tool's own
|
|
96
|
+
# words, not be filed as "no evidence about the code".
|
|
97
|
+
# (b) the scan ran and found nothing, but the tool wrote no file (reported
|
|
98
|
+
# for gitleaks 8.30.1 on WSL; measured NOT to happen in git-bash with
|
|
99
|
+
# the same version, so the code must handle both). Only stdout is left,
|
|
100
|
+
# and stdout is weaker evidence than an artifact, so it still may not
|
|
101
|
+
# produce a PASS.
|
|
102
|
+
#
|
|
103
|
+
# The order matters and is the point: artifact first, tool failure second,
|
|
104
|
+
# tool output last (#449 established the principle; #592 re-opens it).
|
|
105
|
+
if printf '%s\n' "$GITLEAKS_OUTPUT" | grep -qiE '(^|[[:space:]])(FTL|fatal error)[[:space:]:]|unable to load'; then
|
|
106
|
+
echo " ❌ BLOCKED - ENVIRONMENT: gitleaks exited 0 but reported a failure, so"
|
|
107
|
+
echo " NO secret scan took place. This is a tooling problem, not a verdict"
|
|
108
|
+
echo " about the staged code."
|
|
109
|
+
printf '%s\n' "$GITLEAKS_OUTPUT" | grep -iE 'FTL|fatal|unable' | head -3 | sed 's/^/ /'
|
|
110
|
+
echo " Fix the gitleaks config/invocation above and commit again."
|
|
111
|
+
exit 1
|
|
112
|
+
fi
|
|
113
|
+
|
|
114
|
+
if printf '%s\n' "$GITLEAKS_OUTPUT" | grep -q 'no leaks found'; then
|
|
115
|
+
# The scan demonstrably ran and found nothing, but there is no artifact to
|
|
116
|
+
# prove it beyond this line. Reporting PASS from log wording is exactly what
|
|
117
|
+
# #449 forbade, so this stays SKIP — and says WHY, which the old message did
|
|
118
|
+
# not: without that, SKIP is indistinguishable from "tool missing".
|
|
119
|
+
echo " ⚠️ gitleaks scanned and reported no leaks, but wrote no report file"
|
|
120
|
+
echo " (${GITLEAKS_REPORT} missing). No artifact means no PASS (#449)."
|
|
121
|
+
echo " ⏭️ SKIPPED - secret scanning produced no artifact (scan reported clean)."
|
|
122
|
+
else
|
|
123
|
+
echo " ⚠️ gitleaks exited 0 but produced no readable report and no scan summary"
|
|
124
|
+
echo " ⏭️ SKIPPED - secret scanning produced no evidence (not a pass)"
|
|
125
|
+
fi
|
|
82
126
|
GATE_8_STATUS="SKIP"
|
|
83
127
|
else
|
|
84
128
|
echo " ❌ BLOCKED - gitleaks reported $GITLEAKS_FINDINGS finding(s)"
|
|
@@ -88,6 +132,23 @@ if [ "$GITLEAKS_EXIT" -eq 0 ]; then
|
|
|
88
132
|
exit 1
|
|
89
133
|
fi
|
|
90
134
|
elif [ "$GITLEAKS_EXIT" -eq 1 ]; then
|
|
135
|
+
# #582: gitleaks exits 1 for BOTH "leaks found" and "I could not start" (unreadable
|
|
136
|
+
# config, bad path, IO error). Treating every exit 1 as a finding produced the
|
|
137
|
+
# loudest possible wrong answer: "Secrets detected in staged files", plus
|
|
138
|
+
# instructions to rotate a key, on a repository where nothing had been scanned --
|
|
139
|
+
# because WSL handed a POSIX path to the native binary (#582's own case).
|
|
140
|
+
#
|
|
141
|
+
# #592 established this distinction for exit 0 + FTL. The same test applies here,
|
|
142
|
+
# and the order matters as much: a scanner that could not start must never be
|
|
143
|
+
# reported as having found something, or the gate teaches people to disable it.
|
|
144
|
+
if printf '%s\n' "$GITLEAKS_OUTPUT" | grep -qiE '(^|[[:space:]])(FTL|fatal error)[[:space:]:]|unable to load|cannot find the path'; then
|
|
145
|
+
echo " ❌ BLOCKED - ENVIRONMENT: gitleaks exited 1 because it could not start, so"
|
|
146
|
+
echo " NO secret scan took place. This is a tooling problem, not a verdict"
|
|
147
|
+
echo " about the staged code."
|
|
148
|
+
printf '%s\n' "$GITLEAKS_OUTPUT" | grep -iE 'FTL|fatal|unable|cannot find' | head -3 | sed 's/^/ /'
|
|
149
|
+
echo " Fix the gitleaks config/invocation above and commit again."
|
|
150
|
+
exit 1
|
|
151
|
+
fi
|
|
91
152
|
# Secrets found — output details
|
|
92
153
|
echo "$GITLEAKS_OUTPUT"
|
|
93
154
|
echo ""
|
package/hooks/adapter-common.sh
CHANGED
|
@@ -155,19 +155,57 @@ require_tool() {
|
|
|
155
155
|
local tool_name="$1"
|
|
156
156
|
local gate_name="${2:-Gate}"
|
|
157
157
|
local install_hint="${3:-}"
|
|
158
|
-
|
|
158
|
+
|
|
159
|
+
# #593: on Windows the hook runs under git-bash, which does not see the
|
|
160
|
+
# toolchains installed on the Windows side (%APPDATA%\npm, the node dir,
|
|
161
|
+
# pip --user Scripts, ~/bin). Bootstrapping before probing turns "not on this
|
|
162
|
+
# shell's PATH" back into "found", instead of reporting an installed tool as
|
|
163
|
+
# missing. lib/toolchain-path.sh is sourced by pre-commit before this runs; the
|
|
164
|
+
# guard keeps require_tool usable from adapters that load on their own.
|
|
165
|
+
if command -v bootstrap_toolchain_path >/dev/null 2>&1; then
|
|
166
|
+
bootstrap_toolchain_path
|
|
167
|
+
fi
|
|
168
|
+
|
|
169
|
+
# #580: on WSL (appendWindowsPath=false) and in git-bash, `command -v node`
|
|
170
|
+
# returns 1 while node.exe sits in the same directory and runs. One root cause
|
|
171
|
+
# gave Gate 1 a silent SKIP, Gate 1 a false "Biome check failed" and Gate 8 a
|
|
172
|
+
# false "gitleaks not installed" — an environment fault reported as a code
|
|
173
|
+
# verdict in all three. tool_is_available also counts the .exe suffix and the
|
|
174
|
+
# XP_GATE_<TOOL>_BIN override.
|
|
175
|
+
if command -v tool_is_available >/dev/null 2>&1 && tool_is_available "$tool_name"; then
|
|
176
|
+
return 0
|
|
177
|
+
fi
|
|
178
|
+
|
|
159
179
|
if command -v "$tool_name" >/dev/null 2>&1; then
|
|
160
180
|
return 0
|
|
161
181
|
fi
|
|
162
|
-
|
|
182
|
+
|
|
183
|
+
if command -v "${tool_name}.exe" >/dev/null 2>&1; then
|
|
184
|
+
return 0
|
|
185
|
+
fi
|
|
186
|
+
|
|
163
187
|
if command -v npx >/dev/null 2>&1 && npx --no-install "$tool_name" --version >/dev/null 2>&1; then
|
|
164
188
|
return 0
|
|
165
189
|
fi
|
|
166
|
-
|
|
190
|
+
|
|
191
|
+
# #593: monorepos keep the tool in a subproject's node_modules/.bin, where
|
|
192
|
+
# neither PATH nor `npx` from the repo root can see it.
|
|
193
|
+
if command -v resolve_tool_path >/dev/null 2>&1 && resolve_tool_path "$tool_name" >/dev/null 2>&1; then
|
|
194
|
+
if command -v ensure_tool_dir_on_path >/dev/null 2>&1; then
|
|
195
|
+
ensure_tool_dir_on_path "$tool_name" || true
|
|
196
|
+
fi
|
|
197
|
+
return 0
|
|
198
|
+
fi
|
|
199
|
+
|
|
167
200
|
echo "❌ BLOCKED - Required tool '$tool_name' not available for $gate_name"
|
|
168
201
|
if [ -n "$install_hint" ]; then
|
|
169
202
|
echo " Install: $install_hint"
|
|
170
203
|
fi
|
|
204
|
+
# #593 point 3: "not installed" and "installed but not found" need different
|
|
205
|
+
# fixes, so name every location that was searched.
|
|
206
|
+
if command -v describe_tool_search >/dev/null 2>&1; then
|
|
207
|
+
describe_tool_search "$tool_name"
|
|
208
|
+
fi
|
|
171
209
|
echo " Per QUALITY-GATES-CODE-OF-CONDUCT.md: tool unavailable = BLOCK, not SKIP"
|
|
172
210
|
return 1
|
|
173
211
|
}
|
package/hooks/gate-3.sh
CHANGED
|
@@ -233,7 +233,16 @@ else
|
|
|
233
233
|
fi
|
|
234
234
|
fi
|
|
235
235
|
else
|
|
236
|
-
|
|
236
|
+
# #589: "no source files to check" described an empty PROJECT when the real
|
|
237
|
+
# cause is an empty CHANGED_FILES — an empty DIFF, or a `--all` run whose
|
|
238
|
+
# tree genuinely has no sources. The old wording read like a pass, which is
|
|
239
|
+
# why the reporter worked around the gate by running lizard by hand.
|
|
240
|
+
if [ -n "${GATE_SCOPE:-}" ] && [ "$GATE_SCOPE" = "all" ]; then
|
|
241
|
+
echo "⏭️ SKIPPED - Complexity: no tracked source files in this repository (scope: --all, whole tree)."
|
|
242
|
+
else
|
|
243
|
+
echo "⏭️ SKIPPED - Complexity: no changed source files in this run (scope: diff)."
|
|
244
|
+
echo " Run 'xp-gate check . --all' to scan the whole tree."
|
|
245
|
+
fi
|
|
237
246
|
GATE_3_STATUS="SKIP"
|
|
238
247
|
# Consumed by the sourcing parent (pre-commit report JSON), not here.
|
|
239
248
|
# shellcheck disable=SC2034
|
package/hooks/gate-8.sh
CHANGED
|
@@ -33,10 +33,23 @@ fi
|
|
|
33
33
|
|
|
34
34
|
GITLEAKS_CONFIG=""
|
|
35
35
|
if [ -f ".gitleaks.toml" ]; then
|
|
36
|
-
# gitleaks is a native binary and may not understand MSYS paths, so pass an
|
|
36
|
+
# gitleaks is a native binary and may not understand MSYS/WSL paths, so pass an
|
|
37
37
|
# absolute path in the host's own format when we can derive one (#449).
|
|
38
|
-
|
|
38
|
+
#
|
|
39
|
+
# #582 found this the hard way: `cygpath` exists only in Git-for-Windows/MSYS, and on
|
|
40
|
+
# WSL its absence meant the fallback handed `/mnt/d/projects/<repo>/.gitleaks.toml`
|
|
41
|
+
# to the native gitleaks.exe. It could not open the file, exited 1, and Gate 8
|
|
42
|
+
# reported "Secrets detected". The conversion now lives in a shared library so the
|
|
43
|
+
# next gate handing a path to a native binary does not re-derive it (#581).
|
|
44
|
+
#
|
|
45
|
+
# This script may also run standalone (archived/copied hook) without the library, so
|
|
46
|
+
# the converter is used when present and the inline branch kept as the fallback.
|
|
47
|
+
if declare -F to_native_path >/dev/null 2>&1; then
|
|
48
|
+
GITLEAKS_CONFIG="--config=$(to_native_path "$(pwd)/.gitleaks.toml")"
|
|
49
|
+
elif command -v cygpath >/dev/null 2>&1; then
|
|
39
50
|
GITLEAKS_CONFIG="--config=$(cygpath -w "$(pwd)/.gitleaks.toml")"
|
|
51
|
+
elif command -v wslpath >/dev/null 2>&1; then
|
|
52
|
+
GITLEAKS_CONFIG="--config=$(wslpath -w "$(pwd)/.gitleaks.toml")"
|
|
40
53
|
else
|
|
41
54
|
GITLEAKS_CONFIG="--config=$(pwd)/.gitleaks.toml"
|
|
42
55
|
fi
|
|
@@ -75,10 +88,41 @@ if [ "$GITLEAKS_EXIT" -eq 0 ]; then
|
|
|
75
88
|
echo " ✅ PASSED - No secrets detected."
|
|
76
89
|
GATE_8_STATUS="PASS"
|
|
77
90
|
elif [ "$GITLEAKS_FINDINGS" = "?" ]; then
|
|
78
|
-
#
|
|
79
|
-
#
|
|
80
|
-
|
|
81
|
-
|
|
91
|
+
# #592: exit 0 with no usable report has TWO very different causes, and the
|
|
92
|
+
# operator can only act if we tell them which one happened:
|
|
93
|
+
#
|
|
94
|
+
# (a) the scan never ran — config unreadable, FTL, bad --report-path.
|
|
95
|
+
# Nothing was checked, so this must fail closed with the tool's own
|
|
96
|
+
# words, not be filed as "no evidence about the code".
|
|
97
|
+
# (b) the scan ran and found nothing, but the tool wrote no file (reported
|
|
98
|
+
# for gitleaks 8.30.1 on WSL; measured NOT to happen in git-bash with
|
|
99
|
+
# the same version, so the code must handle both). Only stdout is left,
|
|
100
|
+
# and stdout is weaker evidence than an artifact, so it still may not
|
|
101
|
+
# produce a PASS.
|
|
102
|
+
#
|
|
103
|
+
# The order matters and is the point: artifact first, tool failure second,
|
|
104
|
+
# tool output last (#449 established the principle; #592 re-opens it).
|
|
105
|
+
if printf '%s\n' "$GITLEAKS_OUTPUT" | grep -qiE '(^|[[:space:]])(FTL|fatal error)[[:space:]:]|unable to load'; then
|
|
106
|
+
echo " ❌ BLOCKED - ENVIRONMENT: gitleaks exited 0 but reported a failure, so"
|
|
107
|
+
echo " NO secret scan took place. This is a tooling problem, not a verdict"
|
|
108
|
+
echo " about the staged code."
|
|
109
|
+
printf '%s\n' "$GITLEAKS_OUTPUT" | grep -iE 'FTL|fatal|unable' | head -3 | sed 's/^/ /'
|
|
110
|
+
echo " Fix the gitleaks config/invocation above and commit again."
|
|
111
|
+
exit 1
|
|
112
|
+
fi
|
|
113
|
+
|
|
114
|
+
if printf '%s\n' "$GITLEAKS_OUTPUT" | grep -q 'no leaks found'; then
|
|
115
|
+
# The scan demonstrably ran and found nothing, but there is no artifact to
|
|
116
|
+
# prove it beyond this line. Reporting PASS from log wording is exactly what
|
|
117
|
+
# #449 forbade, so this stays SKIP — and says WHY, which the old message did
|
|
118
|
+
# not: without that, SKIP is indistinguishable from "tool missing".
|
|
119
|
+
echo " ⚠️ gitleaks scanned and reported no leaks, but wrote no report file"
|
|
120
|
+
echo " (${GITLEAKS_REPORT} missing). No artifact means no PASS (#449)."
|
|
121
|
+
echo " ⏭️ SKIPPED - secret scanning produced no artifact (scan reported clean)."
|
|
122
|
+
else
|
|
123
|
+
echo " ⚠️ gitleaks exited 0 but produced no readable report and no scan summary"
|
|
124
|
+
echo " ⏭️ SKIPPED - secret scanning produced no evidence (not a pass)"
|
|
125
|
+
fi
|
|
82
126
|
GATE_8_STATUS="SKIP"
|
|
83
127
|
else
|
|
84
128
|
echo " ❌ BLOCKED - gitleaks reported $GITLEAKS_FINDINGS finding(s)"
|
|
@@ -88,6 +132,23 @@ if [ "$GITLEAKS_EXIT" -eq 0 ]; then
|
|
|
88
132
|
exit 1
|
|
89
133
|
fi
|
|
90
134
|
elif [ "$GITLEAKS_EXIT" -eq 1 ]; then
|
|
135
|
+
# #582: gitleaks exits 1 for BOTH "leaks found" and "I could not start" (unreadable
|
|
136
|
+
# config, bad path, IO error). Treating every exit 1 as a finding produced the
|
|
137
|
+
# loudest possible wrong answer: "Secrets detected in staged files", plus
|
|
138
|
+
# instructions to rotate a key, on a repository where nothing had been scanned --
|
|
139
|
+
# because WSL handed a POSIX path to the native binary (#582's own case).
|
|
140
|
+
#
|
|
141
|
+
# #592 established this distinction for exit 0 + FTL. The same test applies here,
|
|
142
|
+
# and the order matters as much: a scanner that could not start must never be
|
|
143
|
+
# reported as having found something, or the gate teaches people to disable it.
|
|
144
|
+
if printf '%s\n' "$GITLEAKS_OUTPUT" | grep -qiE '(^|[[:space:]])(FTL|fatal error)[[:space:]:]|unable to load|cannot find the path'; then
|
|
145
|
+
echo " ❌ BLOCKED - ENVIRONMENT: gitleaks exited 1 because it could not start, so"
|
|
146
|
+
echo " NO secret scan took place. This is a tooling problem, not a verdict"
|
|
147
|
+
echo " about the staged code."
|
|
148
|
+
printf '%s\n' "$GITLEAKS_OUTPUT" | grep -iE 'FTL|fatal|unable|cannot find' | head -3 | sed 's/^/ /'
|
|
149
|
+
echo " Fix the gitleaks config/invocation above and commit again."
|
|
150
|
+
exit 1
|
|
151
|
+
fi
|
|
91
152
|
# Secrets found — output details
|
|
92
153
|
echo "$GITLEAKS_OUTPUT"
|
|
93
154
|
echo ""
|
|
@@ -81,12 +81,27 @@ enforce_python_coverage() {
|
|
|
81
81
|
echo " Run the full suite for a real Python coverage check."
|
|
82
82
|
return 0
|
|
83
83
|
fi
|
|
84
|
-
echo "❌ BLOCKED - Python coverage ${COVERAGE_PERCENT}% below 80% threshold"
|
|
84
|
+
echo "❌ BLOCKED - Python coverage ${COVERAGE_PERCENT}% below ${COVERAGE_THRESHOLD:-80}% threshold"
|
|
85
85
|
return 1
|
|
86
86
|
fi
|
|
87
|
-
echo "Python coverage: ${COVERAGE_PERCENT}% ✅ (≥ 80%)"
|
|
88
|
-
|
|
89
|
-
|
|
87
|
+
echo "Python coverage: ${COVERAGE_PERCENT}% ✅ (≥ ${COVERAGE_THRESHOLD:-80}%)"
|
|
88
|
+
return 0
|
|
89
|
+
fi
|
|
90
|
+
|
|
91
|
+
# #570: reaching here means the gate produced NO measurement. That is a FAILED
|
|
92
|
+
# answer to "is coverage at or above the floor?", not a lenient one — the gate
|
|
93
|
+
# exists to answer that question and answered nothing. Warning and returning 0
|
|
94
|
+
# made the gate fail-open: deleting .coverage (or a pytest run that died before
|
|
95
|
+
# writing it) disabled the coverage check while the summary still looked clean.
|
|
96
|
+
#
|
|
97
|
+
# The one legitimate exception is a subset run: nothing was measured because
|
|
98
|
+
# only part of the suite was run, so there is no total to compare.
|
|
99
|
+
if [ "${PARTIAL_TEST_RUN:-false}" = "true" ]; then
|
|
100
|
+
echo "⚠️ Could not determine Python coverage percentage (subset test run) — not blocking."
|
|
101
|
+
return 0
|
|
90
102
|
fi
|
|
91
|
-
|
|
103
|
+
echo "❌ BLOCKED - Could not determine Python coverage percentage."
|
|
104
|
+
echo " No .coverage / coverage report was produced, so the ${COVERAGE_THRESHOLD:-80}% floor"
|
|
105
|
+
echo " cannot be verified. Run the suite with coverage (pytest --cov) and commit again."
|
|
106
|
+
return 1
|
|
92
107
|
}
|