Skip to main content

investigate-ci

Investigate a ClickHouse CI failure end-to-end from a PR or S3 report URL. Fetches the failed tests and their output, classifies each as flaky vs a real regression using play.clickhouse.com master history, and for every failure searches for both an existing tracking GitHub issue and an existing fix (open/merged PR) — reporting, per failure, whether an issue still needs to be created and whether a fix exists with its status (WIP, merged, already in this branch or not). Downloads and reads the harness artifacts only for failures that history does not explain, and reports a root-cause hypothesis. Read-only first pass — never commits, pushes, or edits.

Source facts

Repository
ClickHouse/ClickHouse
Last source activity
July 27, 2026 at 14:18
Detected SKILL.md language
English
Stars
49,222
Forks
8,783

Install options

The review-first prompt is selected by default. You can switch to a direct command or download a local copy.

Review the source files

Read SKILL.md and any companion files shown by SkillsMP before deciding whether to install.

File Explorer
5 files

Showing SKILL.md

SKILL.md
Source instructions · Read-only preview
name
investigate-ci
description
Investigate a ClickHouse CI failure end-to-end from a PR or S3 report URL. Fetches the failed tests and their output, classifies each as flaky vs a real regression using play.clickhouse.com master history, and for every failure searches for both an existing tracking GitHub issue and an existing fix (open/merged PR) — reporting, per failure, whether an issue still needs to be created and whether a fix exists with its status (WIP, merged, already in this branch or not). Downloads and reads the harness artifacts only for failures that history does not explain, and reports a root-cause hypothesis. Read-only first pass — never commits, pushes, or edits.
argument-hint
<PR-url | S3-report-url | issue-url> [threshold-days]
disable-model-invocation
false
allowed-tools
Bash, Read, Grep, Glob, Agent, Task, WebFetch
# Investigate CI Failure Skill A read-only first pass over a CI failure: turn a single URL into a per-test verdict (flaky vs real) plus a root-cause hypothesis, with no copy-paste and no manual `wget`. ## Arguments - `$0` (required): one of - a GitHub PR URL (`https://github.com/ClickHouse/ClickHouse/pull/NNNNN`), - a direct S3/CI report URL (`https://s3.amazonaws.com/.../json.html?PR=...&sha=...`), or - a GitHub **issue** URL (`https://github.com/ClickHouse/ClickHouse/issues/NNNNN`) — typically a bot-generated `flaky test` issue. Resolved to its report URL in step 0. - `$1` (optional): master-history window in days for the flaky-vs-real verdict. Default `14`. ## Hard rules - **Read-only.** Never `git commit`, `git push`, edit source, switch branches, close issues, or comment on the PR. This skill diagnoses; the human decides what to do. Surface findings, do not act on them. To read source at the report's commit, use `git show <sha>:<path>` rather than checking it out; only fetch/switch after asking the user (see step 4). - Use `tmp/investigate/<sha>/` for all working files, never `/tmp` (per CLAUDE.md). The `<sha>` is the first 7 characters of the report commit SHA — one subdirectory per investigation so artifacts from different PRs or branches never collide and you can return to an earlier investigation without re-downloading. - **Run every `gh` read through `.claude/tools/gh-ro.sh` (same args as `gh`).** It drops a poisoned `GH_CONFIG_DIR` — some agent/CI runners set it to a config dir with no working auth, which makes raw `gh` fail — and refuses any non-read-only subcommand, so it can never create/close/edit/merge/ comment. The examples below use it for this reason. (`fetch_ci_report.js` clears `GH_CONFIG_DIR` internally, so drive it as `node .claude/tools/fetch_ci_report.js …` directly.) - Wrap test names, identifiers, and log excerpts in backticks per the project style rule. - Say "exception", not "crash", for logical errors. ## Steps ### 0. Resolve an issue URL to a report URL `fetch_ci_report.js` accepts only PR, S3 `json.html`, and direct `result_*.json` URLs — **not** issue links. If `$0` is `.../issues/NNNNN`, read the issue body and extract the report URL first: ```bash .claude/tools/gh-ro.sh issue view <NNNNN> --repo ClickHouse/ClickHouse --json title,body ``` Read the issue from the command output — do **not** redirect to a file. A `.claude/tools/gh-ro.sh issue view … > tmp/investigate/$SHA/issue.json` redirect is a file write that rides the wildcard `Bash(.claude/tools/gh-ro.sh:*)` allow (not the hook), so a symlinked `tmp`/`tmp/investigate` could land it outside the scratch dir without a prompt. (Step 1 creates `tmp/investigate/$SHA`.) Bot-generated `flaky test` issues use this body format: ``` Test name: <test_name> Failure reason: <reason> CI report: <S3 json.html url> ← use this as the report URL for the steps below Failing test history: <play.clickhouse.com link> ``` Extract the `CI report:` URL and use it as `$0` for the rest of the skill. Also keep the `Test name:` value — for a flaky-test issue the test to classify is already named, so step 3 can run even if the S3 artifacts have expired (CI report links are retained only for a limited time; if the report 404s, fall back to the named test plus the issue's `Failing test history` link). If the issue is **not** a flaky-test report (no `CI report:` line), ask the user for the report or PR URL rather than guessing. **When the input is a flaky-test issue, step 3 is the decisive signal — run it first.** The issue already names the test and the bot only files it after a failure, so master history settles flaky-vs-real directly. Treat step 1 (the S3 report) and the optional step-4 artifact download as best-effort enrichment: bot-filed issues are often weeks old and their artifacts have already expired, so do not block on them. ### 1. Set up and fetch the failed tests `fetch_ci_report.js` needs `node` on `PATH`. Run these as **separate** commands (not one compound block) so each matches an allowed shape under the investigate profile — a combined `mkdir … ; if … node …` string matches neither the exact `mkdir` allow nor the node-fetch hook and would prompt. Primary inputs (PR/S3) skip step 0, so create the parent working dir first: ```bash mkdir -p tmp/investigate ``` Probe for `node` (`command -v` is allowed; do not wrap the fetch in an `if`/`;` one-liner, which would hide the `node` command from the hook): ```bash command -v node ``` If `node` is present, fetch the failed tests and their output: ```bash node .claude/tools/fetch_ci_report.js "$0" --failed --cidb 2>&1 ``` For a **single HTML report URL** (including `sha=latest`, which resolves to the actual build commit) the tool prints a `SHA: <40-hex-sha>` line — read it and set `$SHA` to the first 7 characters. For a **PR URL** the tool prints a multi-report summary without a `SHA:` line; extract `$SHA` from the `?sha=<hex>` query-string parameter in any `🔗 Report:` URL printed in the summary, or re-run with `--report N` on the relevant job to get single-report output that does print `SHA:`. For a **direct `result_*.json` S3 URL** (e.g. `https://s3.amazonaws.com/clickhouse-test-reports/PRs/111528/<sha>/result_fast_test_arm_darwin.json`) the tool also prints a `SHA:` line extracted from the URL path. Either way, `$SHA` is always a concrete commit hash, never a PR-number fallback. Then create the working directory: ```bash mkdir -p tmp/investigate/$SHA ``` This prints the failed tests **and their output** straight from the praktika `result_*.json` (no copy-paste), with a CIDB link per failed test. Read it from the command output — do **not** add a `> tmp/investigate/$SHA/…` redirect (a redirect is a file write the hook won't auto-approve, since it can't be made symlink-safe, so it would prompt); the harness persists large output to a file you can re-read or `grep`. **If `node` is absent, the fallback depends on the input type:** - **Issue URL** (step 0 already gave you `Test name:` and the `Failing test history` link) → proceed without the report: run step 3 on that named test; the S3 report and step-4 artifacts are best-effort enrichment. A missing `node` is not fatal here. - **PR or S3 report URL** → `node` is **required**. Without the report you have no failed test names, job names, or labels, so steps 2–3 (issue/fix search and the `test_name IN (...)` history query) cannot run. Do **not** limp on with a partial investigation — stop and tell the user to install `node` (or re-run where `node` is on `PATH`). Per failure the tool prints a `🏷️ labels:` line (CI's non-CIDB labels — the `issue` match link and flags like `retry_ok`; see step 2a), the CIDB link, and the **failure reason section** extracted from `result.info` as follows: the bash debug-trace section (`.debuglog:` path header or `+ [timestamp]` xtrace lines) is stripped entirely as pure noise; then from what remains, up to 40 lines are shown as **head + tail** (first 20, `--- (N lines omitted) ---`, last 20), so the `Reason: ...` at the top of stateless-test output and the `ninja: build stopped` / compiler errors at the bottom of build logs are both visible. If the meaningful section is ≤ 40 lines, it appears in full. CI's matchers test `Failure reason` against the **whole** `result.info`; when the full output matters (e.g. an issue with a `Failure reason:` field deep in the trace), drill via the CIDB link or the full artifact (step 4). - If `$0` is a PR URL with many reports and the noise is high, narrow with `--report <n>` after listing reports (run the tool with no `--failed` to see the index). - Record the PR number and the exact failed **test names** as they appear — the names must match `checks.test_name` for step 3 (and feed the issue search in step 2). - **Job-level failures** are printed as `⚙️ JOB: <job name>` (instead of `❌ FAIL:`). These are synthetic entries — the job name is **not** a real `checks.test_name` value. Skip the `checks.test_name` history lookup in step 3 for them; go directly to step 4 (artifact download) to find the root cause from logs and harness output. - Record the **count** of failed tests. The cheap steps (2–3) always run over all of them, but a large count changes how step 3 scopes the expensive deep-dive — see "Scope the deep-dive". ### 2. Search for an existing tracking issue and an existing fix For **every** failed test, run two searches and record a per-test answer to two questions that go into the final report: - **Issue:** is the failure already tracked, or does an issue still need to be created? - **Fix:** does a fix already exist, and what is its status (WIP / merged / already in this branch or not)? Do both before the deep-dive — a tracked failure with a merged fix often short-circuits the rest of the investigation. #### 2a. Existing tracking issue + "does an issue need to be created?" Search issues (open **and** closed) by test name — a hit often names the tracking flaky-test issue, and its comments may already carry the root cause, a fix PR, or a "known flaky" note. ```bash .claude/tools/gh-ro.sh issue list --repo ClickHouse/ClickHouse --state all --limit 10 \ --search "<distinctive test-name fragment> in:title,body" \ --json number,title,state,stateReason,url,labels,closedAt ``` Search on a distinctive fragment (the function or `test_*` name **without** the parametrization suffix), not the full parametrized string — GitHub search tokenizes on punctuation and the full name rarely matches. If a candidate looks relevant, read it **with its comments** — they often already provide the diagnosis: ```bash .claude/tools/gh-ro.sh issue view <NNNNN> --repo ClickHouse/ClickHouse \ --json number,title,state,stateReason,body,comments,labels,closedAt ``` **How CI decides a failure is already tracked** (so you can answer "needs an issue?" the same way CI's matcher does — see `ci/praktika/issue.py`, `Issue.check_result`): CI builds a catalog from issues labeled **`testing`** (`IssueLabels.CI_ISSUE`) that are **open, or were closed within the last ~8 hours**, and routes each by whether it also carries the **`infrastructure`** label: - **Flaky-test issues** (no `infrastructure` label) → `Issue._check_flaky_test_match`. Matches when the issue's `Test name:` body field is a **suffix** of the failing test's name (`result.name.endswith(test_name)`; pytest parametrization/module rules apply), **and** if the issue sets a `Failure reason:`, that text is a **substring** of the failure output. - **Infrastructure issues** (`testing` **+** `infrastructure` label) → `Issue._check_infrastructure_match`. These do **not** match by `Test name:`. They match a failure when **all** of the present fields hold: `Failure reason:` is a substring of the output; every `Failure flags:` value (e.g. `retry_ok`) is a label on the result; `Test pattern:` matches the test name; and `Job pattern:` matches the **job** name. Note the pattern matching is **not** true SQL `LIKE`: the pattern is split on `%`, empty fragments are dropped, and it matches if **any** remaining fragment is a plain substring (so it is OR-across-fragments, order is not enforced, and a bare `%` — like an empty field — is treated as no constraint). Examples: [#87123](https://github.com/ClickHouse/ClickHouse/issues/87123) (`Job pattern: Unit%`), [#91410](https://github.com/ClickHouse/ClickHouse/issues/91410), [#92089](https://github.com/ClickHouse/ClickHouse/issues/92089) (`Job pattern: Stateless tests (amd_msan%`, `Failure reason: DB::Exception: Timeout exceeded`). **Fast path — read the labels `fetch_ci_report.js` prints.** The tool surfaces each failure's non-CIDB labels on a `🏷️ labels:` line. Two are decisive: - An **`issue`** label gives you the matched issue number **for free** — the printed link is the issue CI matched **at run time** (e.g. `Server died` → `issue (…/issues/107487)`, `Hung check …` → `…/107941`). It saves the *search*, but it is **not** by itself `tracked #N`: the label reflects the catalog when the report was produced, not the issue's state **now**. Always `.claude/tools/gh-ro.sh issue view` the linked issue and classify from its **current** `state`/`closedAt` — an issue closed after the run and aged past the ~8 h window is `stale #N` (reopen candidate), not `tracked`. The label shortcuts the lookup; it does not replace the tracked-vs-stale decision below. - **Failure flags** (e.g. `retry_ok`) appear here too — these are exactly the labels an infrastructure issue matches on via `Failure flags:` (below), so this line is how you verify that constraint. And **absence** of an `issue` label is **not** proof of "untracked": CI stamps it from the catalog *as it was at that run* (open + closed-within-8h then), so a tracking issue filed or reopened **after** the run won't show. So: an `issue` label → look up that issue and classify by current state; no label → still run the GitHub search below before concluding `needs issue`/`untracked`. **For an `INFRA/BUILD` or timeout/harness-level failure, also run the infrastructure path.** A test-name search alone will miss these, so a pre-existing, already-tracked infra failure would be mis-reported as `needs issue`. List the infra issues — **`--state all`**, because CI's catalog includes not just open ones but every `testing` issue closed in the last ~8 h (`TestCaseIssueCatalog.from_gh`), and a just-closed infra issue is still auto-matched — and match by `Job pattern` / `Failure reason` against the failing job and its output: ```bash .claude/tools/gh-ro.sh issue list --repo ClickHouse/ClickHouse --state all --label testing --label infrastructure \ --limit 100 --json number,title,url,body,state,closedAt ``` Compare each candidate's fields the way `_check_infrastructure_match` does: `Failure reason:` is a substring of the output; `Job pattern:`/`Test pattern:` match the failing job/test name; and every `Failure flags:` value must appear on the failure's `🏷️ labels:` line from step 1 (that line is the only place these flags are visible — e.g. `test_dns_cache … → retry_ok`). All present fields must hold. A match on an open issue — or one closed within ~8 h (check `closedAt`) — is `tracked #N` exactly as CI would attribute it; a match on an issue closed longer ago is a `stale #N` reopen candidate (same same-failure/recurrence check as the flaky case). **Always run the issue search for every failed name** — it is one cheap `gh issue list` and is the only way to mirror CI's attribution. Generic, harness-level names get tracked too: `Server died`, `Hung check failed, possible deadlock found`, and the upgrade `Error message in clickhouse-server.log` check frequently *do* have a `testing` issue (e.g. `Server died` → [#107487](https://github.com/ClickHouse/ClickHouse/issues/107487), `Test name: Server died`), and `Issue._check_flaky_test_match` will mark such a result with the `issue` label. So do **not** skip the search for them. The `generic failure / untracked` value is **only** for a generic bucket or anonymized error *class* (e.g. `Logical error: Bad cast from type A to B`, where the harness replaces concrete types with `A`/`B` and groups by stack hash `STID`) **after** the search finds no matching `testing` issue — because filing a *new* per-failure issue for such a shifting bucket makes no sense. It is a "searched, nothing matched, and not worth filing" verdict, never a "didn't look" one. Determine, per test: - **Tracked** — a `testing` issue matches (by the rule above, or reached via the report's `issue` label) and is **currently** open or closed within ~8 h (`.claude/tools/gh-ro.sh issue view` → `state`/`closedAt`). No new issue needed; CI will keep auto-matching it. Applies to generic-bucket names too when such an issue exists (e.g. `Server died` → #107487). If the linked/ matched issue is closed longer ago, it is `stale #N`, not `tracked`. - **Needs an issue** — the failure is a pre-existing **FLAKY** or **INFRA/BUILD** problem (per
View on GitHub
This SKILL.md is very large, so SkillsMP previews the first section here. View on GitHub