Use when CI is red on neilotoole/sq and you intend to act on it: an automated autofix pass, a nightly or dispatch failure, or a failing check on a pull request. Carries the triage order, what counts as proof that a fix works, and the rules that keep an attempted fix from landing as a duplicate or unmergeable PR.
68
84%
Does it follow best practices?
Run evals on this skill
Adds up to 20 points to the overall score
View guide
Low
Low-risk findings worth noting
Triage procedure for a red CI run on sq. Policy lives in
AGENTS.md, especially Flaky tests and
Git branch naming. How CI is wired is in
docs/CI.md. This skill is the operational layer: what to check, in what
order, and what to do with the answer.
It exists because of a measured failure mode. Between 2026-06-21 and 2026-09-16, thirteen
autofix PRs were opened against this repo and none merged.
references/case-file.md records each one and why it was closed. Four
causes account for all thirteen: the fix was a t.Skip on a flake with a real root cause, the
flake was already tracked or already being fixed, the diff carried work that belonged to another
branch, or the PR's own green checks never ran the job that had failed.
Two rules follow from that, and they outrank convenience:
Do these in order. Every step before "Classify" is a possible stop, and stopping early is cheap.
| Step | Question |
|---|---|
| 1. Symptom | What exactly failed, in which job, with what error? |
| 2. Prior art | Does someone already own this? |
| 3. Provenance | Which commit and branch was under test? |
| 4. Classify | Regression, flake, or infrastructure? |
| 5. Proof | Can you reproduce it and demonstrate the fix? |
| 6. Publish | Issue, comment, or PR, and against which base? |
| 7. Clean up | Does anything of yours need closing or deleting? |
Read the log. Not the check name, the log.
gh run view <run-id> # job list and conclusions
gh run view --job <job-id> --log-failed # the failing step's outputWrite down three things before going further:
test-windows-full, test-nix (macos-15),
run / test oracle:latest).The process cannot access the file because it is being used by another process is a Windows sharing violation, whatever test it lands on.One symptom per pass. If several tests failed, group them by verbatim error first: failures that share an error usually share a mechanism, and they are one symptom, not several. #1197 and #1200 were two tests failing from one DuckDB fixture lock. Take one group and leave the rest. Multiple symptoms in one PR cannot be reviewed or reverted independently.
Search before you think about a fix. Every recent flake on this repo already had an issue, an open PR, or a prior attempt by the same automation, and the search that finds it takes seconds.
gh search issues --repo neilotoole/sq --include-prs --limit 20 'TestStartMemStatsTracker'
gh search issues --repo neilotoole/sq --include-prs --limit 20 'cmd_sql_test.go'
gh search issues --repo neilotoole/sq --include-prs --limit 20 'sakila.duckdb'
gh pr list --repo neilotoole/sq --state open --limit 40 \
--json number,title,headRefName,filesSearch by identifier, not by prose, and run more than one search. Error text does not match: a
search for sakila.duckdb sharing violation returns nothing. Identifiers do, but which one hits
varies. At the moment PR #1200 was opened, its own test name returned nothing that owned the
failure, while sakila.duckdb and cmd_sql_test.go each returned the owning issue #1198 and the
merged fix #1199. Search the test name, the failing file's path, the fixture filename, and the
symbol named in the error.
Check all four:
testh/, testh/tu/) rather than only the failing test. A harness fix in flight will resolve
failures in tests that look unrelated to it.If any of those own the symptom, stop. Post a comment only if you carry a genuinely new data point, such as a run link for an occurrence on a platform not yet listed, and never repeat what the thread already says.
Count your own open PRs before continuing. Three or more open means stop and do Clean up instead: on 2026-09-15 seven autofix PRs were open at once, and all seven were closed unmerged.
Establish what was actually tested, because it decides where a fix can go.
gh run view <run-id> --json headBranch,headSha,event,createdAtmaster or a nightly or dispatch run of master. The base for any fix is
master.master.
That was the single largest source of unmergeable diffs here: PR #1197 was 517 added lines
against master when the change it intended was one line, because it branched from the head of
another open PR and carried that PR's work along with it.For a master failure, branch from current origin/master once the failure reproduces there,
never from another feature branch:
git fetch origin master && git checkout -b fix/gh<issue>-<short-desc> origin/masterFor a failure on your own PR, work on that PR's branch; there is no new branch to create.
| Evidence | Outcome |
|---|---|
| Fails every time on the tested commit, passes on its parent | Regression. Fix it on the base Provenance gives. |
| Intermittent, and you can state the mechanism and show it (what raced, what held the lock, what was nondeterministic) | Fix the mechanism. |
| Intermittent, mechanism not established | No code PR. File an issue, or add a run link to the existing one. |
| Runner outage, registry 5xx, expired credential, dead external host, container image unavailable | No code PR. Report, and say which of those it is. |
| Already owned per Prior art | Stop. |
"Intermittent with the mechanism established" is a narrow category. It means an explanation that predicts the observed pattern: why this OS, why this pair of tests, why now. If your explanation does not predict which runs fail, it is a guess, and a guess belongs in an issue.
These are not fixes, and a PR containing one will be closed:
t.Skip, t.Skipf, tu.SkipWindows, tu.SkipIf or any conditional skip added because a test
sometimes fails.Gating on a real precondition is different, and these are correct because each one states what the test requires:
| Gate | States |
|---|---|
tu.SkipShort(t, true) | Needs a live database or is long-running. |
tu.SkipNoNetwork(t) | Deliberately uses a real remote host. |
tu.SkipReadOnlyFileUnenforceable | The guard is a no-op on this platform. |
Driver SQ_TEST_SRC__* checks | Needs that engine's container. |
tu.SkipIssue and tu.SkipIssueWindows do exist for a tracked, accepted skip. They are a
maintainer's call, not yours. Propose one on the issue and wait; do not open the PR.
Reproduce before fixing, and demonstrate after. If you cannot do the first, you are reporting, not fixing.
go test ./path/to/pkg -run '^TestName$' -count=1 -v
go test ./path/to/pkg -run '^TestName$' -count=20 # for an intermittent failure
go test ./... -short # the PR dev loopA pipe hides the exit code: go test ./... | tail reports success on a failing suite. Capture the
status first. Server-backed driver tests skip silently without their SQ_TEST_SRC__* envar, so
"passes locally" can mean "never ran".
A pull request runs the fast loop only: lint, test-nix with -short on Linux and macOS,
test-windows-smoke, format, dependency review, plus the touched driver's bookend legs. These do
not run on a PR:
test-windows-full, the full Windows suite, which is where most of this repo's flakes appear.Coverage, CodeQL, and the nightly live-network test (SQ_TEST_NETWORK, gh #1158).PRs #1197 and #1200 both showed every check green while test-windows-full, the job that had
failed, was skipped. Neither change was verified by anything.
To run the full suite on your branch, dispatch Main Pipeline on it. workflow_dispatch sets
FULL_RUN and enables test-windows-full:
gh workflow run main.yml --ref <your-branch>
gh run list --workflow main.yml --branch <your-branch> --limit 5Link each completed run. For an intermittent failure, dispatch at least three times, five if the
failure rate looked below half, and link them all. Wait for each run to finish before dispatching
the next: Main Pipeline cancels in-progress runs on non-master branches.
Main Pipeline dispatch only proves failures in Main Pipeline. Two others need a different run:
A DB matrix leg (run / test <engine>:<tag>). Dispatch DB integration with the failing
engine and tag. Each engine input takes tags or latest, oldest, bookends or all. For a
change that could affect every engine, dispatch db-scheduled.yml with -f mode=bookends.
gh workflow run db-integration.yml --ref <your-branch> -f oracle=latestThe live-network test. SQ_TEST_NETWORK is set on the nightly schedule only, not on
dispatch, so no run you can start will exercise it. A fix for it cannot be proven before merge.
File an issue with the proposed fix rather than opening a PR.
Dispatch needs actions: write. If your token does not have it, or the workflow refuses, the fix is
unproven: say so in your report, and prefer an issue with a proposed fix over a PR whose only
evidence is a check set that never ran the failing job.
Last check before you believe your own fix: would the failing step have reached the line you
changed? PR #1197 skipped a test that Windows CI already skipped, because no
SQ_TEST_SRC__SAKILA_OR is set there. The failure was in the fixture copy that ran before it. The
change could not have mattered.
This is the good outcome for a flake you cannot yet explain, and it is worth more than a skip.
#1198 is the model: the verbatim error, a "Seen
in" list of runs with dates and test names, the mechanism as far as it is known, a proposal, and
acceptance criteria. Label it ci, add windows when it is Windows-only. Say plainly what is not
yet known.
Every one of these is a hard requirement.
Base is the branch that failed, per Provenance.
Diff contains nothing but the fix. Check it and read the list:
git diff --stat origin/<base>...HEADIf a file you did not deliberately change appears, your base is wrong. Fix the base. Do not ship it with an explanation.
Size is proportionate. A CI fix is small. More than about 50 non-test lines, more than five
files, or any change to go.sum, a lockfile, or site/ content that the failure did not
implicate means stop and report instead. PR #1073 rewrote 2,465 lines of site/bun.lock to fix
a broken documentation link.
No drive-by anything. No dependency bumps, no reformatting, no unrelated doc edits. A stale
netlify-cli bump swept into PR #1110 was still being cited when it was closed a month later.
Branch name follows AGENTS.md: fix/ for a bug
fix, chore/ for CI and tooling, plus gh<ISSUE>- when an issue exists. Not feature/ for a
fix, and not an opaque automation suffix.
CHANGELOG.md gets an entry under ## Unreleased for a user-visible fix. A test-only or
CI-only change does not.
make fmt && make lint before pushing, always.
PR body states the failing run link, the mechanism, why this fix addresses that mechanism, and the dispatch runs that demonstrate it. If a related issue exists, link it rather than restating it.
Re-check prior art immediately before opening. Minutes matter here: PR #1145 was opened ten minutes after the PR that superseded it, while that PR was still open, and that PR merged nine minutes later.
Leftover branches and stale PRs are part of the cost being measured. Seven orphaned branches from this automation had to be deleted by hand.
git push origin --delete <branch>.469b92b
If you maintain this skill, you can claim it as your own. Once claimed, you can manage eval scenarios, bundle related skills, attach documentation or rules, and ensure cross-agent compatibility.