Skip to content

fix(ci): decide E2E test coverage per changed path - #3441

Merged
Aleksei Sviridkin (lexfrei) merged 2 commits into
mainfrom
fix/select-e2e-per-path-coverage
Aug 10, 2026
Merged

fix(ci): decide E2E test coverage per changed path#3441
Aleksei Sviridkin (lexfrei) merged 2 commits into
mainfrom
fix/select-e2e-per-path-coverage

Conversation

@lexfrei

@lexfrei Aleksei Sviridkin (lexfrei) commented Jul 23, 2026

Copy link
Copy Markdown
Contributor

What this PR does

hack/select-e2e.sh escalates to the full Chainsaw suite when a changed package reaches no suite through the dependency graph, because otherwise that change gets no E2E coverage at all. The escalation was read off the merged selection, so it depended on the whole diff rather than on the path that needed it. Any other changed path contributing a suite name made the selection non-empty and the escalation vanished with nothing in the output to say it had. Editing a platform component together with one unrelated suite's Chainsaw test ran that single suite instead of everything, which is an ordinary PR shape: change a component, adjust the e2e near it.

Coverage is now decided per changed path. Each path resolves through its owning sources to the suites that exist, and a path reaching none of them forces the full run whatever the rest of the diff selected. The unit is the path rather than the source, because system/postgres-operator belongs to two PackageSources where one reaches no suite, and deciding per source would run everything on every change to it.

The other half is the mapping that left packages unreachable from their own suites. src_to_suites() covered *-application names plus external-dns, and the final filter dropped the rest, so cozystack.kuberture and cozystack.securitygroup-controller were walked to and then discarded. A CNI change ran neither of the two controller suites most likely to regress from it, and a change to either package escalated to the full run rather than selecting the suite that covers it. Every reached source now maps through the table, which is the inverse of select-install.sh's suite_to_source(); a test walks every suite through both, so the next source name fitting neither convention fails there instead of quietly escalating forever.

What was measured

Behaviour was diffed against the current selector over one values.yaml per package under packages/{apps,system,extra}: 159 packages, 8 selections change, all of them the mapping repair. Five networking packages and prometheus-operator-crds widen — 19 to 21 suites and 10 to 11, both gaining kuberture and securitygroup. kuberture and securitygroup-controller narrow from the full suite to their own single suite. Nothing narrows that should not.

That sweep is one path per diff, so it says nothing about the per-path escalation, which only shows on a diff with more than one path. Pairing each package with one unrelated Chainsaw edit gives that number. 82 of the 159 packages escalate on their own and stop escalating once the pair is added, which is the size of the hole: for those, adding an unrelated suite edit to the same pull request replaced the full run with one suite. 88 packages select something different after this change, the extra six being the mapping repair widening a selection rather than restoring an escalation.

Worth stating alongside it, because it decides whether the branch is a corner or the common case: 83 of the 159 escalate on a single-path diff today, and 81 do after this change — the two that leave are kuberture and securitygroup-controller, which now select the suite that covers them. So the escalation is the branch most packages take, and the swallowed one was the branch most packages took. Counting that needs an instrument that separates a graph walk which reached everything from an escalation which printed everything, since the two agree once the mapping is fixed: the count above comes from a scratch suite tree carrying one extra suite that no source names, which only an escalation can emit.

The suite selection is what decides which tests run, so a rule that stops firing does not go red, it disappears. The tests pin the property rather than a worked example: that a path reaching no suite escalates whatever else the diff holds, that a covered path never drags the full suite in with it, and that every suite round-trips between the two mapping tables. Each was checked by mutation — removing the rule it guards turns it red.

Left for a follow-up

resolve_suites writes into the caller's scope because POSIX sh has no local, and what keeps that safe is the single call site sitting inside $( ), pinned by a test that counts occurrences of the name. Declaring the function with ( … ) instead of { … } would contain those variables by construction and let both the test and the two paragraphs explaining the constraint go away. A count-the-name grep is the weakest kind of pin and it exists only because the function leaks.

The output contract at the top of the script lists the causes of a full-suite selection but not the backstop, which after this change is a mechanism of its own: an edit under hack/e2e-chainsaw/<dir>/ where that directory holds no chainsaw-test.yaml. The behaviour is pinned by a test and explained where it lives; only the header block is short. In the same area, the escalation line names the owning sources rather than the changed path, and reports the first uncovered group rather than all of them.

Overlap

#3434 fixes the same escalation from a different angle: it decides the escalation from the whole source-derived selection rather than per path. That closes the "package with no coverage plus an unrelated suite edit" shape, and leaves "package with no coverage plus another package that does select suites", where the source-derived selection is non-empty and the escalation is swallowed again. There is a test for that second shape here.

Screenshots

Not applicable, no UI changes.

Downstream repositories

Walked the trigger map in docs/agents/contributing.md against the diff. The changed files are hack/select-e2e.sh, hack/select-e2e_test.bats and docs/agents/e2e-testing.md. The only hack/-related trigger is cozystack/ccp, which gates on hack/package.mk, hack/common-envs.mk and hack/update-crd.sh and on what a make target does; nothing here is moved, renamed, or changed in make-target behaviour. The ansible-cozystack and talm triggers are about hack/e2e-prepare-cluster.bats, which is untouched.

Release note

fix(ci): a changed package that no E2E suite covers now runs the full suite even when the same pull request also edits an unrelated suite, and changes to the kuberture and securitygroup packages select their own suite instead of escalating

Closes #3330

Closes #3665

@gemini-code-assist

Copy link
Copy Markdown
Contributor

Caution

The consumer version of Gemini Code Assist on GitHub has been sunset. All code review activity has officially ceased.

@coderabbitai

coderabbitai Bot commented Jul 23, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

The E2E selector now validates suite and source discovery, resolves PackageSource dependency groups independently, escalates uncovered changes to the full suite, ignores disabled suites, and adds exact-selection tests and updated documentation.

Changes

E2E suite selection

Layer / File(s) Summary
Suite discovery and dependency graph
hack/select-e2e.sh
Suite and owner discovery now fail explicitly when empty. Reverse dependencies and grouped source mappings support suite resolution.
Grouped suite resolution and escalation
hack/select-e2e.sh
Per-suite edits, disabled suites, dependency groups, runnable-suite filtering, and per-group full-suite escalation are resolved without masking uncovered changes.
Selection contract and behavioral coverage
docs/agents/e2e-testing.md, hack/select-e2e_test.bats, hack/cozyreport.bats
Documentation and Bats coverage verify exact selection, narrowing, disabled suites, missing coverage, mapping consistency, discovery failures, cleanup, and trap inventory updates.

