fix(ci): report what the Kubescape scan actually found - #2847
Conversation
The scan's console table shows only a subset of its findings: a scan whose SARIF carried 4 findings printed 'All controls passed. No issues found', and one carrying 70 listed a single control. Since those findings became Code Scanning alerts, the CI log and the Security tab disagreed — and the log is what a developer reads on a PR. Summarize the SARIF the run already writes, so both surfaces report from one source. Informational only; the compliance threshold remains the sole gate. Fixes #2846
…ith no rule id Self-review found two gaps: the new script was absent from the path filter that runs its shellcheck, so editing it alone would never check it; and a SARIF result carrying no ruleId aborted the summary, because indexing the rule table with null is an error rather than a miss.
✅
|
@coderabbitai review |
|
✅ Action performedReview finished.
|
|
Warning Review limit reached
Next review available in: 42 minutes Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available. How can I continue?After more reviews become available, a review can be triggered using the To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews. How do review limits work?CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability. For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window. Please refer docs for additional details. Review details⚙️ Run configurationConfiguration used: Repository YAML (base), Organization UI (inherited) Review profile: ASSERTIVE Plan: Pro Plus Run ID: 📒 Files selected for processing (4)
📝 WalkthroughWalkthroughAdds a Bash utility that parses Kubescape SARIF output, aggregates findings by control, prints reports, and optionally writes GitHub step summaries. Adds tests for SARIF shapes, failure modes, rule resolution, output formatting, normalization behavior, and CI wiring. Updates validation workflows to preserve raw SARIF before normalization and run the summary as a non-blocking post-scan step. Compliance-threshold gating and uploaded SARIF remain unchanged. Possibly related issues
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@scripts/summarize-sarif-findings.sh`:
- Around line 48-85: Add a dedicated test script for the jq report logic in
summarize-sarif-findings.sh, following the repository’s existing script-test
conventions. Cover minimal SARIF fixtures for zero findings, a result without
ruleId, and multiple controls, asserting the generated summaries and counts so
regressions between SARIF findings and console output are detected.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Repository YAML (base), Organization UI (inherited)
Review profile: ASSERTIVE
Plan: Pro Plus
Run ID: b1a343e1-d682-4028-9029-6f793e49c6e3
📒 Files selected for processing (2)
.github/workflows/ci.yamlscripts/summarize-sarif-findings.sh
📜 Review details
🧰 Additional context used
📓 Path-based instructions (2)
**/*
📄 CodeRabbit inference engine (AGENTS.md)
**/*: Use Conventional Commit messages and titles, keep changes small and focused, and create pull requests as drafts.
Land new behavior disabled, validate it, then enable it using the appropriate layer: OpenFeature for per-request flags, Flagger for traffic shifting, Helm values/Kustomize for component toggles, and Kubernetes feature gates for platform behavior.
Files:
scripts/summarize-sarif-findings.sh
.github/workflows/*.yaml
📄 CodeRabbit inference engine (AGENTS.md)
CI for Kubernetes-related changes must remain static: run validation and Kubescape scanning without starting a cluster.
Files:
.github/workflows/ci.yaml
🧠 Learnings (1)
📚 Learning: 2026-07-28T00:33:10.828Z
Learnt from: devantler
Repo: devantler-tech/platform PR: 2825
File: .github/workflows/dr-rebuild.yaml:70-73
Timestamp: 2026-07-28T00:33:10.828Z
Learning: For GitHub Actions concurrency configuration in this repo, don’t use `queue: single` on a shared production concurrency group (e.g., `prod-deploy`) that is used for normal production deployments—`queue: single` will cancel/replace any pending run in that group. Normal production deployments must use a uniform `queue: max` for that shared group to ensure serialization without coalescing. If you need DR rebuild coalescing (replacement of pending DR rebuild runs), it must use a separate DR-only concurrency scope/group (not the shared production `prod-deploy` group).
Applied to files:
.github/workflows/ci.yaml
🔇 Additional comments (3)
scripts/summarize-sarif-findings.sh (1)
1-99: LGTM!The jq pipeline is carefully defended against the SARIF shape variance it documents (missing
.runs, missing rule metadata, results with noruleId), and the null-ruleIdguard before indexing$rules[$id]is required — jq raisesCannot index object with nullotherwise, so this isn't just defensive noise..github/workflows/ci.yaml (2)
63-63: LGTM!
457-482: 🩺 Stability & Availability | ⚡ Quick winStep can still fail the build despite being described as "cannot fail the build over findings."
scripts/summarize-sarif-findings.shdeliberately exits non-zero on a parse/usage error (its own header: "a broken summary is worth surfacing, but the caller decides whether that is fatal"). This step has nocontinue-on-error, so under the runner's defaultrun:shell (bash -e), such a failure fails thevalidatejob — a path unrelated to the compliance-threshold gate the PR objectives say must be preserved. In practice this should be rare sincenormalize-sarifalready validates the file, but as written the "informational only, cannot fail the build" comment (lines 474-476) isn't strictly guaranteed.If a broken summary should never block merge, add
continue-on-error: true; if it's intentional (a truly malformed SARIF is worth failing loudly), consider updating the comment to say so explicitly.🛡️ Optional fix to guarantee non-blocking behavior
- name: 📋 Summarize Kubescape findings + continue-on-error: true # Kubescape's own console table shows only a subset of what it finds:
The summary fails by under-reporting rather than by crashing, so a dropped control reads as a clean scan. Pin the grouping, counts and ordering, plus the two SARIF shapes that abort a naive jq pipeline: a result with no ruleId, and a ruleId absent from the rule table. Also stop the summary step from acting as a second merge gate. The script exits non-zero on a SARIF it cannot parse and leaves the caller to decide whether that is fatal; this caller had made no decision, so a reporting bug failed the validate job the step's own comment said it could not fail.
Fixed in
Wired in both directions, so it cannot decay silently: a new 8 ablations, each RED on its own assertion (not merely red):
Also acted on the |
@coderabbitai review Head |
|
✅ Action performedReview finished.
|
User evaluation at
So the new guard runs where it was wired rather than only locally, and adding Locally: 8 ablations each RED on their own assertion (table in the thread above), |
CodeRabbit lane: Worth recording precisely, because that same comment contains Advancing to Codex (lane 2) rather than waiting out the window. |
@codex review |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: f45f163717
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
normalize-sarif-paths.sh drops results with an empty artifactLocation.uri because Code Scanning cannot anchor an alert without a file. Summarizing that file therefore omitted cluster-level controls with no manifest behind them — under-reporting the exact findings this summary exists to surface. Summarize a copy taken before normalization; the upload still reads the normalized file. Also summarize on push to main. ci.yaml scans on pull_request only, so the kubescape-baseline job is the sole default-branch scan; without a summary there the log still reads as a clean scan while the same run uploads findings.
Both P2s verified against the code and both fixed in 1. "Summarize before dropping locationless findings" — correct, and the more serious of the two. | .results |= map(select((.locations[0].physicalLocation.artifactLocation.uri // "") != ""))So summarizing the normalized file omits cluster-level controls with no manifest behind them — the precise class of finding that is otherwise invisible, which is what this PR exists to fix. The step comment at Fixed by splitting the two consumers: 2. "Summarize the default-branch scan too" — also correct. The guard is wiring-level, because that is what broke. A test over the script alone would have passed throughout. Both workflows are now asserted to copy before normalizing and to summarize the raw file, with an explicit rejection of the normalized path. Plus a differential that keeps the split honest: it runs the real normalizer over a fixture carrying one located and one locationless finding, asserts the normalizer drops it ( 4 new ablations, each RED on its own assertion:
|
@coderabbitai review Head |
|
Tip For best results, initiate chat on the files or code changes. 🧩 Analysis chain🏁 Script executed: #!/bin/bash
set -euo pipefail
echo '=== Changed files and patch at requested head ==='
git diff --stat f45f1637..ffcf6531
git diff --find-renames --find-copies f45f1637..ffcf6531 -- \
.github/workflows/ci.yaml \
.github/workflows/validate-main.yaml \
scripts/summarize-sarif-findings.sh \
scripts/normalize-sarif-paths.sh \
scripts/tests/test-summarize-sarif-findings.sh
echo '=== Workflow/script outlines ==='
ast-grep outline .github/workflows/ci.yaml --items all || true
ast-grep outline .github/workflows/validate-main.yaml --items all || true
ast-grep outline scripts/summarize-sarif-findings.sh --items all || true
ast-grep outline scripts/tests/test-summarize-sarif-findings.sh --items all || true
echo '=== Relevant workflow references ==='
rg -n -C 8 \
'kubescape\.(raw\.)?sarif|normalize-sarif|summarize-sarif|Summarize Kubescape|Validate Kubescape|continue-on-error' \
.github/workflows/ci.yaml .github/workflows/validate-main.yaml
echo '=== Summary script and regression test ==='
cat -n scripts/summarize-sarif-findings.sh
cat -n scripts/tests/test-summarize-sarif-findings.sh
echo '=== Normalizer result filtering ==='
rg -n -C 12 'results|artifactLocation|physicalLocation|locations' scripts/normalize-sarif-paths.sh
echo '=== Repository test conventions and workflow references ==='
fd '^test-.*\.sh$' scripts/tests -x sh -c 'echo --- "$1"; sed -n "1,100p" "$1"' sh {} \; | head -n 500
rg -n -C 4 'test-summarize-sarif-findings\.sh|summarize-sarif-findings\.sh' .Length of output: 75762 🏁 Script executed: #!/bin/bash
set -euo pipefail
base="$(git merge-base HEAD origin/main 2>/dev/null || git merge-base HEAD main)"
echo "=== Base and complete PR file list ==="
printf 'base=%s\nhead=%s\n' "$base" "$(git rev-parse HEAD)"
git diff --name-status "$base"..HEAD
git diff --check "$base"..HEAD
echo '=== Complete PR patches ==='
git diff --find-renames "$base"..HEAD -- \
.github/workflows/ci.yaml \
.github/workflows/validate-main.yaml \
scripts/summarize-sarif-findings.sh \
scripts/tests/test-summarize-sarif-findings.sh \
scripts/normalize-sarif-paths.sh
echo '=== validate-main workflow context ==='
sed -n '1,235p' .github/workflows/validate-main.yaml
echo '=== ci validate-job context ==='
sed -n '300,515p' .github/workflows/ci.yaml
echo '=== SARIF producer/consumer inventory ==='
rg -n -C 3 \
'ksail workload scan|kubescape\.sarif|kubescape\.raw\.sarif|upload-sarif|normalize-sarif-paths|summarize-sarif-findings' \
.github scripts
echo '=== Shell static diagnostics, if available ==='
if command -v shellcheck >/dev/null 2>&1; then
shellcheck scripts/summarize-sarif-findings.sh scripts/normalize-sarif-paths.sh scripts/tests/test-summarize-sarif-findings.sh
else
echo 'shellcheck unavailable'
fi
echo '=== YAML syntax validation using Ruby Psych, if available ==='
if command -v ruby >/dev/null 2>&1; then
ruby -e 'require "yaml"; ARGV.each { |p| YAML.load_file(p); puts "valid: #{p}" }' \
.github/workflows/ci.yaml .github/workflows/validate-main.yaml
else
echo 'ruby unavailable'
fiLength of output: 74350
Verified that both workflows preserve the pre-normalization SARIF for informational reporting while continuing to upload the normalized SARIF. The summary remains non-blocking, and the new regression test exercises the locationless-finding differential plus both workflow paths. Static checks also passed: |
CodeRabbit lane: That verdict arrived on the chat surface ( Advancing to Codex. Not counting this as a failure of the change — only as a lane that did not deliver a gate-satisfying artifact. |
@codex review |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: ffcf65317f
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
…lizer abort
Three ways the summary could still under-report or read falsely clean:
A file that is valid JSON but not a SARIF document ({}, {"runs":null}) flowed
through the optional iteration and reported 'Kubescape: 0 findings' — a broken
scan indistinguishable from a clean one, which is the ambiguity this reporting
exists to remove. Require .runs to be an array; an empty array stays a
legitimate zero.
SARIF lets a result name its rule by ruleIndex instead of ruleId. Grouping on
ruleId alone collapsed every such result into one '<no rule id>' bucket and
discarded the metadata. Resolve per run, since the index is run-scoped, and
bounds-check it so an out-of-range index is a miss rather than an attribution to
the last rule in the table.
The summary was gated on the normalizer succeeding, so an unresolvable path
skipped it exactly when the findings mattered most. Gate it on the raw copy
instead, emitted before the normalizer runs.
All three P2s reproduced first, then fixed in 1. Structurally invalid SARIF read as a clean scan. This was the worst of the three, because it is the failure this PR exists to remove wearing the PR's own output. This one also corrected my own test, which had asserted 2. 3. Summary skipped when normalization aborts. Correct, and it was a consequence of my own previous commit: I changed the summary's input to the raw copy but left its gate on 5 new ablations, each RED on its own assertion:
|
@coderabbitai review Head |
User evaluation re-run at
The second row is the one worth reading: identical to the output at Full check state at this head: 19 pass, 11 skipped, 0 failing, 0 unresolved threads, |
🧩 Analysis chain🏁 Script executed: #!/bin/bash
set -euo pipefail
base="$(git merge-base HEAD origin/main 2>/dev/null || git merge-base HEAD main)"
printf '=== Revision ===\nbase=%s\nhead=%s\n' "$base" "$(git rev-parse HEAD)"
printf '\n=== Changed files ===\n'
git diff --name-status "$base"..HEAD
git diff --check "$base"..HEAD
printf '\n=== Relevant outlines ===\n'
ast-grep outline scripts/summarize-sarif-findings.sh --items all || true
ast-grep outline scripts/tests/test-summarize-sarif-findings.sh --items all || true
printf '\n=== Complete PR patch: relevant files ===\n'
git diff --find-renames "$base"..HEAD -- \
.github/workflows/ci.yaml \
.github/workflows/validate-main.yaml \
scripts/summarize-sarif-findings.sh \
scripts/tests/test-summarize-sarif-findings.sh \
scripts/normalize-sarif-paths.sh
printf '\n=== Summary script ===\n'
cat -n scripts/summarize-sarif-findings.sh
printf '\n=== Summary regression tests ===\n'
cat -n scripts/tests/test-summarize-sarif-findings.sh
printf '\n=== CI Kubescape contexts ===\n'
rg -n -C 18 \
'kubescape\.(raw\.)?sarif|normalize-sarif|summarize-sarif|raw=ready|sarif=ready|Summarize Kubescape|Validate Kubescape' \
.github/workflows/ci.yaml .github/workflows/validate-main.yaml
printf '\n=== Normalizer filtering context ===\n'
rg -n -C 12 'results|artifactLocation|physicalLocation|locations' scripts/normalize-sarif-paths.sh
printf '\n=== Static diagnostics (when installed) ===\n'
if command -v shellcheck >/dev/null 2>&1; then
shellcheck scripts/summarize-sarif-findings.sh scripts/tests/test-summarize-sarif-findings.sh scripts/normalize-sarif-paths.sh
else
echo 'shellcheck unavailable'
fi
if command -v actionlint >/dev/null 2>&1; then
actionlint .github/workflows/ci.yaml .github/workflows/validate-main.yaml
else
echo 'actionlint unavailable'
fiLength of output: 98287
P2 — Keep rule metadata scoped to each SARIF run. Carry the resolved rule metadata alongside each result before grouping (or key the lookup by run plus rule ID), rather than looking it up from a global ID-only table. A regression fixture with two runs sharing a rule ID but differing metadata would pin this behavior. ✅ Action performedReview finished.
|
Two runs may define the same rule id with different metadata. The rule table was built by flattening every run and calling from_entries, which keeps the LAST definition of a duplicate key — so findings from the first run were reported with the second run's level and description. A confident wrong attribution, of the same kind the ruleIndex bounds check prevents. Each result now resolves against its own run and carries the answer forward, and grouping keys on the resolved triple: runs that agree still merge into one control, runs that disagree are reported separately instead of one silently overwriting the other.
CodeRabbit lane at But the content was right, and it is fixed in
The other half matters as much: runs that agree must still merge, or every ordinary multi-run report fragments. Pinned both directions —
2 ablations, each RED on its own assertion: restoring the flattened table fails the conflicting-runs case, and grouping on Advancing to Codex for the gate. |
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@scripts/tests/test-summarize-sarif-findings.sh`:
- Around line 57-62: Update run_summary to unset or clear GITHUB_STEP_SUMMARY
only for fixture executions, preventing non-summary tests from modifying the
ambient GitHub Actions step summary. Preserve the explicit fixture’s ability to
set GITHUB_STEP_SUMMARY when it intentionally tests summary output.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Repository YAML (base), Organization UI (inherited)
Review profile: ASSERTIVE
Plan: Pro Plus
Run ID: a453657d-4a58-4cd9-b681-02cb22b92308
📒 Files selected for processing (4)
.github/workflows/ci.yaml.github/workflows/validate-main.yamlscripts/summarize-sarif-findings.shscripts/tests/test-summarize-sarif-findings.sh
📜 Review details
🧰 Additional context used
📓 Path-based instructions (2)
.github/workflows/*.yaml
📄 CodeRabbit inference engine (AGENTS.md)
CI for Kubernetes-related changes must remain static: run validation and Kubescape scanning without starting a cluster.
Files:
.github/workflows/validate-main.yaml.github/workflows/ci.yaml
**/*
📄 CodeRabbit inference engine (AGENTS.md)
**/*: Use Conventional Commit messages and titles, keep changes small and focused, and create pull requests as drafts.
Land new behavior disabled, validate it, then enable it using the appropriate layer: OpenFeature for per-request flags, Flagger for traffic shifting, Helm values/Kustomize for component toggles, and Kubernetes feature gates for platform behavior.
Files:
scripts/summarize-sarif-findings.shscripts/tests/test-summarize-sarif-findings.sh
🧠 Learnings (1)
📚 Learning: 2026-07-28T00:33:10.828Z
Learnt from: devantler
Repo: devantler-tech/platform PR: 2825
File: .github/workflows/dr-rebuild.yaml:70-73
Timestamp: 2026-07-28T00:33:10.828Z
Learning: For GitHub Actions concurrency configuration in this repo, don’t use `queue: single` on a shared production concurrency group (e.g., `prod-deploy`) that is used for normal production deployments—`queue: single` will cancel/replace any pending run in that group. Normal production deployments must use a uniform `queue: max` for that shared group to ensure serialization without coalescing. If you need DR rebuild coalescing (replacement of pending DR rebuild runs), it must use a separate DR-only concurrency scope/group (not the shared production `prod-deploy` group).
Applied to files:
.github/workflows/validate-main.yaml.github/workflows/ci.yaml
🔇 Additional comments (5)
.github/workflows/ci.yaml (1)
64-64: LGTM!Also applies to: 252-260, 411-426, 483-518
.github/workflows/validate-main.yaml (1)
164-171: LGTM!Also applies to: 192-211
scripts/summarize-sarif-findings.sh (2)
58-59: Rule metadata still flattened into a global ID-keyed table.
$rulesis built once from every.runs[].tool.driver.rulesflattened together and keyed only by.id(line 59);from_entrieskeeps the last entry on a key collision. When two runs carry the same rule id with different metadata,$rules[$id](line 88) reports whichever run's metadata happened to load last, mis-attributing level/description for the other run's findings. This is the exact scenario the PR's own review discussion flagged as a remaining P2.A fix needs to resolve rule metadata per-run at the same point
$runRulesis already in scope (lines 66-75), carrying{id, level, desc}per result instead of just the id string, so grouping never needs to fall back to a merged global table.Also applies to: 87-88
1-57: LGTM!Also applies to: 60-108, 110-122
scripts/tests/test-summarize-sarif-findings.sh (1)
1-56: LGTM!Also applies to: 64-231, 239-415
GITHUB_STEP_SUMMARY is set for every Actions step and inherited by child processes, so each fixture run appended its output to the real job summary — 2772 bytes across 12 blocks, including 'Kubescape: 4 finding(s) across 2 control(s)', which is indistinguishable from the genuine scan report on the same run page. A test for a findings reporter must not publish fabricated findings. Clear the variable for every fixture run; the one case that exercises the step-summary output sets its own path. An ambient sentinel now stands in for the job summary and is asserted empty at the end, so the isolation is proven rather than assumed.
Valid, and 🟠 Major is the right severity — fixed in
Fixed by clearing the variable for every fixture run ( Guarded rather than just fixed. The suite now points the ambient variable at a sentinel that stands in for the job summary and asserts it is empty at the end, so the isolation is proven every run instead of assumed. Ablation — restoring the inherited environment — fails with After: 0 bytes. One process note, since it affected how I scored your lane: this review object landed at 21:33:17Z for |
@coderabbitai full review Using |
|
✅ Action performedFull review finished. Your included review limit is currently reached under our Fair Usage Limits Policy. This review may still proceed through usage-based billing if eligible. Your next included review will be available in 42 minutes. |
CodeRabbit lane: Worth stating the distinction, because I got it wrong once on this PR: at 42 minutes exceeds what this run can hold, so advancing to Codex rather than waiting it out. |
@codex review |
|
Codex Review: Didn't find any major issues. 👍 Reviewed commit: ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
If Codex has suggestions, it will comment; otherwise it will react with 👍. Codex can also answer questions or update the PR. Try commenting "@codex address that feedback". |
Readiness at 1. Programmatically tested. 2. Reviewed. Both inline comments still anchored to this head are already-resolved CodeRabbit findings that GitHub re-anchored when the head moved (created 20:29 and 21:33, before this commit existed). All 7 review threads are resolved; nothing is outstanding. 3. Tried and evaluated as a user — run 30401273612:
The middle row is unchanged from Nine findings were raised across five rounds and every one was real — three of them defects in this PR's own premise (a broken scan reported as clean, locationless findings dropped, the summary skipped exactly when normalization failed), two mis-attribution bugs where a confident wrong answer beats an absent one, and the 🟠 Major where the test itself was publishing fabricated findings onto the run page. Promoting and merging. |

Why
Kubescape's console table shows only part of what it finds. Measured on this repository: a scan whose SARIF carried 4 findings printed
All controls passed. No issues found, and one carrying 70 findings listed a single control. Since those findings are now uploaded as Code Scanning alerts, the CI log and the Security tab actively contradict each other — and the log is what a developer reads on a PR.What
Reports the findings from the SARIF the scan already produces, so the log and the alerts cannot disagree. Covers pull requests and pushes to
main(the latter is the only default-branch scan). Purely informational — the compliance threshold stays the sole merge gate, and a summary that cannot be built annotates rather than blocking a merge.Fixes #2846
Part of #2451