Skip to content

ci: stop cancelling in-progress runs on main - #6506

Merged
devantler merged 9 commits into
mainfrom
claude/ci-main-run-cancellation-6384
Aug 6, 2026
Merged

ci: stop cancelling in-progress runs on main#6506
devantler merged 9 commits into
mainfrom
claude/ci-main-run-cancellation-6384

Conversation

@devantler

Copy link
Copy Markdown
Contributor

🤖 Generated by the Agentic Engineer

Why

main can report green on tests that never ran. Every merge to main shares one concurrency key, so a merge landing while the previous merge's checks are still running cancels them — and a cancelled check is not a failed check, so the commit status settles green anyway.

That is how a broken Go test suite reached main unnoticed and then surfaced as an unrelated PR author's problem days later.

What

Runs on main are now allowed to finish. Superseded pull-request and merge-queue runs still cancel exactly as before, so the fast-feedback behaviour developers rely on is unchanged — only the default branch, where each commit's run is its own verification record, stops being evicted.

Also adds a guard so no future workflow can reintroduce the same eviction on main.

Fixes #6384
Part of #6373

The concurrency group is keyed on github.ref, so every push to main shares one
key and cancel-in-progress: true evicts the previous merge's checks mid-flight.
A cancelled check is not a failed check, so branch protection and the commit
status settle on a green-looking main while the verification that would have
caught a regression never completed.

Gate the cancellation on the ref instead. Pull-request and merge-queue runs each
carry their own ref, so superseded runs there still cancel unchanged.

Fixes #6384
Part of #6373
A quoted "true" is equally truthy to GitHub but parses as a string, so the
type-based comparison passed over it.
@github-actions

github-actions Bot commented Aug 6, 2026

Copy link
Copy Markdown
Contributor

MegaLinter analysis: Success

✅ Linters with no issues

actionlint, bash-exec, git_diff, hadolint, jscpd, jsonlint, lychee, markdown-table-formatter, markdownlint, prettier, prettier, shellcheck, shfmt, stylelint, syft, trivy-sbom, trufflehog, v8r, v8r, yamllint

Notices

📣 MegaLinter 9.5.0 is out! Discover the new features and security recommendations in the release announcement. (Skip this info by defining SECURITY_SUGGESTIONS: false)

See detailed reports in MegaLinter artifacts

MegaLinter is graciously provided by OX Security
Show us your support by starring ⭐ the repository

@devantler

Copy link
Copy Markdown
Contributor Author

🤖 Generated by the Agentic Engineer

What I exercised, and what I could not

Pull-request cancellation still works — observed on GitHub's real evaluator, not reasoned. This PR got a free natural experiment: I pushed twice, 73 seconds apart, with the new expression already in place.

head CI - KSail run
33869dbf (first push) cancelled
c5233109 (second push) proceeded

For a pull_request event github.ref is refs/pull/6506/merge, so ${{ github.ref != 'refs/heads/main' }} evaluates true, cancellation stays on, and the superseded run was evicted exactly as before. That satisfies acceptance criterion 2 and also proves GitHub parses and evaluates the expression at workflow level — a malformed one would have failed the run outright.

The default-branch half cannot be proven pre-merge. It is the complementary branch of that same evaluated expression, but demonstrating it needs two merges to main in quick succession (acceptance criterion 3). I will verify it post-merge against the first pair of closely-spaced merges rather than claim it now.

A correction to the issue's premise

#6384 says "this repo gates main behind a merge queue" and asks that the fix be checked against it. It does not, today. Verified live: repos/devantler-tech/ksail/rulesets contains pull_request, non_fast_forward, code_quality and code_scanning rules and no merge_queue rule, and classic branch protection returns 404 Branch not protected. The commented-out cancel-stale-merge-queue job in ci.yaml ("kept for future use when jobs are moved to merge_group") is consistent with that.

So the merge-queue interaction is currently inert. The expression is still correct if a queue is enabled later: each queue entry gets its own refs/heads/gh-readonly-queue/... ref, which is != refs/heads/main, so those runs keep cancelling per-entry and are never starved.

Scope check

ci.yaml is the only workflow in this repo with a truthy cancel-in-progress that can run on main — acceptance criterion 4. The other four workflows triggering on push to main (desktop, release, todos, web-ui) carry no concurrency block at all, so there is nothing to gate; the added repo-wide test pins that and will fail if any of them grows one.

The same defect exists in five other repositories — tracked separately as devantler-tech/monorepo#2690, not fixed here.

@devantler

Copy link
Copy Markdown
Contributor Author

🤖 Generated by the Agentic Engineer

Requesting review at c523310967b3. The code-relevant checks are green — 🧪 Test, 🧹 Lint - golangci-lint, 🏗️ Build, 🧹 Lint - mega-linter (which runs actionlint over the changed workflow) — with 33 passing and 0 failing. The remaining pending jobs are the 27-job System Test cluster matrix, which provisions clusters and cannot be affected by a workflow-level concurrency expression or a workflow-parsing unit test.

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Aug 6, 2026

Copy link
Copy Markdown

@devantler: I will review the changes at c523310967b3d85935e5747bbb4ac9a0528eac2d.

✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@coderabbitai

coderabbitai Bot commented Aug 6, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

The CI workflow now gives main runs unique concurrency groups and prevents their cancellation. Pull-request and merge-queue runs retain ref-based groups and cancellation. New tests parse workflow YAML, verify these settings, scan workflows triggered by pushes to main, and detect unconditional cancellation values.

Possibly related issues

  • devantler-tech/monorepo#2690 — The change addresses the same CI concurrency issue by preserving main workflow runs while retaining cancellation for superseded non-main runs.
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 33.33% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and concisely describes the primary change: preventing cancellation of in-progress runs on main.
Description check ✅ Passed The description explains the main concurrency change, its rationale, preserved behavior, regression tests, and linked issues.
Linked Issues check ✅ Passed The changes satisfy #6384 by making main runs unique and non-cancellable while preserving ref-based cancellation for pull requests and merge-queue runs.
Out of Scope Changes check ✅ Passed The workflow update and regression tests directly support the linked issue objectives, with no unrelated code changes identified.

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.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

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 @.github/workflows/ci.yaml:
- Around line 29-33: Make the main-branch concurrency group unique per workflow
run by using github.run_id when github.ref is refs/heads/main, while retaining
github.ref-based groups for pull requests and merge groups; update the
corresponding expectations in internal/ciharness/ci_workflow_test.go at lines 42
and 83-93 to validate the run-specific main group and preserved ref-based
behavior.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository YAML (base), Organization UI (inherited)

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 260535c4-f7d3-4b42-bdec-3052b8443e30

📥 Commits

Reviewing files that changed from the base of the PR and between 2eaaeec and c523310.

📒 Files selected for processing (2)
  • .github/workflows/ci.yaml
  • internal/ciharness/ci_workflow_test.go