Estimated code review effort: 4 (Complex) | ~45 minutes

Possibly related issues

Possibly related PRs

Suggested reviewers: ivanhunters

Sequence Diagram(s)

sequenceDiagram
  participant ChangedPaths
  participant SelectE2E as select-e2e.sh
  participant SuiteDiscovery
  participant SourceGraph
  participant Resolver
  ChangedPaths->>SelectE2E: Changed paths
  SelectE2E->>SuiteDiscovery: Discover runnable suites
  SelectE2E->>SourceGraph: Build grouped dependency data
  SourceGraph-->>Resolver: PackageSource groups
  Resolver-->>SelectE2E: Resolve suite candidates or escalation
  SelectE2E-->>ChangedPaths: Selected suites or full suite
Loading
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Linked Issues check ✅ Passed The changes satisfy issue #3330 by preserving full-suite escalation when any changed path lacks suite coverage.
Out of Scope Changes check ✅ Passed The script, tests, documentation, and trap inventory updates directly support the stated E2E coverage objectives.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main change: selecting E2E coverage per changed path.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/select-e2e-per-path-coverage

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@github-actions github-actions Bot added area/ci Issues or PRs related to CI workflows, GitHub Actions, automation kind/bug Categorizes issue or PR as related to a bug size/L This PR changes 100-499 lines, ignoring generated files labels Jul 23, 2026
@lexfrei
Aleksei Sviridkin (lexfrei) force-pushed the fix/select-e2e-per-path-coverage branch from a701d72 to 365195d Compare July 23, 2026 11:40
@lexfrei
Aleksei Sviridkin (lexfrei) marked this pull request as ready for review July 23, 2026 11:40
@gemini-code-assist

Copy link
Copy Markdown
Contributor

Caution

The consumer version of Gemini Code Assist on GitHub has been sunset. All code review activity has officially ceased.

@dosubot dosubot Bot added the area/testing Issues or PRs related to testing (e2e, bats, unit tests) label Jul 23, 2026

@IvanHunters IvanHunters left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Verdict

NOT LGTM

The selection logic is correct and the headline fix is protected by a non-vacuous regression test, but one new test's premise-guard pipes yq straight into awk -F'\t', diverging from the repo's established echo "$VAR" | awk pattern, so it computes owners=0 and fails on mikefarah yq — turning the PR's own test suite red.

Findings

[MAJOR] hack/select-e2e_test.bats:229-230 — test "a path owned by several sources counts as covered if any one is" fails: its premise-guard splits yq output with awk -F'\t' but never passes it through echo first.

mikefarah yq emits "\t" in string concatenation as a literal two-byte backslash-t, not a tab. Verified: yq -n '"a" + "\t" + "b"' | od -An -tx161 5c 74 62 (\=5c, t=74), on mikefarah v4.46.1. Every other awk -F'\t' in both scripts routes through echo "$VAR" | awk (hack/select-e2e.sh:151, :196, and all of select-install.sh), and /bin/sh's echo converts the literal \t into a real tab (echo of a\tb61 09 62), so those splits work. The guard at line 230 is the sole place that feeds yq directly into awk -F'\t', so the split never happens: $1 is the whole line, no row matches system/postgres-operator, owners computes 0, and [ "$owners" -ge 2 ] fails under set -e.

Reproduction — the test body run verbatim under sh -eu:

GUARD owners=[       0]
FINAL rc=1

