fix(ci): decide E2E test coverage per changed path - #3441
Conversation
|
Caution The consumer version of Gemini Code Assist on GitHub has been sunset. All code review activity has officially ceased. |
|
Note Reviews pausedIt 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 Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
📝 WalkthroughWalkthroughThe 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. ChangesE2E suite selection
Estimated code review effort: 4 (Complex) | ~45 minutes Possibly related issues
Possibly related PRs
Suggested reviewers: 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
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
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 |
a701d72 to
365195d
Compare
|
Caution The consumer version of Gemini Code Assist on GitHub has been sunset. All code review activity has officially ceased. |
IvanHunters
left a comment
There was a problem hiding this comment.
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 -tx1 → 61 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\tb → 61 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, anddocs/agents/e2e-testing.md. Nopackages/, 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 underpackages/{apps,system,extra}. Exactly 8 selections change: 5 networking packages (kilo,kubeovn,cilium,cilium-networkpolicy,gateway-api-crds) 19→21 suites andprometheus-operator-crds10→11 (both addkuberture+securitygroup, widening);kuberturefull→[kuberture]andsecuritygroup-controllerfull→[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.shcleanly: on any test failure it invokeskubectl/crust-gatheragainst the ambient kube-context. Ran the failing/relevant test bodies in isolation undersh -euinstead. - Verified the guard failure on mikefarah yq v4.46.1. Could not execute CI's exact yq build; however the codebase's
echo-through-awkpattern 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_patternis unchanged): rootpkg/,go.mod,go.sum,test/,tools/are neither infull_suite_pattern(hack/select-e2e.sh:23) nor underpackages/, so a diff confined to them falls through to "silently ignored" → CI skips E2E. Verified: apkg/apiserver/foo.gochange and ago.modchange both produce empty output.pkg/is compiled into thecozystack-api/cozystack-operatorimages, so apkg-only change ships with no E2E. Consider addingpkg/,go.mod,go.sumtofull_suite_pattern.
365195d to
caddc93
Compare
|
Fixed the guard. It now routes the yq output through For the record, the test was green on CI (the On the |
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 `@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
📒 Files selected for processing (3)
docs/agents/e2e-testing.mdhack/select-e2e.shhack/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
myasnikovdaniil
left a comment
There was a problem hiding this comment.
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
-
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 runschainsaw 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_appsdiscovery would need to become recursive. -
Seconding the pre-existing
full_suite_patternfall-through: a diff confined topkg/,go.mod,go.sum,test/, ortools/produces empty output and CI skips E2E, yetpkg/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, foldingpkg/|go.mod|go.suminto 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.
caddc93 to
71208d2
Compare
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 `@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
📒 Files selected for processing (3)
docs/agents/e2e-testing.mdhack/select-e2e.shhack/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
|
IvanHunters Fixed at Verified by breaking things: 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 The |
71208d2 to
58f3492
Compare
|
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. |
58f3492 to
66ee550
Compare
|
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. |
66ee550 to
aa8211c
Compare
|
IvanHunters the guard you flagged was fixed in the 27 July update:
The tests also cleaned up through 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 One thing about the checks: the unit job is red on main because |
aa8211c to
96e2625
Compare
…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 -->
…#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>
96e2625 to
3c10284
Compare
|
IvanHunters guard is fixed. On yq v4.53.3 it computes 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 |
3c10284 to
528f4f9
Compare
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>
528f4f9 to
587a7b9
Compare
IvanHunters
left a comment
There was a problem hiding this comment.
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:
-
[MAJOR, blocking] Test premise-guard pipes
yqstraight intoawk -F'\t'(originally flagged athack/select-e2e_test.bats:229-230): mikefarahyqemits a literal two-byte\tin string concatenation, not a real tab, so the guard computedowners=0and failed the PR's own test suite underset -e.
Status: Addressed. At HEAD (hack/select-e2e_test.bats:587-588) the guard now routes throughecho "$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. -
[Major, CodeRabbit]
OWNERS/REVERSEcaptured via a pipe intosort, soset -eseessort's exit status, notyq's (originally flagged aroundhack/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 ofhack/select-e2e.shbetweenmainand HEAD shows it is identical on both sides. It was fixed onmaindirectly (commit37a395363f, "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, separateOWNERS=$(build_owners_index) || broken=.../REVERSE=$(build_reverse_deps) || broken=..., sorted only after the status check) is correct. -
[CodeRabbit, first thread]
paste -sd , -assumed to require newline-separated input (originallyhack/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 matchingawkrecord, so$srcis newline-separated by construction whenever more than one source owns a path, andecho "$src" | paste -sd , -joins it correctly into a comma group. I traced theawkcall myself to confirm rather than taking either side's word for it. -
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 outstandingCHANGES_REQUESTEDdecision 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 thepackages/(apps|system|extra)/...branch aggregates results. - Any path inside
packages/with noOWNERSentry 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 thepkg/,go.mod/go.sum,tools/gap the original review flagged as a follow-up; those paths are also individually present infull_suite_patternat HEAD. - The new per-group escalation (
hack/select-e2e.sh:379-394) is the actual fix for #3330: it forcesescalate_to_full_suitethe 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. securitygroupandkubertureare confirmed to exist as real Chainsaw suite directories (hack/e2e-chainsaw/securitygroup/,hack/e2e-chainsaw/kuberture/), andsrc_to_suites()maps to them correctly. The round-trip test againstselect-install.sh's independently-authoredsuite_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.mdslightly 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-onlycall 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.
What this PR does
hack/select-e2e.shescalates 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-operatorbelongs 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*-applicationnames plusexternal-dns, and the final filter dropped the rest, socozystack.kubertureandcozystack.securitygroup-controllerwere 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 ofselect-install.sh'ssuite_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.yamlper package underpackages/{apps,system,extra}: 159 packages, 8 selections change, all of them the mapping repair. Five networking packages andprometheus-operator-crdswiden — 19 to 21 suites and 10 to 11, both gainingkubertureandsecuritygroup.kubertureandsecuritygroup-controllernarrow 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
kubertureandsecuritygroup-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_suiteswrites into the caller's scope because POSIX sh has nolocal, 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 nochainsaw-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.mdagainst the diff. The changed files arehack/select-e2e.sh,hack/select-e2e_test.batsanddocs/agents/e2e-testing.md. The onlyhack/-related trigger is cozystack/ccp, which gates onhack/package.mk,hack/common-envs.mkandhack/update-crd.shand on what a make target does; nothing here is moved, renamed, or changed in make-target behaviour. The ansible-cozystack and talm triggers are abouthack/e2e-prepare-cluster.bats, which is untouched.Release note
Closes #3330
Closes #3665