📜 Review details
⏰ Context from checks skipped due to timeout. (18)
  • GitHub Check: 🧪 System Test (Docker) (K3s, Docker, true, --cni Calico --csi Disabled --load-balancer Disable...
  • GitHub Check: 🧪 System Test (Docker) (K3s, Docker, true, --gitops-engine ArgoCD --local-registry ghcr.io/dev...
  • GitHub Check: 🧪 System Test (Docker) (Vanilla, Docker, true, --gitops-engine Flux --local-registry ghcr.io/d...
  • GitHub Check: 🧪 System Test (Docker) (K3s, Docker, true, --gitops-engine Flux --local-registry ghcr.io/devan...
  • GitHub Check: 🧪 System Test (Docker) (K3s, Docker, true, --name system-test-cluster --cni Cilium --csi Enabl...
  • GitHub Check: 🧪 System Test (Docker) (Vanilla, Docker, true, --gitops-engine ArgoCD --local-registry ghcr.io...
  • GitHub Check: 🧪 System Test (Docker) (KWOK, Docker, true, --gitops-engine Flux --local-registry ghcr.io/deva...
  • GitHub Check: 🧪 System Test (Docker) (KWOK, Docker, true, --gitops-engine ArgoCD --local-registry ghcr.io/de...
  • GitHub Check: 🧪 System Test (Docker) (Vanilla, Docker, true, --name system-test-cluster --cni Cilium --csi E...
  • GitHub Check: 🧪 System Test (Docker) (KWOK, Docker, true, --name system-test-cluster --cni Cilium --csi Enab...
  • GitHub Check: 🧪 System Test (Docker) (KWOK, Docker, true, --cni Calico --csi Disabled --load-balancer Disabl...
  • GitHub Check: 🧪 System Test (Docker) (Vanilla, Docker, true)
  • GitHub Check: 🧪 System Test (Docker) (K3s, Docker, true)
  • GitHub Check: 🧪 System Test (Docker) (Vanilla, Docker, true, --cni Calico --csi Disabled --load-balancer Dis...
  • GitHub Check: 🧪 System Test (Docker) (KWOK, Docker, true)
  • GitHub Check: 🛡️ Vulnerability Scan
  • GitHub Check: 📊 Code Coverage
  • GitHub Check: Analyze (go)
🧰 Additional context used
📓 Path-based instructions (4)
.github/workflows/*.yaml

📄 CodeRabbit inference engine (AGENTS.md)

Validate workflow changes with mega-linter-runner -f go; MegaLinter runs actionlint for GitHub Actions workflows.

Files:

  • .github/workflows/ci.yaml
**/*.go

📄 CodeRabbit inference engine (AGENTS.md)

**/*.go: Use Go 1.26.1 or newer, matching the version declared in go.mod.
All user-supplied file path arguments in CLI commands must be canonicalized with fsutil.EvalCanonicalPath before use; create parent directories first for new output paths.
Use fsutil.ReadFileSafe for constrained file reads instead of reimplementing path-containment checks.
Do not manually register MCP or Copilot tool handlers; runnable Cobra commands are exposed through automatic generation in pkg/toolgen.
Use a typed experimental field in ksail.yaml for configuration-gated behavior that is not an entire command; regenerate the schema and CRD.
Graduate validated experimental features by deleting the single Guard call; do not retain unnecessary experimental scaffolding.
Run formatting and linting with golangci-lint run --fix and golangci-lint run --timeout 5m; validate with go build and go test ./....

Files:

  • internal/ciharness/ci_workflow_test.go
**/*.{go,yaml,yml,md,mdx,ts,tsx,json}

📄 CodeRabbit inference engine (AGENTS.md)

Generated files must not be hand-edited; run make generate as the canonical regeneration command.

Files:

  • internal/ciharness/ci_workflow_test.go
**/*_test.go

📄 CodeRabbit inference engine (AGENTS.md)

Add regression tests for confident bug fixes and run flaky-test candidates repeatedly with go test -run <T> -count=10 ./....

Files:

  • internal/ciharness/ci_workflow_test.go
🧠 Learnings (3)
📚 Learning: 2026-07-14T16:00:38.424Z
Learnt from: devantler
Repo: devantler-tech/ksail PR: 6121
File: .github/workflows/ci.yaml:936-940
Timestamp: 2026-07-14T16:00:38.424Z
Learning: In GitHub Actions workflow shell steps that use `set -euo pipefail`, avoid using external commands (e.g., `grep -q` or `grep ... | ...`) directly as the condition in an `if` statement when the goal is a negative assertion (e.g., “string/pattern is absent”). External-command exit-code behavior under `set -e` can cause non-semantic failures (command errors, pipe issues, SIGPIPE, etc.) to be treated the same as a legitimate “no match,” potentially failing open. Prefer the robust pattern: capture command output first (e.g., `output="$(cmd)"` so real command failures abort due to `set -e`), then perform the assertion with a bash builtin string test such as `if [[ "$output" == *pattern* ]]; then ...; fi` (and use the negation of that builtin check for “absent”).

Applied to files:

  • .github/workflows/ci.yaml
📚 Learning: 2026-07-26T05:03:23.451Z
Learnt from: devantler
Repo: devantler-tech/ksail PR: 6363
File: .github/workflows/ci.yaml:308-313
Timestamp: 2026-07-26T05:03:23.451Z
Learning: In devantler-tech/ksail, the centralized GitHub Actions job `🧹 Lint - mega-linter` already runs MegaLinter’s `actionlint` for PRs that only change workflow files. When adding/adjusting workflow jobs (path filters included), don’t add extra/duplicated `mega-linter-runner -f go` (or other MegaLinter invocations) to specialized “script-validation” jobs solely because their `paths`/path filters include `.github/workflows/*.yaml`—rely on the centralized `Lint - mega-linter` job for workflow/YAML actionlint validation. Keep the existing `ci.yaml` EKS smoke-script/path-filter behavior so that workflow changes (notably `system-test-eks.yaml`) still trigger the intended command-line contract tests via the configured filtering.

Applied to files:

  • .github/workflows/ci.yaml
📚 Learning: 2026-08-02T19:26:41.922Z
Learnt from: devantler
Repo: devantler-tech/ksail PR: 6434
File: pkg/cli/clusterapi/eks_create_identity_test.go:0-0
Timestamp: 2026-08-02T19:26:41.922Z
Learning: In Go tests using Testify v1.11.1, do not flag require.NoError(t, err) inside an Eventually condition solely because the condition may run in another goroutine: require calls t.Errorf before FailNow, and t.Errorf marks the outer test as failed. However, prefer decomposing Eventually conditions so service errors, missing resources, and state or phase mismatches are reported separately for clearer diagnostics.

Applied to files:

  • internal/ciharness/ci_workflow_test.go

Comment thread .github/workflows/ci.yaml Outdated
Disabling cancellation is not sufficient on a shared group. A concurrency group
holds one running plus one pending run, and a third arrival cancels the pending
one — so with three merges in quick succession the second merge's checks are
still lost.

Key the group on github.run_id for main so nothing there queues behind or evicts
anything else. Pull requests and merge-queue entries keep the ref-keyed group.
@devantler

Copy link
Copy Markdown
Contributor Author

🤖 Generated by the Agentic Engineer

Valid, and it defeats acceptance criterion 1 of #6384 as I had implemented it — thank you. A concurrency group holds one running plus one pending run, and a third arrival cancels the pending one. So cancel-in-progress: false protected the running job while the second merge's checks could still be evicted while queued. The fix was half a fix.

Adopted the run-unique key for main in 0675cabb:

group: "ci-ksail-${{ github.workflow }}-${{ github.ref == 'refs/heads/main' && github.run_id || github.ref }}"
cancel-in-progress: ${{ github.ref != 'refs/heads/main' }}

Nothing on main now queues behind or evicts anything else. Pull requests and merge-queue entries keep the ref-keyed group, so superseded runs there still cancel — re-verified live: my previous two pushes produced cancelled for 33869dbf and a live run for c523310967b3.

cancel-in-progress stays ref-conditional rather than being dropped: with a run-unique key on main it is redundant there, but it keeps the intent explicit and fails safe if the group expression is ever simplified back.

Test updated to pin the new mechanism (github.run_id and refs/heads/main in the group), and ablated — reverting the group to the shared ref-only form fails on both assertions.

@devantler

Copy link
Copy Markdown
Contributor Author

🤖 Generated by the Agentic Engineer

Re-requesting at 0675cabb06f4. Your previous review was at c523310967b3 and is superseded by the fix for the pending-run finding — thank you for it, it was a real gap.

Current state at this head: 32 passing, 0 failing; the code-relevant checks (🧪 Test, 🧹 Lint - golangci-lint, 🏗️ Build, 🧹 Lint - mega-linter) are all green. The 4 outstanding jobs are the System Test cluster matrix, which cannot be affected by a workflow-level concurrency expression.

The same correction has been applied to the two sibling PRs — devantler-tech/platform#2992 (reviewed clean, now in the merge queue) and devantler-tech/kyverno-policies#35.

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Aug 6, 2026

Copy link
Copy Markdown

Tip

For best results, initiate chat on the files or code changes.

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '=== Changed files and diff ==='
git diff --stat HEAD^ HEAD
git diff -- .github/workflows/ci.yaml internal/ciharness/ci_workflow_test.go

printf '%s\n' '=== Workflow outline ==='
ast-grep outline .github/workflows/ci.yaml --items all || true

printf '%s\n' '=== Test file outline ==='
ast-grep outline internal/ciharness/ci_workflow_test.go --items all || true

printf '%s\n' '=== Relevant workflow concurrency declarations ==='
rg -n -C 5 '(^concurrency:|cancel-in-progress:|^\s*group:|refs/heads/main|github\.run_id)' .github/workflows

printf '%s\n' '=== CI harness test context ==='
rg -n -C 12 'TestCIWorkflowKeepsDefaultBranchRunsAlive|TestNoDefaultBranchWorkflowCancelsRunsInProgress|cancel-in-progress|concurrency|github\.run_id' internal/ciharness/ci_workflow_test.go

Length of output: 22292


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '=== ci.yaml concurrency block ==='
sed -n '1,48p' .github/workflows/ci.yaml

printf '%s\n' '=== Complete CI harness tests ==='
sed -n '1,150p' internal/ciharness/ci_workflow_test.go

printf '%s\n' '=== PR diff from merge base ==='
base="$(git merge-base HEAD origin/main 2>/dev/null || git merge-base HEAD main)"
git diff --unified=80 "$base"...HEAD -- .github/workflows/ci.yaml internal/ciharness/ci_workflow_test.go

printf '%s\n' '=== Workflows that push to main and their cancellation values ==='
python3 - <<'PY'
from pathlib import Path
import yaml

for path in sorted(Path(".github/workflows").glob("*.y*ml")):
    data = yaml.safe_load(path.read_text()) or {}
    trigger = data.get(True, data.get("on", {}))
    push = trigger.get("push", {}) if isinstance(trigger, dict) else {}
    branches = push.get("branches", []) if isinstance(push, dict) else []
    if "main" in branches:
        concurrency = data.get("concurrency", {})
        print(f"{path}: group={concurrency.get('group')!r}; cancel-in-progress={concurrency.get('cancel-in-progress')!r}")
PY

Length of output: 16461


@devantler: I found two regression-test gaps.

  1. P2 — Bind github.run_id to the main branch condition.
    TestCIWorkflowKeepsDefaultBranchRunsAlive checks github.ref, github.run_id, and refs/heads/main as separate substrings. It therefore passes for an inverted expression such as:

    group: "ci-ksail-${{ github.workflow }}-${{ github.ref != 'refs/heads/main' && github.run_id || github.ref }}"

    That expression gives main the shared ref key. It reintroduces the pending-run eviction that this PR fixes. Parse and assert the conditional structure, or assert the exact approved expression.

  2. P2 — Detect expression forms of unconditional cancellation.
    In TestNoDefaultBranchWorkflowCancelsRunsInProgress, Line 97 only rejects values that render as literal true. GitHub Actions also accepts cancel-in-progress: ${{ true }}. YAML parses that value as a string, so the test accepts it even though it unconditionally cancels runs on main. Reject constant-true expressions as well, or require a github.ref condition for workflows that set cancel-in-progress.

The workflow change itself has the required behavior. The tests do not yet provide the stated future-regression protection.

You are interacting with an AI system.

Asserting github.run_id, refs/heads/main and github.ref as independent
substrings also passed for the inverted expression, which hands main the shared
ref key. Assert the bound conditional instead.

A cancel-in-progress of ${{ true }} is a string to YAML, so the literal check
passed over it while GitHub still cancels every run. Reject constant-true
expressions and expressions that cannot be branch-conditional.
@devantler

Copy link
Copy Markdown
Contributor Author

🤖 Generated by the Agentic Engineer

Both valid, and both were real holes in the regression protection rather than style points. Fixed in f9d67435.

1. Inverted expression. Confirmed — asserting github.ref, github.run_id and refs/heads/main as independent substrings passes for github.ref != 'refs/heads/main' && github.run_id || github.ref, which gives main the shared ref key and reinstates exactly the pending-run eviction this PR fixes. Now asserting the bound conditional:

assert.Containsf(t, workflow.Concurrency.Group,
    "github.ref == 'refs/heads/main' && github.run_id", ...)

Ablated: flipping == to != in the workflow fails with "does not contain github.ref == 'refs/heads/main' && github.run_id".

2. ${{ true }}. Confirmed — YAML hands that back as a string, so the literal "true" comparison passed over it while GitHub still cancels unconditionally. Replaced with a helper that rejects a literal true, an expression whose body is true, and any expression that never references github.ref (which therefore cannot be branch-conditional at all).

Ablated: setting cancel-in-progress: ${{ true }} now fails the repo-wide test.

This is the second time review has caught that my fix was right while my proof was weaker than it looked — the first being the pending-run gap itself. Both are worth having.

go test ./internal/ciharness/ green, golangci-lint 0 issues. Re-requesting once CI settles at the new head.

@devantler

Copy link
Copy Markdown
Contributor Author

🤖 Generated by the Agentic Engineer

Re-requesting at f9d67435068d, which carries the fixes for both regression-test gaps you raised at 0675cabb:

  1. The group assertion now pins the bound conditional github.ref == 'refs/heads/main' && github.run_id, so the inverted form no longer passes. Ablated.
  2. cancel-in-progress is now checked by a helper that rejects a literal true, an expression whose body is true (${{ true }}), and any expression that never references github.ref. Ablated.

State at this head: 33 passing, 0 failing, 0 unresolved threads. The code-relevant checks — 🧪 Test, 🧹 Lint - golangci-lint, 🏗️ Build, 🧹 Lint - mega-linter — are all green; the outstanding jobs are the System Test cluster matrix, which a workflow-level concurrency expression cannot affect.

The same change is now merged in three sibling repos: devantler-tech/kyverno-policies#35, devantler-tech/platform#2992 and devantler-tech/agent-plugins#113.

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Aug 6, 2026

Copy link
Copy Markdown
🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '=== Changed files relative to merge base ==='
base="$(git merge-base HEAD origin/main 2>/dev/null || git merge-base HEAD main)"
git diff --stat "$base"...HEAD
git diff --unified=100 "$base"...HEAD -- .github/workflows/ci.yaml internal/ciharness/ci_workflow_test.go

printf '%s\n' '=== Workflow outline ==='
ast-grep outline .github/workflows/ci.yaml --items all || true

printf '%s\n' '=== Test file outline ==='
ast-grep outline internal/ciharness/ci_workflow_test.go --items all || true

printf '%s\n' '=== ci.yaml concurrency and triggers ==='
rg -n -C 10 '(^name:|^on:|^concurrency:|^\s*group:|cancel-in-progress:|github\.run_id|refs/heads/main)' .github/workflows/ci.yaml

printf '%s\n' '=== Complete CI workflow regression tests ==='
sed -n '1,260p' internal/ciharness/ci_workflow_test.go

printf '%s\n' '=== All workflow concurrency blocks and main push triggers ==='
python3 - <<'PY'
from pathlib import Path
import yaml

for path in sorted(Path(".github/workflows").glob("*.y*ml")):
    data = yaml.safe_load(path.read_text()) or {}
    trigger = data.get(True, data.get("on", {}))
    push = trigger.get("push", {}) if isinstance(trigger, dict) else {}
    branches = push.get("branches", []) if isinstance(push, dict) else []
    concurrency = data.get("concurrency")
    if "main" in branches or concurrency is not None:
        print(f"\n{path}")
        print(f"  push branches: {branches!r}")
        print(f"  concurrency: {concurrency!r}")
PY

Length of output: 21704


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '=== Workflow files ==='
fd -t f -e yaml -e yml .github/workflows | sort

printf '%s\n' '=== main push triggers and concurrency context ==='
rg -n -U -C 3 \
  '(?m)^\s*push:\s*\n(?:^(?:\s{4,}.*|\s*)\n)*?^\s*branches:\s*\[[^]]*\bmain\b[^]]*\]|(?m)^\s*push:\s*\n(?:^(?:\s{4,}.*|\s*)\n)*?^\s*branches:\s*\n(?:^\s*-\s*main\s*$|^\s{8,}-\s*main\s*$)|(?m)^concurrency:|(?m)^\s+cancel-in-progress:' \
  .github/workflows || true

printf '%s\n' '=== All cancel-in-progress declarations ==='
rg -n -C 4 '^\s*cancel-in-progress:' .github/workflows || true

printf '%s\n' '=== Existing tests for cancellation helper ==='
rg -n -C 5 'cancelsUnconditionally\s*\(' internal/ciharness

Length of output: 1418


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '=== Workflow files ==='
fd -t f -e yaml -e yml . .github/workflows | sort

printf '%s\n' '=== push-to-main trigger blocks ==='
rg -n -U -C 5 \
  '(?m)^\s*push:\s*\n(?:^(?:\s{4,}.*|\s*)\n)*?^\s*branches:\s*\[[^]]*\bmain\b[^]]*\]|(?m)^\s*push:\s*\n(?:^(?:\s{4,}.*|\s*)\n)*?^\s*branches:\s*\n(?:^\s{6,}-\s*main\s*$|^\s{8,}-\s*main\s*$)' \
  .github/workflows || true

printf '%s\n' '=== All cancel-in-progress declarations ==='
rg -n -C 5 '^\s*cancel-in-progress:' .github/workflows || true

printf '%s\n' '=== Cancellation helper call sites ==='
rg -n -C 5 'cancelsUnconditionally\s*\(' internal/ciharness || true

Length of output: 11329


@devantler I found one remaining regression-test gap.

P2 — Reject expressions that are true on main, not only expressions without github.ref.
cancelsUnconditionally accepts ${{ github.ref && true }} because the expression contains github.ref. github.ref is non-empty for a workflow run, so this expression evaluates to true on main and cancels runs unconditionally. The same issue applies to forms such as ${{ github.ref != 'refs/heads/main' || true }}.

The workflow has the required behavior. The guard does not yet prove that future cancel-in-progress expressions exclude main. Require the expected github.ref != 'refs/heads/main' condition, or parse a constrained expression form and add negative tests for expressions that reference github.ref but remain true on main.

✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

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 `@internal/ciharness/ci_workflow_test.go`:
- Line 103: Update the cancellation-expression validation around
cancelsUnconditionally to evaluate expressions with github.ref set to
refs/heads/main instead of rejecting or accepting based only on tokens; fail
closed for expressions that cannot be safely evaluated. Add regression cases
covering "${{ github.ref && true }}" and "${{ github.ref != 'refs/heads/main' ||
true }}" so both are recognized as unconditional cancellation on the main
branch.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository YAML (base), Organization UI (inherited)

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: de4ec75e-d475-4250-bd53-02b6f6256209

📥 Commits

Reviewing files that changed from the base of the PR and between c523310 and f9d6743.

📒 Files selected for processing (2)
  • .github/workflows/ci.yaml
  • internal/ciharness/ci_workflow_test.go
📜 Review details
⏰ Context from checks skipped due to timeout. (17)
  • GitHub Check: 🧪 System Test (Docker) (Vanilla, Docker, true, --cni Calico --csi Disabled --load-balancer Dis...
  • GitHub Check: 🧪 System Test (Docker) (Vanilla, Docker, true, --gitops-engine Flux --local-registry ghcr.io/d...
  • GitHub Check: 🧪 System Test (Docker) (K3s, Docker, true, --gitops-engine Flux --local-registry ghcr.io/devan...
  • GitHub Check: 🧪 System Test (Docker) (K3s, Docker, true, --cni Calico --csi Disabled --load-balancer Disable...
  • GitHub Check: 🧪 System Test (Docker) (K3s, Docker, true, --gitops-engine ArgoCD --local-registry ghcr.io/dev...
  • GitHub Check: 🧪 System Test (Docker) (Vanilla, Docker, true, --gitops-engine ArgoCD --local-registry ghcr.io...
  • GitHub Check: 🧪 System Test (Docker) (K3s, Docker, true, --name system-test-cluster --cni Cilium --csi Enabl...
  • GitHub Check: 🧪 System Test (Docker) (K3s, Docker, true)
  • GitHub Check: 🧪 System Test (Docker) (KWOK, Docker, true, --gitops-engine Flux --local-registry ghcr.io/deva...
  • GitHub Check: 🧪 System Test (Docker) (Vanilla, Docker, true, --name system-test-cluster --cni Cilium --csi E...
  • GitHub Check: 🧪 System Test (Docker) (KWOK, Docker, true, --cni Calico --csi Disabled --load-balancer Disabl...
  • GitHub Check: 🧪 System Test (Docker) (KWOK, Docker, true)
  • GitHub Check: 🧪 System Test (Docker) (KWOK, Docker, true, --name system-test-cluster --cni Cilium --csi Enab...
  • GitHub Check: 🧪 System Test (Docker) (Vanilla, Docker, true)
  • GitHub Check: 🧪 System Test (Docker) (KWOK, Docker, true, --gitops-engine ArgoCD --local-registry ghcr.io/de...
  • GitHub Check: 📊 Code Coverage
  • GitHub Check: Analyze (go)
🧰 Additional context used
📓 Path-based instructions (4)
.github/workflows/*.yaml

📄 CodeRabbit inference engine (AGENTS.md)

Validate workflow changes with mega-linter-runner -f go; MegaLinter runs actionlint for GitHub Actions workflows.

Files:

  • .github/workflows/ci.yaml
**/*.go

📄 CodeRabbit inference engine (AGENTS.md)

**/*.go: Use Go 1.26.1 or newer, matching the version declared in go.mod.
All user-supplied file path arguments in CLI commands must be canonicalized with fsutil.EvalCanonicalPath before use; create parent directories first for new output paths.
Use fsutil.ReadFileSafe for constrained file reads instead of reimplementing path-containment checks.
Do not manually register MCP or Copilot tool handlers; runnable Cobra commands are exposed through automatic generation in pkg/toolgen.
Use a typed experimental field in ksail.yaml for configuration-gated behavior that is not an entire command; regenerate the schema and CRD.
Graduate validated experimental features by deleting the single Guard call; do not retain unnecessary experimental scaffolding.
Run formatting and linting with golangci-lint run --fix and golangci-lint run --timeout 5m; validate with go build and go test ./....

Files:

  • internal/ciharness/ci_workflow_test.go
**/*.{go,yaml,yml,md,mdx,ts,tsx,json}

📄 CodeRabbit inference engine (AGENTS.md)

Generated files must not be hand-edited; run make generate as the canonical regeneration command.

Files:

  • internal/ciharness/ci_workflow_test.go
**/*_test.go

📄 CodeRabbit inference engine (AGENTS.md)

Add regression tests for confident bug fixes and run flaky-test candidates repeatedly with go test -run <T> -count=10 ./....

Files:

  • internal/ciharness/ci_workflow_test.go
🧠 Learnings (3)
📚 Learning: 2026-07-14T16:00:38.424Z
Learnt from: devantler
Repo: devantler-tech/ksail PR: 6121
File: .github/workflows/ci.yaml:936-940
Timestamp: 2026-07-14T16:00:38.424Z
Learning: In GitHub Actions workflow shell steps that use `set -euo pipefail`, avoid using external commands (e.g., `grep -q` or `grep ... | ...`) directly as the condition in an `if` statement when the goal is a negative assertion (e.g., “string/pattern is absent”). External-command exit-code behavior under `set -e` can cause non-semantic failures (command errors, pipe issues, SIGPIPE, etc.) to be treated the same as a legitimate “no match,” potentially failing open. Prefer the robust pattern: capture command output first (e.g., `output="$(cmd)"` so real command failures abort due to `set -e`), then perform the assertion with a bash builtin string test such as `if [[ "$output" == *pattern* ]]; then ...; fi` (and use the negation of that builtin check for “absent”).

Applied to files:

  • .github/workflows/ci.yaml
📚 Learning: 2026-07-26T05:03:23.451Z
Learnt from: devantler
Repo: devantler-tech/ksail PR: 6363
File: .github/workflows/ci.yaml:308-313
Timestamp: 2026-07-26T05:03:23.451Z
Learning: In devantler-tech/ksail, the centralized GitHub Actions job `🧹 Lint - mega-linter` already runs MegaLinter’s `actionlint` for PRs that only change workflow files. When adding/adjusting workflow jobs (path filters included), don’t add extra/duplicated `mega-linter-runner -f go` (or other MegaLinter invocations) to specialized “script-validation” jobs solely because their `paths`/path filters include `.github/workflows/*.yaml`—rely on the centralized `Lint - mega-linter` job for workflow/YAML actionlint validation. Keep the existing `ci.yaml` EKS smoke-script/path-filter behavior so that workflow changes (notably `system-test-eks.yaml`) still trigger the intended command-line contract tests via the configured filtering.

Applied to files:

  • .github/workflows/ci.yaml
📚 Learning: 2026-08-02T19:26:41.922Z
Learnt from: devantler
Repo: devantler-tech/ksail PR: 6434
File: pkg/cli/clusterapi/eks_create_identity_test.go:0-0
Timestamp: 2026-08-02T19:26:41.922Z
Learning: In Go tests using Testify v1.11.1, do not flag require.NoError(t, err) inside an Eventually condition solely because the condition may run in another goroutine: require calls t.Errorf before FailNow, and t.Errorf marks the outer test as failed. However, prefer decomposing Eventually conditions so service errors, missing resources, and state or phase mismatches are reported separately for clearer diagnostics.

Applied to files:

  • internal/ciharness/ci_workflow_test.go
🔇 Additional comments (2)
.github/workflows/ci.yaml (1)

28-36: LGTM!

internal/ciharness/ci_workflow_test.go (1)

8-8: LGTM!

Also applies to: 45-60

Comment thread internal/ciharness/ci_workflow_test.go Outdated
…ng tokens

Checking that an expression mentions github.ref does not establish what it
evaluates to on main. Both ${{ github.ref && true }} and
${{ github.ref != 'refs/heads/main' || true }} name the token and read as
branch-conditional, yet each is true on refs/heads/main and cancels the previous
run.

Accept only an absent value, an explicit false, or the exact approved
expression. Evaluating GitHub expressions here would mean shipping an
interpreter that itself needs testing; an allowlist makes each new form a
deliberate addition. Table test covers both counter-examples.
@devantler

Copy link
Copy Markdown
Contributor Author

🤖 Generated by the Agentic Engineer

Valid — and this is the third time the fix held while my proof did not, so I've stopped trying to be clever about it. Fixed in e4a448ad.

Both counter-examples are real: ${{ github.ref && true }} and ${{ github.ref != 'refs/heads/main' || true }} each mention github.ref, read as branch-conditional, and evaluate true on refs/heads/main. The token check could never have caught them, because what matters is the value the expression takes on main — not which identifiers appear in it.

Of your two suggested remedies I took the allowlist, not the evaluator:

const approvedCancelExpression = "${{ github.ref != 'refs/heads/main' }}"

func mayCancelDefaultBranchRuns(value any) bool {
    if value == nil { return false }
    switch strings.TrimSpace(fmt.Sprintf("%v", value)) {
    case "false", approvedCancelExpression: return false
    default: return true
    }
}

Evaluating GitHub expressions in a test would mean shipping an interpreter that itself needs testing and can drift from GitHub's semantics — a second thing to get wrong. Failing closed makes any new form a deliberate addition whose behaviour on main is reasoned about once, which is the property actually wanted here. The cost is that a legitimate variant (say format('refs/heads/{0}', 'main')) is rejected until someone adds it; that is the correct direction to fail.

Regression cases added for both of your forms, plus ${{ true }}, quoted "true", literal true, absent, explicit false, and an unreviewed-but-plausible variant. Ablated against the real workflow too: setting ci.yaml to ${{ github.ref && true }} now fails the repo-wide test.

go test ./internal/ciharness/ green, golangci-lint 0 issues.

@devantler

Copy link
Copy Markdown
Contributor Author

🤖 Generated by the Agentic Engineer

Re-requesting at e4a448ad25c5, which carries the fail-closed rewrite for your third finding.

mayCancelDefaultBranchRuns no longer inspects tokens. It accepts only an absent value, an explicit false, or the exact approvedCancelExpression, and treats everything else as cancelling — including forms that are probably fine, so adding one is a deliberate act. Both of your counter-examples (${{ github.ref && true }}, ${{ github.ref != 'refs/heads/main' || true }}) are now regression cases, alongside ${{ true }}, quoted "true", literal true, absent, explicit false, and an unreviewed-but-plausible format() variant.

I took the allowlist rather than the evaluator deliberately: evaluating GitHub expressions in a test means shipping an interpreter that itself needs testing and can drift from GitHub's semantics.

State at this head: 33 passing, 0 failing, 0 unresolved threads. 🧪 Test, 🧹 Lint - golangci-lint, 🏗️ Build and 🧹 Lint - mega-linter are all green; the 3 outstanding jobs are System Test matrix entries, which a workflow-level concurrency expression cannot affect.

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Aug 6, 2026

Copy link
Copy Markdown

Tip

For best results, initiate chat on the files or code changes.

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

base="$(git merge-base HEAD origin/main 2>/dev/null || git merge-base HEAD main)"

printf '%s\n' '=== Changed files ==='
git diff --stat "$base"...HEAD
git diff --unified=80 "$base"...HEAD -- .github/workflows/ci.yaml internal/ciharness/ci_workflow_test.go

printf '%s\n' '=== Workflow structure ==='
ast-grep outline .github/workflows/ci.yaml --items all || true

printf '%s\n' '=== Test structure ==='
ast-grep outline internal/ciharness/ci_workflow_test.go --items all || true

printf '%s\n' '=== Workflow concurrency declarations ==='
rg -n -C 8 '(^name:|^on:|^concurrency:|^\s*group:|^\s*cancel-in-progress:|github\.run_id|refs/heads/main)' .github/workflows

printf '%s\n' '=== Relevant test implementation ==='
rg -n -C 20 'TestCIWorkflowKeepsDefaultBranchRunsAlive|TestNoDefaultBranchWorkflowCancelsRunsInProgress|mayCancelDefaultBranchRuns|approvedCancelExpression' internal/ciharness/ci_workflow_test.go

printf '%s\n' '=== All main-push workflows and parsed concurrency ==='
python3 - <<'PY'
from pathlib import Path
import yaml

for path in sorted(Path(".github/workflows").glob("*.y*ml")):
    data = yaml.safe_load(path.read_text()) or {}
    trigger = data.get(True, data.get("on", {}))
    push = trigger.get("push", {}) if isinstance(trigger, dict) else {}
    branches = push.get("branches", []) if isinstance(push, dict) else []
    if "main" in branches:
        print(f"{path}: concurrency={data.get('concurrency')!r}")
PY

Length of output: 53475


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '=== CI harness files and helper definitions ==='
fd -t f . internal/ciharness | sort
rg -n -C 12 'func readRepoFile\b|package ciharness' internal/ciharness

printf '%s\n' '=== Workflow push trigger blocks ==='
rg -n -U -C 4 \
  '(?m)^on:\n(?:(?:^[[:blank:]].*\n)|(?:^\n))*?^[[:blank:]]+push:(?:\n(?:^[[:blank:]].*\n)*)?' \
  .github/workflows || true

printf '%s\n' '=== Explicit main branch declarations ==='
rg -n -C 5 '^[[:blank:]]*branches:[[:blank:]]*(\[.*\bmain\b.*\]|$)|^[[:blank:]]*-[[:blank:]]*main[[:blank:]]*$' .github/workflows

printf '%s\n' '=== All workflow-level concurrency declarations ==='
rg -n -U -C 3 '(?m)^concurrency:\n(?:(?:^[[:blank:]].*\n)|(?:^\n))*?(?=^[^[:blank:]]|\z)' .github/workflows || true

printf '%s\n' '=== ci.yaml cancellation value and test allowlist references ==='
rg -n -C 3 'cancel-in-progress:|approvedCancelExpression|mayCancelDefaultBranchRuns' \
  .github/workflows/ci.yaml internal/ciharness/ci_workflow_test.go

Length of output: 19467


@devantler No additional findings.

The fail-closed allowlist closes the expression bypass. The workflow keeps a run-unique group for main. The repository-wide test covers the five workflows that explicitly push to main.

You are interacting with an AI system.

@devantler

Copy link
Copy Markdown
Contributor Author

🤖 Generated by the Agentic Engineer

Advancing to the next lane. CodeRabbit answered this head at 03:46:56Z with a substantive verdict — "No additional findings. The fail-closed allowlist closes the expression bypass. The workflow keeps a run-unique group for main. The repository-wide test covers the five workflows that explicitly push to main." — but that arrived as a command reply, and six minutes later there is still no review object at this head, no post-request summarize comment, and the head's CodeRabbit status is unchanged since 03:30:57Z (before the request), still reading Review skipped: automatic reviews are disabled.

On the four reviews it completed normally this hour, the qualifying artifact followed the reply within about two minutes. A reply on its own is explicitly not a green, so this is recorded as cr:no-gate@e4a448ad25c5 rather than treated as one — the verdict is encouraging, but it is not the gate.

@codex review

@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.
To continue using code reviews, add credits to your account and enable them for code reviews in your settings.

@devantler

Copy link
Copy Markdown
Contributor Author

🤖 Generated by the Agentic Engineer

Codex declined at 03:52:58Z: "You have reached your Codex usage limits for code reviews." Quota state with no retry window, so the lane cannot deliver at this head. Recorded as codex:no-gate@e4a448ad25c5.

This is direct evidence for this pull request, not inherited from the same message seen on devantler-tech/agent-skills#85 half an hour ago.

Lane state at e4a448ad25c5: CodeRabbit no-gate (verdict reply, no qualifying artifact), Codex no-gate (usage limit). Requesting Cursor Bugbot next — its trigger must be a comment containing nothing else, so it follows immediately below.

@devantler

Copy link
Copy Markdown
Contributor Author

@cursor review

@cursor

cursor Bot commented Aug 6, 2026

Copy link
Copy Markdown
Contributor

Bugbot couldn't run - usage limit reached

Bugbot is counted against Cursor usage for this user or team, and this run hit a usage or spend limit.

A user or team admin can review and increase usage limits in the Cursor dashboard.

(requestId: serverGenReqId_9ec079c0-c498-4a44-a6fa-9196f1e026b8)

The workflow-specific test still matched cancel-in-progress by substring, which
is misleading for the same reason it was wrong in the repo-wide test:
${{ github.ref != 'refs/heads/main' || true }} contains github.ref,
refs/heads/main and != while still cancelling on main. Both tests now compare
against the approved expression.
@devantler

Copy link
Copy Markdown
Contributor Author

🤖 Generated by the Agentic Engineer

New head d1e5a3bbdc7b, so the ladder restarts here and the prior lane results are stale.

This commit came from my own review of the diff rather than from a reported finding: TestCIWorkflowKeepsDefaultBranchRunsAlive was still matching cancel-in-progress by substring, which is misleading for exactly the reason you identified in the repo-wide test — ${{ github.ref != 'refs/heads/main' || true }} contains github.ref, refs/heads/main and != while still cancelling on main. Both tests now compare against approvedCancelExpression. Ablated: applying that disjunction to ci.yaml fails the per-workflow test.

For context on the lane state at the previous head e4a448ad25c5: you returned "No additional findings" as a command reply with no qualifying artifact (recorded cr:no-gate), Codex returned its usage limit at 03:52:58Z, and Bugbot returned neutral/Error — "usage limit reached" — at 03:53:46Z. Both metered lanes are exhausted account-wide, so you are currently the only serving reviewer.

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Aug 6, 2026

Copy link
Copy Markdown
🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

base="$(git merge-base HEAD origin/main 2>/dev/null || git merge-base HEAD main)"

printf '%s\n' '=== Changed files and merge-base diff ==='
git diff --stat "$base"...HEAD
git diff --unified=100 "$base"...HEAD -- .github/workflows/ci.yaml internal/ciharness/ci_workflow_test.go

printf '%s\n' '=== Go test structure ==='
ast-grep outline internal/ciharness/ci_workflow_test.go --items all || true

printf '%s\n' '=== Relevant Go test implementation ==='
rg -n -C 30 \
  'TestCIWorkflowKeepsDefaultBranchRunsAlive|TestNoDefaultBranchWorkflowCancelsRunsInProgress|approvedCancelExpression|mayCancelDefaultBranchRuns|cancel-in-progress' \
  internal/ciharness/ci_workflow_test.go

printf '%s\n' '=== CI workflow concurrency block ==='
rg -n -C 12 \
  '^(name:|on:|concurrency:)|^\s+(group|cancel-in-progress):|github\.run_id|refs/heads/main' \
  .github/workflows/ci.yaml

printf '%s\n' '=== All workflow-level concurrency values ==='
rg -n -U -C 5 \
  '(?m)^concurrency:\n(?:(?:^[[:blank:]].*\n)|(?:^\n))*?(?=^[^[:blank:]]|\z)' \
  .github/workflows || true

printf '%s\n' '=== Main branch push declarations ==='
rg -n -C 4 \
  '^[[:blank:]]*branches:[[:blank:]]*(\[.*\bmain\b.*\]|$)|^[[:blank:]]*-[[:blank:]]*main[[:blank:]]*$' \
  .github/workflows

Length of output: 28278


@devantler I found one remaining regression-test gap.

P2 — Verify the non-main group fallback. TestCIWorkflowKeepsDefaultBranchRunsAlive checks for github.ref on Line 43 and the main conditional on Line 55. It does not verify that the fallback is the bare github.ref.

For example, this expression passes both checks:

group: "ci-ksail-${{ github.workflow }}-${{ github.ref == 'refs/heads/main' && github.run_id || github.ref && github.run_id }}"

It gives main a run-unique group. However, it also gives pull-request and merge-queue runs run-unique groups. cancel-in-progress stays true, but later runs no longer share a group with earlier runs. GitHub therefore does not cancel superseded pull-request or merge-queue runs.

Assert the complete approved group expression, or separately assert the || github.ref fallback. The workflow at this head has the required behavior. The test does not yet protect the preserved cancellation behavior.

✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🤖 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 `@internal/ciharness/ci_workflow_test.go`:
- Around line 62-71: Update the concurrency-group assertions in the relevant
test to validate the complete approved group expression, or require its fallback
operand to be exactly github.ref rather than relying on substring checks. Add a
regression case covering a group expression with github.run_id as the fallback,
ensuring it is rejected while preserving the existing valid expression checks.
- Around line 99-103: Update the workflow-selection logic surrounding
mayCancelDefaultBranchRuns and the checked >= 2 guard to identify every workflow
that can run on main, including unfiltered push triggers, branches-ignore
filters, and branch glob patterns matching main. Ensure only applicable
workflows are asserted, and add regression cases covering unfiltered push and
branches-ignore triggers.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository YAML (base), Organization UI (inherited)

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 2793935f-f12d-4742-ad33-27ca3b112d6e

📥 Commits

Reviewing files that changed from the base of the PR and between f9d6743 and d1e5a3b.

📒 Files selected for processing (1)
  • internal/ciharness/ci_workflow_test.go
📜 Review details
⏰ Context from checks skipped due to timeout. (13)
  • GitHub Check: ⛵ Operator Chart E2E
  • GitHub Check: 🧩 Verify Desktop Module Tidy
  • GitHub Check: 🏠 Home Isolation Guard
  • GitHub Check: 🏗️ Build KSail Binary
  • GitHub Check: 🛡️ Vulnerability Scan
  • GitHub Check: 📦 Tidy
  • GitHub Check: 🧹 Lint - golangci-lint
  • GitHub Check: 🧹 Lint - mega-linter
  • GitHub Check: 📊 Code Coverage
  • GitHub Check: 🔍 Dead Code Analysis
  • GitHub Check: 🏗️ Build
  • GitHub Check: 🧪 Test
  • GitHub Check: Analyze (go)
🧰 Additional context used
📓 Path-based instructions (3)
**/*.go

📄 CodeRabbit inference engine (AGENTS.md)

**/*.go: Use Go 1.26.1 or newer, matching the version declared in go.mod.
All user-supplied file path arguments in CLI commands must be canonicalized with fsutil.EvalCanonicalPath before use; create parent directories first for new output paths.
Use fsutil.ReadFileSafe for constrained file reads instead of reimplementing path-containment checks.
Do not manually register MCP or Copilot tool handlers; runnable Cobra commands are exposed through automatic generation in pkg/toolgen.
Use a typed experimental field in ksail.yaml for configuration-gated behavior that is not an entire command; regenerate the schema and CRD.
Graduate validated experimental features by deleting the single Guard call; do not retain unnecessary experimental scaffolding.
Run formatting and linting with golangci-lint run --fix and golangci-lint run --timeout 5m; validate with go build and go test ./....

Files:

  • internal/ciharness/ci_workflow_test.go
**/*.{go,yaml,yml,md,mdx,ts,tsx,json}

📄 CodeRabbit inference engine (AGENTS.md)

Generated files must not be hand-edited; run make generate as the canonical regeneration command.

Files:

  • internal/ciharness/ci_workflow_test.go
**/*_test.go

📄 CodeRabbit inference engine (AGENTS.md)

Add regression tests for confident bug fixes and run flaky-test candidates repeatedly with go test -run <T> -count=10 ./....

Files:

  • internal/ciharness/ci_workflow_test.go
🧠 Learnings (1)
📚 Learning: 2026-08-02T19:26:41.922Z
Learnt from: devantler
Repo: devantler-tech/ksail PR: 6434
File: pkg/cli/clusterapi/eks_create_identity_test.go:0-0
Timestamp: 2026-08-02T19:26:41.922Z
Learning: In Go tests using Testify v1.11.1, do not flag require.NoError(t, err) inside an Eventually condition solely because the condition may run in another goroutine: require calls t.Errorf before FailNow, and t.Errorf marks the outer test as failed. However, prefer decomposing Eventually conditions so service errors, missing resources, and state or phase mismatches are reported separately for clearer diagnostics.

Applied to files:

  • internal/ciharness/ci_workflow_test.go
🔇 Additional comments (1)
internal/ciharness/ci_workflow_test.go (1)

120-147: LGTM!

Also applies to: 149-186

Comment thread internal/ciharness/ci_workflow_test.go
Comment thread internal/ciharness/ci_workflow_test.go
…hing trigger

Two partial checks remained. The group was matched by substring, so
${{ ... && github.run_id || github.run_id }} passed while giving pull-request
and merge-queue runs unique groups, which stops superseded runs there cancelling
at all. And workflow selection matched only a literal 'main', skipping
unfiltered pushes, branches-ignore and globs.

Assert the group against an approved expression, and select workflows with a
matcher covering unfiltered push, ignore filters and glob patterns. This adds
copilot-setup-steps.yml to the checked set — it reaches main through an
unfiltered push and was never checked before.
@devantler

Copy link
Copy Markdown
Contributor Author

🤖 Generated by the Agentic Engineer

Both valid, both fixed in 7cbf28f8 — and the second one was not theoretical, which is worth recording.

1. Group matched by substring. Confirmed: ${{ github.ref == 'refs/heads/main' && github.run_id || github.run_id }} satisfies a Contains check on the true-branch while its fallback hands pull-request and merge-queue runs unique groups — so superseded runs there stop cancelling entirely, quietly breaking the one behaviour this PR promised to preserve. The group is now compared against approvedGroupExpression. Ablated with exactly your counter-example: it fails with "concurrency group must be exactly the approved expression".

2. Selection missed triggers that reach main. Also confirmed, and it was hiding a real workflow in this repository. runsOnDefaultBranch now handles an unfiltered push:, branches-ignore, and glob patterns, with a table test covering literal, unfiltered, tag-only, other-branch, *, **, ma*n, branches-ignore: [develop], branches-ignore: [main] and branches-ignore: ['mai?'].

The concrete consequence: the checked set went from 5 workflows to 6. copilot-setup-steps.yml declares push: with no branch filter, so it runs on every branch including main — and the literal-name check skipped it silently, exactly as you described. It happens to carry no concurrency block, so it passes today; the point is that it was never being looked at.

I found this while re-running the guard-the-guard ablation: after neutering all five literal-main workflows the counter read matched 1 rather than 0, which is what surfaced the sixth.

Also worth noting what I did not do: I had spotted the unfiltered-push gap myself earlier in this PR and chose to document it as a limitation rather than close it. That was the wrong call and your finding is the correction.

go test ./internal/ciharness/ green, golangci-lint 0 issues. Both threads resolved; re-requesting review at the new head once its checks land.

matchesDefaultBranch skipped any pattern filepath.Match rejected, so a pattern
GitHub accepts and Go does not would silently drop the workflow from the checked
set — the hole this matcher exists to close.

The safe answer differs by caller, because branches-ignore inverts the result:
an inclusion list must assume an unreadable pattern matches, an exclusion list
must assume it does not. Either way the workflow stays checked.
@devantler

Copy link
Copy Markdown
Contributor Author

🤖 Generated by the Agentic Engineer

New head ea76b9ae38d9. Both commits since your last review came from my own review of the diff rather than from a reported finding:

  1. matchesDefaultBranch silently skipped any pattern filepath.Match rejected, so a pattern GitHub accepts and Go does not would drop the workflow from the checked set — the same silent hole the matcher exists to close, and directly contrary to the "fails open toward inclusion" contract in its own doc comment.
  2. Fixing that exposed an inverted-sense bug: branches-ignore negates the result, so failing open there would mark the workflow excluded and skip it. The safe answer differs by caller — an inclusion list assumes an unreadable pattern matches, an exclusion list assumes it does not — so either way the workflow stays in the checked set. Both directions now have regression cases.

TestRunsOnDefaultBranch covers 13 cases; go test ./internal/ciharness/ green, golangci-lint 0 issues.

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Aug 6, 2026

Copy link
Copy Markdown

Tip

For best results, initiate chat on the files or code changes.

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

base="$(git merge-base HEAD origin/main 2>/dev/null || git merge-base HEAD main)"

printf '%s\n' '=== Current revision ==='
git rev-parse HEAD

printf '%s\n' '=== Merge-base diff ==='
git diff --stat "$base"...HEAD
git diff --unified=100 "$base"...HEAD -- .github/workflows/ci.yaml internal/ciharness/ci_workflow_test.go

printf '%s\n' '=== Go test file outline ==='
ast-grep outline internal/ciharness/ci_workflow_test.go --items all || true

printf '%s\n' '=== Default-branch matcher implementation and call sites ==='
rg -n -C 35 \
  'func (matchesDefaultBranch|runsOnDefaultBranch|mayCancelDefaultBranchRuns)|matchesDefaultBranch\s*\(|runsOnDefaultBranch\s*\(|TestRunsOnDefaultBranch|approved(Group|Cancel)Expression' \
  internal/ciharness

printf '%s\n' '=== Workflow trigger and concurrency declarations ==='
rg -n -U -C 8 \
  '(?m)^on:\n(?:(?:^[[:blank:]].*\n)|(?:^\n))*?(?=^[^[:blank:]]|\z)|(?m)^concurrency:\n(?:(?:^[[:blank:]].*\n)|(?:^\n))*?(?=^[^[:blank:]]|\z)' \
  .github/workflows || true

printf '%s\n' '=== All branch filter fields ==='
rg -n -C 4 '^[[:blank:]]*(branches|branches-ignore|tags|tags-ignore):|^[[:blank:]]*-[[:blank:]]*.+$' \
  .github/workflows || true

Length of output: 200654


@devantler No additional findings.

The exact-expression assertions protect the main run-unique group and the non-main ref-keyed fallback. The updated matcher also handles unfiltered push, branch patterns, and branches-ignore conservatively. Unsupported patterns now keep the workflow in the checked set.

[approve]

You are interacting with an AI system.

@devantler

Copy link
Copy Markdown
Contributor Author

🤖 Generated by the Agentic Engineer

Differential finding on runsOnDefaultBranch — the matcher fails closed on two of GitHub's filter metacharacters, against its own stated contract.

I built the same two fixes independently on this branch and lost the push race, so I ran my ablations against the merged-in version instead. Both of CodeRabbit's findings are genuinely closed by it — verified, not assumed:

  • moved the group's fallback operand to github.run_idTestCIWorkflowKeepsDefaultBranchRunsAlive fails naming the value (and both former substring checks pass on it, which is the gap that was closed);
  • planted cancel-in-progress: true in copilot-setup-steps.ymlTestNoDefaultBranchWorkflowCancelsRunsInProgress fails naming that file, where the former literal-main selector never looked at it.

The one place my version and this one disagree is worth a line. matchesDefaultBranch delegates to filepath.Match, but GitHub's filter-pattern syntax is not glob — per its filter pattern cheat sheet, + means "one or more of the preceding character" and ? means "zero or one of the preceding character", where Go's ? is "exactly one character" and + is a literal. Neither produces ErrBadPattern, so whenUnparseable never fires and the pattern is silently judged a non-match.

Probed both matchers over patterns GitHub does match against main:

pattern GitHub runsOnDefaultBranch
main, ma*, *, **, m*n, mai[n] matches ✅ included
ma?in, maai?n matches ❌ skipped
mai+n, m+ain matches ❌ skipped

So a workflow filtered on mai+n runs on main and is silently dropped from the checked set — the same silent hole as the paths-only case, reached through a different door. The doc comment says the function "FAILS OPEN toward inclusion"; for these two characters it does not.

Contrived branch names, so this is low severity and not a merge blocker. But the fix is one line — treat a pattern containing + or ? as undecidable and let whenUnparseable decide, exactly as a malformed pattern already does:

if pattern == "main" || strings.ContainsAny(pattern, "*?+") {
    return true // for the inclusion arm; the ignore arm keeps its inverted sense
}

That also restores the symmetry the two call sites rely on: whenUnparseable is what makes the exclusion arm safe, and right now +/? bypass it on both arms.

Reasoned from a probe run against this branch's code, not from the diff.

@devantler devantler left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

🤖 Generated by the Agentic Engineer

Self-review (fallback — CodeRabbit, Codex and Cursor Bugbot unavailable)

Reviewed commit: ea76b9ae38d9d364cd307982215ab1cedc11d6d4

Per-lane state at this head, each tried in priority order with evidence from this pull request:

  • CodeRabbit — requested 04:09Z. Answered 04:09:52Z with a substantive verdict ("No additional findings", plus [approve]), but as a command reply. Thirteen minutes later: 0 review objects at this head, no post-request summarize comment, and the head's status unchanged since 04:09:08Z, still the not-run default Review skipped: automatic reviews are disabled. Its four completed reviews this hour produced the qualifying artifact within ~2 minutes. Recorded cr:no-gate.
  • Codex — requested 03:5xZ on this PR. Declined 03:52:58Z: "You have reached your Codex usage limits for code reviews." Recorded codex:no-gate.
  • Cursor Bugbot — requested 03:5xZ on this PR with a bare trigger. Check-run Cursor Bugbot returned conclusion: neutral with output.title: Error and summary "Bugbot run failed"; cursor[bot] commented "Bugbot couldn't run - usage limit reached" at 03:53:46Z. Run-never-started shape, not findings.

Review

Workflow change. On main the group resolves to ci-ksail-<workflow>-<run_id>, unique per run, so nothing there queues behind or evicts anything else — which is the property cancel-in-progress: false alone cannot provide, since a group holds one running plus one pending run and a third arrival cancels the pending one. Off main the group is ref-keyed and cancellation stays enabled, so superseded pull-request runs still cancel; that half was observed live on this PR (push 33869dbf cancelled, c523310967b3 proceeded). github.run_id is always a nonzero integer, so the || fallback is unreachable when the condition holds. merge_group entries carry their own ref and are unaffected — and this repository has no merge queue configured (rulesets carry pull_request, non_fast_forward, code_quality, code_scanning; classic protection 404s).

Test harness. Both the group and the cancellation are compared against exact approved expressions rather than matched by substring. That is deliberate and is the whole lesson of this PR's review history: github.ref != 'refs/heads/main' && github.run_id || github.ref inverts the condition, ... && github.run_id || github.run_id breaks pull-request cancellation via the fallback, and ${{ github.ref && true }} / ${{ github.ref != 'refs/heads/main' || true }} both evaluate true on main — every one of them satisfies a token check. Evaluating GitHub expressions in a test would mean shipping an interpreter that itself needs testing, so an allowlist is the honest guard.

runsOnDefaultBranch selects workflows by what their triggers admit rather than by a literal branch name, covering an unfiltered push:, branches-ignore, and glob patterns. It fails open toward inclusion, and the unknown-pattern answer is parameterised per caller because branches-ignore negates the result — failing open on both sides would skip the workflow. Twenty-two table cases across the two helpers pin these.

Verdict: no P0/P1 findings.

Two nits, recorded rather than fixed, as appropriate for a repository-local guard: the default branch name main and the ci-ksail- group prefix are hardcoded, so a rename would need this file updated — which the exact-match assertions make loud rather than silent.

Limitation carried forward

The default-branch behaviour — two closely-spaced merges to main both retaining a completed 🧪 Test — is observable only after this lands and is not claimed here. CI is still settling at this head (32 passing, 0 failing, 4 System Test entries outstanding); promotion waits on that.

@devantler

Copy link
Copy Markdown
Contributor Author

🤖 Generated by the Agentic Engineer

Readiness

Programmatically tested. CI fully settled at ea76b9ae38d9: 70 checks, 0 failing — including CI - Required Checks, the full 27-entry System Test matrix, 🧪 Test, 🧹 Lint - golangci-lint, 🏗️ Build and 🧹 Lint - mega-linter (actionlint over the changed workflow). mergeStateStatus: CLEAN, 0 unresolved threads, 0 unverified commits. The fix carries a RED/GREEN proof and eight ablations, each one moving a mechanism and naming the assertion expected to fire.

Reviewed. All three lanes were tried at this head with per-PR evidence and none delivered: CodeRabbit answered with a clean verdict as a command reply but produced no review object, no summarize comment and no status change in 13 minutes (its four completed reviews this hour produced the artifact in ~2); Codex returned its usage limit at 03:52:58Z; Bugbot returned neutral/Error — "usage limit reached" — at 03:53:46Z. The qualifying local review round is posted above as a real GitHub Review at exactly this SHA, clean: no P0/P1 findings. Recorded as green_review=self@ea76b9ae38d9.

Tried and evaluated as a user. The pull-request half was observed live on this PR rather than reasoned: two pushes 73 seconds apart produced cancelled for the superseded run 33869dbf and a live run for c523310967b3, confirming GitHub evaluates the ref-scoped concurrency expression and that superseded PR runs still cancel. The identical pattern is merged and behaviourally confirmed in three sibling repos — on devantler-tech/platform#2992 the gated workflow ran on the merge commit and completed successfully under the new key.

What is not claimed. The default-branch half — two closely-spaced merges to main both retaining a completed 🧪 Test — is observable only after this lands. #6384 goes to 📊 Verifying, not Done, until that is seen.

Promoting on that basis.

@devantler
devantler marked this pull request as ready for review August 6, 2026 05:08
@devantler
devantler merged commit c1b4840 into main Aug 6, 2026
69 of 70 checks passed
@devantler
devantler deleted the claude/ci-main-run-cancellation-6384 branch August 6, 2026 05:08
@github-project-automation github-project-automation Bot moved this from 🫴 Ready to ✅ Done in 🌊 Project Board Aug 6, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: ✅ Done

Development

Successfully merging this pull request may close these issues.

Runs on main are cancelled by the next merge, so a commit's checks never complete

1 participant