This is a self-inflicted red in the Unit & controller tests job (the same class as a bashism-under-#!/bin/sh whose bats test runs it under the wrong shell: production works, the shipped test does not). The script's own output for this input is correct — hack/select-e2e.sh <diff> <sources> for packages/system/postgres-operator/values.yaml returns harbor postgres — so only the guard is broken, not the behaviour under test. It failed loudly on my run of the documented runner hack/cozytest.sh, which then invoked its on-failure kubectl/crust-gather capture. Fix: match the codebase pattern, e.g. owners=$(echo "$(yq -rN '…' "$tmp/sources"/*.yaml)" | awk -F'\t' '$1=="system/postgres-operator"{print $2}' | sort -u | wc -l), or derive it the way the script derives OWNERS.

Caveats

  • Phase 5b classification: this PR touches only hack/select-e2e.sh, hack/select-e2e_test.bats, and docs/agents/e2e-testing.md. No packages/, no chart/values/CRD/RBAC/migration/image. There is no existing-customer-upgrade or fresh-install surface — the only operational surface is which Chainsaw suites CI selects.
  • Regression gate (executed): behaviour diffed old-vs-new (git show <base>:hack/select-e2e.sh) over every package under packages/{apps,system,extra}. Exactly 8 selections change: 5 networking packages (kilo, kubeovn, cilium, cilium-networkpolicy, gateway-api-crds) 19→21 suites and prometheus-operator-crds 10→11 (both add kuberture+securitygroup, widening); kuberture full→[kuberture] and securitygroup-controller full→[securitygroup] (narrowing). Both narrowings land on the package's own covering suite — no coverage-losing narrowing. The PR-body claims all check out.
  • Non-vacuity (executed): test "escalates despite other selections" is a real regression guard — mutating the script back to pre-fix behaviour makes it go RED. The two new fail-loud exits are confirmed: empty suite tree → rc=1, empty owners index → rc=1.
  • Could not run the documented runner hack/cozytest.sh cleanly: on any test failure it invokes kubectl/crust-gather against the ambient kube-context. Ran the failing/relevant test bodies in isolation under sh -eu instead.
  • Verified the guard failure on mikefarah yq v4.46.1. Could not execute CI's exact yq build; however the codebase's echo-through-awk pattern only exists because mikefarah yq emits a literal \t.

Recommended follow-ups

  • Pre-existing #2983-class scoping gap (not introduced by this PR — full_suite_pattern is unchanged): root pkg/, go.mod, go.sum, test/, tools/ are neither in full_suite_pattern (hack/select-e2e.sh:23) nor under packages/, so a diff confined to them falls through to "silently ignored" → CI skips E2E. Verified: a pkg/apiserver/foo.go change and a go.mod change both produce empty output. pkg/ is compiled into the cozystack-api/cozystack-operator images, so a pkg-only change ships with no E2E. Consider adding pkg/, go.mod, go.sum to full_suite_pattern.

@lexfrei
Aleksei Sviridkin (lexfrei) force-pushed the fix/select-e2e-per-path-coverage branch from 365195d to caddc93 Compare July 23, 2026 14:45
@lexfrei

Copy link
Copy Markdown
Contributor Author

Fixed the guard. It now routes the yq output through echo "$VAR" | awk, matching the owners split in select-e2e.sh (lines 151, 196) and every awk -F'\t' in select-install.sh. That is the portable form: on older mikefarah yq the literal \t is converted to a real tab by echo, so the split works regardless of yq version.

For the record, the test was green on CI (the Unit & controller tests job passed on both attempts of the run) and on yq 4.53.x, since that version emits "\t" as a real tab. The portability point still holds for older yq, and matching the codebase pattern removes the version dependency, so I applied it.

On the pkg/, go.mod, go.sum, test/, tools/ scoping gap: it is real and pre-existing (full_suite_pattern is unchanged here). It is out of scope for this PR, which is narrowly the swallowed-escalation fix. I can open a separate follow-up issue for it rather than widen full_suite_pattern in the same change.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 `@hack/select-e2e.sh`:
- Around line 153-155: Update the selected_groups construction near the
selected_groups assignment to convert the space-separated owning sources in src
into a comma-joined group, rather than relying on paste -sd , - which only
processes newline-separated input. Preserve the grouping semantics so later
space-split iteration evaluates all owning sources for each path together.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 5ca9a3bf-1383-482f-8517-846089c5bd7d

📥 Commits

Reviewing files that changed from the base of the PR and between 365195d and caddc93.

📒 Files selected for processing (3)
  • docs/agents/e2e-testing.md
  • hack/select-e2e.sh
  • hack/select-e2e_test.bats
🚧 Files skipped from review as they are similar to previous changes (2)
  • docs/agents/e2e-testing.md
  • hack/select-e2e_test.bats

Comment thread hack/select-e2e.sh Outdated

@myasnikovdaniil myasnikovdaniil left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM — coverage is now decided per changed path, the selection logic is correct, and it is protected by non-vacuous tests that pass on mikefarah yq.

Business context: select-e2e.sh read the full-suite escalation off the merged selection, so a diff that changed an uncovered package together with any unrelated suite ran just that one suite instead of escalating; this decides coverage per changed path and closes two fail-open E2E-skip paths.

The earlier change-request about the premise guard piping yq straight into awk -F'\t' looks addressed: the guard now routes through echo "$owners_tsv" | awk -F'\t', and hack/cozytest.sh hack/select-e2e_test.bats is green on mikefarah yq v4.52.4 (24/24, including the multi-source coverage test).

Non-blocking follow-ups

  1. Nested-suite escalation is effectively a no-op (hack/select-e2e.sh:122-126). The new branch escalates to the full suite for a suite nested deeper than the depth-2 discovery, but the escalation emits $all_apps — itself built with -maxdepth 2 — and the workflow then runs chainsaw test <those explicit names>, so a nested suite still would not run. The workflow reads empty output as skip (pull-requests.yaml:649), so the script cannot signal "full" by emitting empty and instead emits the explicit depth-2 list. This is latent only: every suite lives at depth-2 today and the pre-PR safety net behaved identically, so nothing regresses. The docs line "escalates to the full suite ... because Chainsaw discovers suites recursively" slightly overstates it — if nested suites are ever introduced, all_apps discovery would need to become recursive.

  2. Seconding the pre-existing full_suite_pattern fall-through: a diff confined to pkg/, go.mod, go.sum, test/, or tools/ produces empty output and CI skips E2E, yet pkg/ compiles into the cozystack-api/operator images (verified each of those paths returns empty output). Since this PR is specifically about closing fail-open E2E-skip paths, folding pkg/|go.mod|go.sum into the regex fits the theme.

For the record, I could not reproduce the CodeRabbit note on select-e2e.sh:153-155: $src is awk '{print $2}' output and is therefore newline-separated, so echo "$src" | paste -sd , - joins it correctly (cozystack.monitoring,cozystack.postgres-operator), and a change to system/postgres-operator selects harbor postgres rather than escalating — so the grouping works as intended.

@lexfrei
Aleksei Sviridkin (lexfrei) force-pushed the fix/select-e2e-per-path-coverage branch from caddc93 to 71208d2 Compare July 27, 2026 19:49

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 `@hack/select-e2e.sh`:
- Around line 79-80: Update the OWNERS and REVERSE assignments to capture
build_owners_index and build_reverse_deps output before sorting, preserving and
checking each command’s failure status so a failed yq aborts without retaining
partial output. Apply sort -u only after successful output generation.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 96a9bddd-1040-483d-8097-a0284aff48ae

📥 Commits

Reviewing files that changed from the base of the PR and between caddc93 and 71208d2.

📒 Files selected for processing (3)
  • docs/agents/e2e-testing.md
  • hack/select-e2e.sh
  • hack/select-e2e_test.bats
🚧 Files skipped from review as they are similar to previous changes (2)
  • hack/select-e2e_test.bats
  • docs/agents/e2e-testing.md

Comment thread hack/select-e2e.sh Outdated
@lexfrei

Copy link
Copy Markdown
Contributor Author

IvanHunters Fixed at hack/select-e2e_test.bats:232-233. The guard splits in two steps now, owners_tsv=$(yq -rN ...) then echo "$owners_tsv" | awk -F'\t', same as select-e2e.sh:151 and :196.

Verified by breaking things:

literal \t TSV piped into awk -F'\t'   -> owners=0   (old form, your repro)
echo "$owners_tsv" | awk -F'\t'        -> owners=2   (current form)
guard aimed at apps/kafka              -> owners=1, rc=1, body never reached
select-e2e.sh:155 reverted to $src     -> guard green, assertion RED

The last line is the tautology check. With per-source grouping instead of per-path the guard still passes, but the output escalates to all 21 suites instead of harbor postgres, so the test does catch the regression it claims to. 24/24 on yq v4.53.3.

The pkg//go.mod/go.sum gap in full_suite_pattern is real but predates this change. I would rather do it in a separate PR.

@lexfrei
Aleksei Sviridkin (lexfrei) force-pushed the fix/select-e2e-per-path-coverage branch from 71208d2 to 58f3492 Compare August 6, 2026 20:56
@coderabbitai

coderabbitai Bot commented Aug 6, 2026

Copy link
Copy Markdown
Contributor

Note

GitHub couldn't provide a complete incremental comparison for this pull request, so CodeRabbit is performing a full review instead. This review may take a little longer.

@coderabbitai

coderabbitai Bot commented Aug 6, 2026

Copy link
Copy Markdown
Contributor

Note

GitHub couldn't provide a complete incremental comparison for this pull request, so CodeRabbit is performing a full review instead. This review may take a little longer.

@lexfrei
Aleksei Sviridkin (lexfrei) force-pushed the fix/select-e2e-per-path-coverage branch from 66ee550 to aa8211c Compare August 7, 2026 08:04
@lexfrei

Copy link
Copy Markdown
Contributor Author

IvanHunters the guard you flagged was fixed in the 27 July update: owners_tsv=$(yq ...) then echo "$owners_tsv" | awk -F'\t', same shape as select-e2e.sh:151. Two more things landed since, so this needs a fresh look rather than just clearing the old verdict.

OWNERS and REVERSE were built through yq | sort -u, which returns sort's status. yq streams its rows and only then fails on a file it cannot parse, so a broken sources dir left a truncated graph and the script kept going, and a short reverse-dep graph narrows the run instead of escalating. Both are captured before sorting now, with a test that was red before the fix.

The tests also cleaned up through trap ... EXIT, which docs/agents/e2e-testing.md bans for hack/*.bats: under the real bats binary a failing test then prints no TAP line at all. Cleanup moved to the end of each body, and the file moved onto the converted list in hack/cozyreport.bats. 25/25 under both runners.

The docs line claiming a nested-suite edit escalates "because Chainsaw discovers suites recursively" was overstating it, since the escalation names the suites the depth-2 scan found and a nested one still would not run. Reworded. The full_suite_pattern fall-through for pkg/, go.mod and go.sum is real but predates this change, and #3392 already tracks that class, so I would rather do it there.

One thing about the checks: the unit job is red on main because hack/multus-install-cni-plugins.bats arrived without its entry in the frozen trap counts. #3584 fixes that. This branch stays red until it lands.

@lexfrei
Aleksei Sviridkin (lexfrei) force-pushed the fix/select-e2e-per-path-coverage branch from aa8211c to 96e2625 Compare August 7, 2026 13:20
Aleksei Sviridkin (lexfrei) added a commit that referenced this pull request Aug 8, 2026
…gregated-API auth (#3444)

## What this PR does

Two unrelated defects made the sandbox install job's failure handling
unreliable. This fixes both.

The first hit every red run. When a test fails, the diagnostic snapshot
runs `crust-gather collect`, and its host-log leg spawns a privileged
debug pod per node. The sandbox runs baseline PodSecurity, which rejects
each of those pods with a 403, so crust-gather keeps retrying until it
burns its whole collection budget and exits 1. The snapshot verdict then
printed `FAILED` on every failed run even though the namespaced object
state was already captured, so the line that says whether the uploaded
snapshot is trustworthy was always wrong. The host-log leg collects
nothing usable here anyway, so this passes `--disable-additional-logs`
at all three collect sites: the host snapshot from the BATS runner, the
host snapshot from the Chainsaw error handler, and the nested tenant
snapshot. Object collection now finishes and the verdict again separates
a real collection failure from a complete snapshot.

The second is a race in the install test. The "Configure Tenant" step
issues its first `kubectl patch` of the root tenant as soon as an
earlier test sees the aggregated APIService go `Available`. That
condition only tracks endpoint availability, not the serving pod's auth
state. A freshly rolled cozystack-api pod can start answering before it
has loaded the requestheader client-CA, and until it does it drops the
front-proxy identity and answers as `system:anonymous`, so the patch is
rejected with a 403 on the test's very first command. This adds a short
authenticated GET against the same resource before the patch, so the
step proceeds only once the aggregated API both serves and authenticates
the request. If the aggregation layer is actually wedged past two
minutes the gate fails loud instead of hiding a real regression.

Both were traced from a recent sandbox failure (run 30004030331, during
PR #3441's CI).

Out of scope: the broader convergence flakiness of the install step is
not touched here. That covers the long reconcile windows for the storage
and platform releases and the app-level races in the Chainsaw suite. The
durable product-side fix for the second defect, where cozystack-api
should not report ready before the requestheader client-CA is loaded, is
a separate follow-up.

This partially addresses #3368, its crust-gather diagnostics item, and
does not close it.

### Screenshots

<!-- REQUIRED for UI changes: attach screenshots or screen recordings
demonstrating
the visual impact of your changes. PRs with UI changes without
screenshots will not be merged. -->

No UI changes.

### Downstream repositories

<!--
Cozystack is upstream for repositories that are NOT kept in sync with it
automatically, and nothing in CI compares the two sides, so a change
here can break them silently.

Which change forces what, and which file to touch, is the trigger map in
docs/agents/contributing.md under "Downstream Repositories". Walk it
before you tick anything.

IF YOU ARE AN AI AGENT OR AN LLM, THIS IS PART OF THE TASK, NOT A FORM
TO FILL IN:

1. Walk the trigger map against the actual diff of this PR, file by
file. Do not decide from the PR title or from your memory of what you
changed.
2. For every repository you tick, open the follow-up PR there and link
it on the same line. A ticked box with no link claims work that does not
exist, which is worse than an empty box. Search that repository for an
open PR or issue covering it first, and link that instead of filing a
duplicate.
3. When the follow-up is out of scope, or needs a decision that is not
yours to make, open an issue in that repository instead, link that, and
say so in the PR body. Do not push a speculative PR into another
repository just to fill a line here.
4. Do not tick "No downstream repository is affected" to make the
checklist go away. If you are unsure, leave every box empty and say so
in the PR body, so a human decides.
-->

The diff touches only the e2e test harness (`hack/cozytest.sh`,
`hack/e2e-chainsaw/.chainsaw.yaml`,
`hack/e2e-chainsaw/_lib/run-kubernetes.sh`,
`hack/e2e-install-cozystack.bats`). The trigger map's `hack/` couplings
are `hack/e2e-prepare-cluster.bats` (node prerequisites for
ansible-cozystack and talm) and `hack/package.mk`,
`hack/common-envs.mk`, `hack/update-crd.sh` plus the package layout
(ccp, external-apps-example, cozyhr). None of those are moved, renamed,
or edited here, and nothing else in the map matches.

- [x] No downstream repository is affected by this change
- [ ] [cozystack/website](https://github.com/cozystack/website) -
follow-up:
- [ ]
[cozystack/terraform-provider-cozystack](https://github.com/cozystack/terraform-provider-cozystack)
- follow-up:
- [ ]
[cozystack/ansible-cozystack](https://github.com/cozystack/ansible-cozystack)
- follow-up:
- [ ] [cozystack/ccp](https://github.com/cozystack/ccp) - follow-up:
- [ ] [cozystack/talm](https://github.com/cozystack/talm) - follow-up:
- [ ] [cozystack/cozyhr](https://github.com/cozystack/cozyhr) -
follow-up:
- [ ] [cozystack/cozy-proxy](https://github.com/cozystack/cozy-proxy) -
follow-up:
- [ ]
[cozystack/cozystack-telemetry-server](https://github.com/cozystack/cozystack-telemetry-server)
- follow-up:
- [ ]
[cozystack/external-apps-example](https://github.com/cozystack/external-apps-example)
- follow-up:
- [ ] [cozystack/examples](https://github.com/cozystack/examples) -
follow-up:

### Release note

<!--  Write a release note:
- Explain what has changed internally and for users.
- Start with the same `type(scope):` prefix as in the PR title
- Follow the guidelines at
https://github.com/kubernetes/community/blob/master/contributors/guide/release-notes.md.
-->

```release-note
fix(e2e): keep failure snapshots complete when PodSecurity blocks crust-gather's host-log leg, and wait for the aggregated API to authenticate before patching the root tenant so the install step no longer flakes on a system:anonymous 403
```


<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

- **Bug Fixes**
- Improved failure diagnostics during automated Kubernetes and tenant
tests by avoiding unsupported privileged log collection steps.
- Ensured failure snapshots complete more reliably instead of timing out
or returning incomplete results.
- Added a readiness check before configuring the root tenant, preventing
intermittent authorization failures during installation tests.

- **Tests**
- Increased reliability of installation and end-to-end test workflows
under baseline security policies.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
Aleksei Sviridkin (lexfrei) added a commit that referenced this pull request Aug 8, 2026
…#3622)

<!-- Thank you for making a contribution! Here are some tips for you:
- Use Conventional Commits for the PR title: `type(scope): description`
- Types: feat, fix, docs, style, refactor, perf, test, build, ci, chore
- Scopes are not an exhaustive list — pick the most specific scope for
the change and extend the list when a genuinely new area appears.
Examples:
- System components: dashboard, platform, operator, cilium, kube-ovn,
linstor, fluxcd, cluster-api
- Managed apps: postgres, mariadb, redis, kafka, clickhouse,
virtual-machine, kubernetes
- Development and maintenance: api, hack, tests, ci, docs, maintenance
- Breaking changes: append `!` after type/scope (`feat(api)!: ...`) or
add a `BREAKING CHANGE:` footer
- If it's a work in progress, consider creating this PR as a draft.
- Don't hesistate to ask for opinion and review in the community chats,
even if it's still a draft.
- Add the label `backport` if it's a bugfix that needs to be backported
to a previous version.
-->

## What this PR does

The ban on EXIT-trap cleanup in `hack/*.bats` was enforced by two lists
inside `hack/cozyreport.bats`: one naming the files known to be clean,
one freezing an exact trap count for each file that was not. Both had to
be edited from whatever change moved them, and that is where the guard
kept failing.

It is worth being exact about how badly, because the history is sharper
than "it went stale a few times". **The inventory was never correct on
main for a single commit.** It landed in #3567 at 15:25:20. #3195 had
landed `multus-install-cni-plugins.bats` carrying twelve traps at
15:24:37, forty-three seconds earlier. Replaying the old guard's own
logic against the tree at the very commit that introduced it already
gives `found != frozen`. Main then stayed red for roughly twenty-two and
three quarter hours. The first repair, #3584, landed *already red* the
next morning at 10:42:43, because #3548 had brought in
`run-kubernetes-talos-diagnostics_test.bats` with eight traps at
10:40:03, under three minutes ahead of it. That fix was correct and
bought zero green time. Green arrived only with the second repair,
#3602.

Neither pair of PRs shared a line, and each was green against its own
base. That is the whole mechanism, and it is why a third one-line repair
is not the answer.

The mechanism is structural rather than careless. A change that adds a
trap to its own file had to edit a string in a suite it otherwise never
touches, so two changes sharing no line still invalidated each other:
each stayed green against its own base, git merged both cleanly, and the
guard went red only once the second one landed. A file *arriving* with
traps was worse still, because nothing in its author's diff pointed at
that string at all. That is exactly how
`run-kubernetes-talos-diagnostics_test.bats` got in, twice.

That contention is not in the past tense. Two open PRs are editing that
one line right now, and they disagree about what it should say: #3575
adds `run-kubernetes-talos-diagnostics_test.bats=8` to it, repeating a
repair that has already landed, and #3441 removes
`select-e2e_test.bats=15` from it, because it converts that file.
Neither PR is about EXIT traps. Both have to touch that string anyway,
and whichever lands second is wrong until someone edits it again.

So this PR is not fixing a red main. It removes the thing that keeps
making main red, which is why it is worth more than the one-line fix
that is now the established habit.

One honest caveat about its own landing. The textual-conflict property
is steady-state: a branch forked *before* the declaration exists has no
line to disagree with, so it converts traps in its own file, merges
clean, and the count only goes wrong once both sides are on main. I
checked this against #3441, which takes `select-e2e_test.bats` from
fifteen traps to zero. Merged after this, that file would declare
fifteen and hold none. What the move buys even in that case is that the
red names a file the branch already edited, the repair is one line
inside it, and rebasing before merge catches it on the branch's own CI.
None of those three held against the central inventory. Whoever merges
this should expect one such adjustment on the conversion branches still
in flight.

So this replaces both lists with a declaration each file makes about
itself: one `# EXIT-TRAP DEBT: N` comment in its leading comment block.
A file carrying no declaration must install no EXIT trap. Growing or
shedding a trap now fails in the file the change already edits, so two
changes that disagree about a count get a real textual conflict instead
of silently invalidating each other, and a change that leaves the traps
alone edits nothing.

**The include list is redundant, not lost.** It named the files proven
clean, so that a trap reappearing in one of them would fail. Under the
new rule those files carry no declaration, and a file with no
declaration must hold zero traps, so a trap reappearing in any of them
fails on its own, with no list to be on. Coverage widens rather than
narrows: the two lists named twenty files between them, and the rule
covers all fifty bats files under `hack/`, subdirectories included, plus
the ones added tomorrow.

Rebasing this branch onto current main is the property working. Main has
since gained `hack/kubernetes-pre-delete-hook.bats` and
`hack/tenant-pre-delete-hook.bats`, and `hack/cozyreport.bats` grew by
some eight hundred lines. Neither new file installs an EXIT trap, so
neither needed a declaration and neither needed an edit here; the rebase
took no conflict at all. Under the inventory, each arriving file was a
coin toss on whether somebody had remembered the string.

To be precise about what the inventory could and could not do, since it
is easy to overstate: it did *notice* a new file carrying traps. The
string it compared was built by scanning the directory, so an arriving
file appended a token and failed the comparison, which is exactly how
main went red. What it could not do is let that file arrive without an
edit in a foreign suite. Being seen and being absorbable are different
properties, and only the second one decides whether two changes can land
independently.

The declaration is pinned in both directions. Declaring N while holding
N+1 fails, obviously; declaring N while holding N−1 fails too. Without
that second half the number becomes a ceiling and rots upward: somebody
converts half a file, the declaration stays, and the guard quietly
licenses traps that were removed long ago.

It is read only from the leading comment block, under the shebang and
above the first line of code. A `.bats` file is shell that writes shell,
so the same line turns up inside a heredoc, a fixture writer or an
expected-output string, where it is data belonging to one test;
honouring it there would let an unrelated fixture excuse a real trap,
silently, with nothing in that test's own diff looking like a
declaration. A comment block is the region with no interior; stopping
instead at the first `@test` would still read a line out of a helper's
heredoc.

Not every counted handler is debt. A trap inside an explicit subshell
does not replace the one the `bats` binary installs, so a test failing
inside `( … )` still prints its `not ok`, checked against a test-level
trap in the same file, where the TAP line vanishes.
`hack/e2e-test-openapi.bats` kills a backgrounded `kubectl proxy`
exactly that way, and "convert it like the others" would leak a process
holding a fixed port and wedge the next run. Its declaration now records
the carve-out instead of scheduling a conversion, and because the
ratchet is exact in both directions, *removing* that trap fails too, so
the count protects the construct rather than marking it for deletion.
`docs/agents/e2e-testing.md` previously scoped this exception to
Chainsaw `script` steps only; it now names the BATS subshell case as
well.

The counting bounds the keyword and the signal the same way, at any
character that cannot be part of an identifier, and matches the signal
in either case. Whitespace on the right missed `trap … EXIT; cd "$tmp"`;
whitespace on the left missed `tmp=$(mktemp -d);trap … EXIT` and `(trap
… EXIT; true)`; upper case missed `trap … exit`, which bash and dash
both install. All of those are real handlers that scored zero. The left
boundary is the one worth dwelling on, because the inventory being
replaced had none at all and *did* catch the semicolon form. Getting it
wrong here would have narrowed coverage while the commit claimed to
widen it. A plain word boundary is not enough either: `bootstrap ` ends
in `trap `, and it has to keep scoring nothing, or the documented answer
to a red guard (add a debt line) would buy a file a permanent licence
for one real trap to silence a line that has none.

Two handlers sharing one line are reported rather than counted, because
the count is a count of lines and the second would otherwise arrive
free. Splitting such a line properly needs a shell parser, since a
semicolon inside a handler's own quoted action is not a separator, and
guessing wrong undercounts, the one direction a ratchet cannot afford.

**What this does not fix.** The declaration is still a loophole: a new
file can write `# EXIT-TRAP DEBT: 8` instead of cleaning up, and nothing
here makes that impossible. What changes is that the admission is local
and visible. It sits at the top of the file it excuses, in front of
whoever reviews that file, instead of being a number in a neighbouring
suite nobody in that review is reading. Today's loophole is the same
size and invisible.

Three more limits, all stated in the guard's own header rather than left
to be discovered. The scan is lexical, so a signal computed at runtime
and a quoted action spanning physical lines without a backslash are both
invisible. An exact count catches addition and removal but never
substitution: swap the openapi file's subshell trap for a test-level one
and the total stays 1. And the scan reads `.bats` only, so a handler
arriving through a sourced `.sh` is outside it.
`hack/e2e-chainsaw/_lib/run-kubernetes.sh` installs two right now, and
each is benign for its own reason rather than by design: the one in
`cozy_capture_tenant_talos` because that function is declared with `(`
and so runs in a subshell, the one in `run_kubernetes_test` because no
`@test` calls it despite being declared with `{`. Three `hack/*.bats`
source that library, and two tests in the converted file call
`cozy_capture_tenant_talos`, so flipping a single `(` to `{` reinstates
a test-level handler in both of them with the guard green. Widening the
scan to `.sh` would mean counting handlers that are correct in a script
and wrong only in a test body, so the honest answer is that this is
where the instrument stops.

Three further boundaries, recorded here so they land as known edges
rather than as surprises. The old include list also failed when a file
named on it disappeared from the tree; the new rule can only judge a
file that is present, so a deleted converted file goes unnoticed. That
is a genuinely smaller check, though its absence shows up in the diff
that deletes the file. The guard's own failure messages are code lines,
so they are scanned by the pattern they belong to: they pass today only
because no bare `EXIT` or `0` happens to follow the keyword in any of
them, and a rewording that introduced one would make the file demand a
debt of itself. It fails loudly rather than quietly, and the fixture
writers and test titles already split the keyword from the signal for
this reason, but the messages do not. Finally,
`docs/agents/e2e-testing.md` bans test-level `EXIT` *and* `RETURN`
traps, while every mechanical guard this repo has had, the one being
deleted included, matches only `EXIT` and `0`. `hack/` holds no RETURN
trap today, so nothing regresses here, but half of that documented rule
has never had an executor.

The guard moves out of `hack/cozyreport.bats` into
`hack/bats-no-exit-trap.bats`, because its subject is every unit suite
under `hack/` and not the report collector it grew up in. Living inside
`cozyreport.bats` is precisely why unrelated PRs kept converging on one
line. `hack/md-no-hardwrap.bats` is the neighbouring precedent.

Separately, the eight EXIT traps in
`hack/run-kubernetes-talos-diagnostics_test.bats` are converted to
end-of-body cleanup rather than given a declaration. That file was
written six weeks after the ban was documented, so "it predates the
rule" is not available to it, and handing it a debt is literally the
move that failed the last two times. The remaining files keep their
declarations, since a mass conversion would collide with the branches
that own them.

One note on where the prose sits. The guard's own header carries only
what a reader has to keep true: why the ban exists, the two carve-outs,
where the declaration lives and why there, that the count is exact in
both directions, why the marker is anchored at column zero, and where
the lexical scan stops. Everything above about *how* the inventory
failed (the timestamps, the PR numbers, the arithmetic) is deliberately
not in the file. It is true today and will rot on its own; a comment
nobody is obliged to maintain is how a file grows a paragraph that is
quietly false.

### Screenshots

<!-- REQUIRED for UI changes: attach screenshots or screen recordings
demonstrating
the visual impact of your changes. PRs with UI changes without
screenshots will not be merged. -->

### Downstream repositories

<!--
Cozystack is upstream for repositories that are NOT kept in sync with it
automatically, and nothing in CI compares the two sides, so a change
here can break them silently.

Which change forces what, and which file to touch, is the trigger map in
docs/agents/contributing.md under "Downstream Repositories". Walk it
before you tick anything.

IF YOU ARE AN AI AGENT OR AN LLM, THIS IS PART OF THE TASK, NOT A FORM
TO FILL IN:

1. Walk the trigger map against the actual diff of this PR, file by
file. Do not decide from the PR title or from your memory of what you
changed.
2. For every repository you tick, open the follow-up PR there and link
it on the same line. A ticked box with no link claims work that does not
exist, which is worse than an empty box. Search that repository for an
open PR or issue covering it first, and link that instead of filing a
duplicate.
3. When the follow-up is out of scope, or needs a decision that is not
yours to make, open an issue in that repository instead, link that, and
say so in the PR body. Do not push a speculative PR into another
repository just to fill a line here.
4. Do not tick "No downstream repository is affected" to make the
checklist go away. If you are unsure, leave every box empty and say so
in the PR body, so a human decides.
-->

- [x] No downstream repository is affected by this change
- [ ] [cozystack/website](https://github.com/cozystack/website) -
follow-up:
- [ ]
[cozystack/terraform-provider-cozystack](https://github.com/cozystack/terraform-provider-cozystack)
- follow-up:
- [ ]
[cozystack/ansible-cozystack](https://github.com/cozystack/ansible-cozystack)
- follow-up:
- [ ] [cozystack/ccp](https://github.com/cozystack/ccp) - follow-up:
- [ ] [cozystack/talm](https://github.com/cozystack/talm) - follow-up:
- [ ] [cozystack/cozyhr](https://github.com/cozystack/cozyhr) -
follow-up:
- [ ] [cozystack/cozy-proxy](https://github.com/cozystack/cozy-proxy) -
follow-up:
- [ ]
[cozystack/cozystack-telemetry-server](https://github.com/cozystack/cozystack-telemetry-server)
- follow-up:
- [ ]
[cozystack/external-apps-example](https://github.com/cozystack/external-apps-example)
- follow-up:
- [ ] [cozystack/examples](https://github.com/cozystack/examples) -
follow-up:

### Release note

<!--  Write a release note:
- Explain what has changed internally and for users.
- Start with the same `type(scope):` prefix as in the PR title
- Follow the guidelines at
https://github.com/kubernetes/community/blob/master/contributors/guide/release-notes.md.
-->

```release-note
test(tests): each hack/*.bats file now declares its own remaining EXIT-trap debt in a `# EXIT-TRAP DEBT: N` header comment, checked by hack/bats-no-exit-trap.bats, replacing the central inventory in hack/cozyreport.bats
```


<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

- **Testing**
- Added automated auditing for `EXIT` traps across Bats end-to-end
tests, including validation of tracking declarations and edge cases.
- Improved diagnostics tests by replacing trap-based temporary-directory
cleanup with explicit cleanup steps.
  - Added tracking annotations for remaining trap-related cleanup work.

- **Documentation**
- Clarified when traps are permitted inside self-contained subshells and
how remaining cleanup debt is reported.
  - Updated review guidance for consistent end-to-end test maintenance.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
src_to_suites() covered PackageSource names ending in -application plus
external-dns, and the filter after the graph walk dropped every other
source it reached. cozystack.kuberture and cozystack.securitygroup-
controller are neither, so both were walked to and then discarded: a CNI
change ran neither of the two controller suites most likely to regress
from it, and a change to either package reached no suite at all and
escalated to the full run instead of selecting the one that covers it.

Every reached source now maps through the table. That table is the
inverse of select-install.sh's suite_to_source(), and a test walks every
suite through both, so the next source name fitting neither convention
fails there rather than quietly making its own suite unreachable.

The cilium test measured the gap rather than hid it, asserting the
19 suites the selector could reach and naming the two it could not.
The subtraction is removed rather than inverted: kept as an exclusion
list it would stay green if the walk stopped reaching them for an
unrelated reason. What replaces it is a second assertion against a tree
holding one suite the graph reaches and one no source names, which an
escalation cannot satisfy.

Assisted-By: Claude <noreply@anthropic.com>
Signed-off-by: Aleksei Sviridkin <f@lex.la>
@lexfrei
Aleksei Sviridkin (lexfrei) force-pushed the fix/select-e2e-per-path-coverage branch from 96e2625 to 3c10284 Compare August 9, 2026 18:34
@lexfrei

Copy link
Copy Markdown
Contributor Author

IvanHunters guard is fixed. On yq v4.53.3 it computes owners=2 and the file is 40/40 green. Your follow-up about pkg/ and go.mod is closed too, main added them to full_suite_pattern meanwhile.

The branch is rebased now and about half of what it carried is already in main, so it needs a re-read rather than a re-check. What is left is coverage decided per changed path, measured at 88 of the 159 packages losing their escalation today when the same diff also touches one suite, plus the mapping for sources not named <suite>-application, which is what makes kuberture and securitygroup reachable from their own packages.

@lexfrei
Aleksei Sviridkin (lexfrei) force-pushed the fix/select-e2e-per-path-coverage branch from 3c10284 to 528f4f9 Compare August 9, 2026 18:52
The full-suite escalation for a changed package that reaches no suite was
read off the merged selection, so it depended on the whole diff rather
than on the path that needed it. Any other changed path contributing a
suite name made the selection non-empty and the escalation vanished with
nothing in the output to record it: edit a platform component, adjust the
Chainsaw test beside it, and the component ran that single suite instead
of everything. Over one values.yaml per package under
packages/{apps,system,extra}, 82 of the 159 escalate on their own and
stop escalating once an unrelated suite edit joins the diff.

Coverage is now decided per changed path. Each path resolves through its
owning sources to the suites that exist, and a path reaching none of them
forces the full run whatever the rest of the diff selected. The unit is
the path rather than the source: system/postgres-operator belongs to two
PackageSources where one reaches no suite, so deciding per source would
escalate on that half and run everything for every change to it.

The escalation names its cause on stderr, as every other escalation in
this script already does. Most packages reach no suite of their own, so
this is the branch a full run usually takes; without a line saying which
sources bought it, "why did this run everything" has no answer in the log.

The walk moves into resolve_suites() to be called once per path. POSIX sh
has no `local`, so the function writes into the caller's scope and is
safe only inside the $( ) its single call site wraps it in; a test pins
that the call count stays one.

Two entries leave src_to_suites. `postgres-application` restated what
stripping the suffix produces and `external-dns` restated what carrying
the source name through produces, so both made the exception list read as
longer than the set of real exceptions.

Assisted-By: Claude <noreply@anthropic.com>
Signed-off-by: Aleksei Sviridkin <f@lex.la>

@IvanHunters IvanHunters left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Overview

This PR fixes a real E2E test-selection defect: select-e2e.sh used to read the "escalate to full suite" decision off the merged selection across the whole diff, so a changed package with no owning Chainsaw suite would silently lose its escalation whenever the same PR also touched any other path that selected a suite (#3330). It also repairs src_to_suites(), which dropped cozystack.kuberture and cozystack.securitygroup-controller from the mapping table even though the reverse-dependency walk reached them, so changes to those packages either escalated to a full run or (worse) ran neither controller's own suite (#3665).

I verified this against the current PR head and current main, not just the diff text.

Verdict: LGTM. The mechanism is correct, the regression tests are non-vacuous (pinned by mutation, independently spot-checked below), and the diff is scoped to exactly the three files the PR body claims (hack/select-e2e.sh, hack/select-e2e_test.bats, docs/agents/e2e-testing.md) — no packages/, chart, RBAC, or migration surface.

Status of previously requested changes

The PR currently shows CHANGES_REQUESTED. I checked every prior review/thread against the code that is actually at HEAD today:

  1. [MAJOR, blocking] Test premise-guard pipes yq straight into awk -F'\t' (originally flagged at hack/select-e2e_test.bats:229-230): mikefarah yq emits a literal two-byte \t in string concatenation, not a real tab, so the guard computed owners=0 and failed the PR's own test suite under set -e.
    Status: Addressed. At HEAD (hack/select-e2e_test.bats:587-588) the guard now routes through echo "$owners_tsv" | awk -F'\t' ..., matching the pattern used everywhere else in both scripts. I read the exact bytes at HEAD to confirm this rather than trusting the follow-up comment claiming it.

  2. [Major, CodeRabbit] OWNERS/REVERSE captured via a pipe into sort, so set -e sees sort's exit status, not yq's (originally flagged around hack/select-e2e.sh:79-80).
    Status: Addressed, but not by this PR's own diff. The current diff does not touch this section at all — a byte-for-byte diff of hack/select-e2e.sh between main and HEAD shows it is identical on both sides. It was fixed on main directly (commit 37a395363f, "stop select-e2e from deciding on inputs it never checked"), and this branch inherited it via rebase. The end state at HEAD (hack/select-e2e.sh:212-222, separate OWNERS=$(build_owners_index) || broken=... / REVERSE=$(build_reverse_deps) || broken=..., sorted only after the status check) is correct.

  3. [CodeRabbit, first thread] paste -sd , - assumed to require newline-separated input (originally hack/select-e2e.sh:153-155, now :296-305).
    Status: Not a real bug — independently confirmed. src=$(echo "$OWNERS" | awk ... '{print $2}') emits one line per matching awk record, so $src is newline-separated by construction whenever more than one source owns a path, and echo "$src" | paste -sd , - joins it correctly into a comma group. I traced the awk call myself to confirm rather than taking either side's word for it.

  4. The prior LGTM review is currently DISMISSED, not active. Its timing lines up with a later push, which is consistent with an automatic stale-review dismissal on push, not a retraction over a new defect. Net effect: the outstanding CHANGES_REQUESTED decision is procedurally stale relative to the current code — the concern it cited is fixed, and no fresh approval has been recorded since.

Regression-gate check (false-negative E2E coverage)

Given the nature of this change, I specifically checked whether the new per-path scoping could itself introduce a silent coverage gap:

  • The classification loop (hack/select-e2e.sh:237-320) is unchanged in its glob rules (full_suite_pattern, inert_config_pattern); what changed is how the packages/(apps|system|extra)/... branch aggregates results.
  • Any path inside packages/ with no OWNERS entry still conservatively escalates (hack/select-e2e.sh:307-310, unchanged).
  • Any path matching none of the classification rules escalates via the "unclassified" fallback (hack/select-e2e.sh:314-319, unchanged) — this generalized fail-safe already subsumes the pkg/, go.mod/go.sum, tools/ gap the original review flagged as a follow-up; those paths are also individually present in full_suite_pattern at HEAD.
  • The new per-group escalation (hack/select-e2e.sh:379-394) is the actual fix for #3330: it forces escalate_to_full_suite the instant any one changed path's group resolves to zero suites, before the union/backstop steps — so no other path's non-empty selection can mask it.
  • securitygroup and kuberture are confirmed to exist as real Chainsaw suite directories (hack/e2e-chainsaw/securitygroup/, hack/e2e-chainsaw/kuberture/), and src_to_suites() maps to them correctly. The round-trip test against select-install.sh's independently-authored suite_to_source() is a real cross-check, not a tautology.

No confirmed regression found in the scoping logic itself.

Non-blocking / nits

  • hack/select-e2e.sh:112-126: the nested-suite escalation branch is a latent no-op (it still emits $all_apps, itself built with -maxdepth 2), already called out by the prior reviewer as pre-existing and not touched by this diff. Worth a follow-up issue.
  • docs/agents/e2e-testing.md slightly overstates that Chainsaw "discovers suites recursively" — the depth-2 scan doesn't match that today. Same latent issue as above.
  • There is a known rename-detection blind spot in the E2E-selection git diff --name-only call in .github/workflows/pull-requests.yaml (it does not use --no-renames, unlike the docs-gate a few lines above it), but it predates this PR and is out of scope here — flagging for awareness only.

Closing

The headline fix is correct, conservative in every failure direction I could construct, and backed by tests pinned to fail when the property they guard is removed. The only open item is procedural — the CHANGES_REQUESTED state reflects a since-fixed concern and a dismissed follow-up approval, not a live defect. Approving to clear the stale gate; no code changes required from my read.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area/ci Issues or PRs related to CI workflows, GitHub Actions, automation area/testing Issues or PRs related to testing (e2e, bats, unit tests) kind/bug Categorizes issue or PR as related to a bug size/L This PR changes 100-499 lines, ignoring generated files

Projects

None yet

3 participants