Debugging PostHog CI failures
Before you propose a change to CI, check things already tried for the idea. It records what was measured, and why some good-sounding changes were reverted or rejected.
Find the first meaningful failure, classify it, reproduce the smallest useful
case locally when appropriate, and report the result. Avoid public-visible or
irreversible actions unless the user explicitly asks.
Always start with the hogli ci:insights digest. It aggregates across runs and
branches, which gh cannot do cheaply, and tells you whether a failure is
likely trunk-borne, gate-only, or isolated to a small set of branches. gh is
authoritative for one run's current state and attribution. Use the digest to
decide what to inspect; use gh to confirm whose failure it is and exactly
what failed in a given run.
This skill triages and classifies. Once a failure is confirmed flaky, hand off
to the fixing-flaky-tests skill, which owns local reproduction, root-cause
fixing, and N-run validation. For "who broke master" — the culprit commit and
the commit that fixed it — hand off to the investigating-ci-failures skill,
which owns the green/red boundary analysis. For aggregate pipeline health (is CI
getting slower, which workflow is the long pole, how long PRs take to merge),
read diagnosing-ci-and-merge-bottlenecks. Both are product skills under
products/engineering_analytics/skills/, not invocable here: read the
SKILL.md at that path.
Rule out a platform outage first
GitHub Actions goes down often enough that it belongs before any log reading, and
a platform incident makes every other signal a symptom. It costs two page loads:
https://www.githubstatus.com/ for GitHub, https://status.depot.dev/ for the
runners. Check them whenever failures are broad — several workflows at once, a
burst of runs failing together, jobs dying before Checkout, or anything red
across unrelated PRs.
Report an outage as an outage, name the component, and stop recommending reruns.
Safety rules
Do not do any of these without explicit approval in the current conversation:
- Rerun or cancel a GitHub Actions run.
- Post a GitHub comment, PR review, or issue comment through any CLI, MCP, or
API tool.
- Push commits, force-push, rename branches, or delete branches.
- Edit
.github/workflows/ files (CI infra changes need human review).
- Merge, close, or reopen the PR.
- Accept or update snapshots.
Read-only gh calls and read-only GitHub tools are fine. If you need to
change local Git state, make sure it is necessary for the task and does not
overwrite unrelated work.
Workflow
1. Start with CI insights (always first)
hogli ci:insights reads PostHog's own engineering analytics — the cross-run
failure history a single run can't show. Consult it before any raw gh log
archaeology.
hogli ci:insights # digest for the current repo + branch
hogli ci:insights search "<error or test name>" # match a specific failure
hogli ci:insights view <ref> # one failure in full
hogli ci:insights view <ref> --logs # ...plus the failing log lines
- Broad question ("is CI red?", "is master green today?", "what's broken right
now?"): the no-arg digest answers directly. It gives the default-branch verdict
(how many workflows are failing on their latest run, and which), the live
failures grouped by state, the jobs red on trunk right now, a grouped feed of
default-branch failures, and the PR your branch belongs to. You often do not
need a target PR or run at all; report from the digest.
- Specific failure: run
search "<error>" to match it before reading logs, then
view <ref> on a row the digest or search printed. --logs prints the thinned
failing lines from that failure's latest run, which is usually enough to
classify without touching gh.
Read each row's state as a triage ranking:
| State |
Means |
breaking_master |
failing on the default branch and that job's latest run is still red |
blocking_merge_queue |
failed only on merge-queue gate branches in the window |
novel_burst |
new within a day, already spreading across branches, not on trunk yet |
potentially_resolved |
hit trunk but that job's latest run is green again |
flaky |
sporadic across two or more branches over more than a day |
pr_only |
limited branch spread; job status may be missing or behind the failure lines |
potentially_resolved is a hint, not a conclusion: confirm from run data before
reporting a failure as already fixed.
blocking_merge_queue proves a gate failure happened in the window, not that it
still blocks landings. Check the current queue run with gh before reporting it
as active. Likewise, confirm pr_only from the current run before assigning the
failure to a PR; it is also the fallback when job status is missing or stale.
Caveats to carry into whatever you report:
- Failure grouping is pytest-only. Jest, Playwright, and cargo failures appear
only in the digest's grouped master-failures section, never as a row with a ref.
- Every count is absolute, never a rate. Passing runs are not in the test-level
data, so there is no denominator to quote. Job conclusions do record greens;
the base-rate section below is how to get a real rate from them.
- A run's conclusion can lag until GitHub's
workflow_run webhook settles it, so
during a live incident confirm a specific run against gh.
If nobody has signed in on this machine, hogli ci:insights exits 78. Treat
exit 78 as "no CI insights available" and fall back to the gh-based
inspection below — then tell the user they can run hogli posthog:login
once, which opens a browser and needs no API key. Do not run it yourself: it
waits on a consent screen you cannot see. Surface what you find per the Safety
rules — do not auto-apply a fix.
2. Find the failing run (for a specific failure)
Determine the target in this order:
- If the user gave a PR number, run ID, check name, or branch, use it.
- Otherwise, infer from the current branch with
gh pr view --json number,headRefName,statusCheckRollup.
- If neither works, ask the user for a PR URL or run ID. Do not guess.
A PR kicked from the merge queue is the exception: its own checks are the
wrong target. Trunk tests each queued PR on a trunk-merge/pr-<n>/<uuid>
branch holding master plus every PR queued ahead of it whose impacted targets
overlap its own. The lane script over-reports targets on purpose, so in
practice that is most of the queue. So:
- The failing run is on that branch, never on the PR's head SHA. Take it from
trunk merge status <n> (/merging-prs step 4), not gh pr checks. Trunk publishes no check run here.
The branch is ephemeral; the run and its logs stay on GitHub, and the
warehouse keeps its jobs under that head_branch (query 8 in the
investigating-ci-failures references).
- The PR's own checks can be green with the failing job skipped or
narrowed. On the PR, path filters see only that diff and the Django suite runs
a selected subset; on the queue branch the diff is every carried PR's and the
full matrices always run. A docs-only PR can be kicked by a job its own CI
never ran.
- The branch names one PR but carries many. A failure on it is not evidence
against that PR until you find the change that caused it; the branch's other
merge commits are the first suspects.
- In the digest this is the
blocking_merge_queue state.
Inspect read-only:
gh pr checks <pr>
gh pr view <pr> --json statusCheckRollup
gh run view <run-id> --json jobs,conclusion,name,workflowName,url
gh run view <run-id> --log-failed
Use the full job log only when --log-failed lacks the failing command or
enough surrounding output:
gh run view <run-id> --log --job <job-id>
Given a run id, the engineering-analytics-run-failure-logs MCP tool returns
every failed job's error region with original line numbers, already thinned.
One call instead of a jobs listing plus a log download, and it works when the
job died before any test ran. It is bounded by Logs retention, so fall back to
gh for older runs. Given a PR number instead of a run id,
engineering-analytics-ci-failure-logs does the same across every run that PR
has pushed, so an earlier push's failure is still there.
Extract these before classifying:
- Workflow name or file, e.g.
.github/workflows/ci-backend.yml.
- Job name, e.g.
backend-tests (4/10).
- Step name, e.g.
Run pytest.
- Failing command and the smallest useful output excerpt.
When scanning logs, search for FAIL, Error, error:, assert,
Traceback, exit code, and ##[error]. Stop at the first failing step that
explains the run's conclusion. Keep excerpts under 40 lines.
For test-job failures, the trunk MCP server's investigate-ci-failure tool
is a shortcut past log scanning: give it the run URL
(https://github.com/PostHog/posthog/actions/runs/<run-id>) and it returns
structured failing-test details — names, error messages, stdout/stderr — from
the results CI uploaded to Trunk Flaky Tests, with quarantined known-flaky
tests already filtered out. It only covers what ran and uploaded: for jobs
that died before tests (build, setup, lint), use
engineering-analytics-run-failure-logs or gh run view --log.
Authenticate once via /mcp → trunk (browser OAuth); headless environments
instead add an Authorization: Bearer header with a TRUNK_API_TOKEN org
token to the server entry in .mcp.json. To dig into one test's flakiness
history, hand off to fixing-flaky-tests, which covers the search-test and
fix-flaky-test tools.
Classification
| Signal in the log |
Class |
First action |
AssertionError, test diff, FAILED test_... in a committed test file |
code regression |
reproduce with hogli test <path>::<test> |
Test failed here, passed on master or on rerun in the same PR |
flaky test |
confirm against master history; to fix, use fixing-flaky-tests |
ruff, oxlint, stylelint, markdownlint, prettier errors |
lint |
hogli lint:python:fix or hogli format on touched files |
mypy, pyright, tsc, typescript:check errors |
typecheck |
run the same checker locally, not the full suite |
| Chromatic / Storybook / Playwright visual diff, snapshot mismatch |
snapshot / visual |
surface the diff URL; do NOT auto-accept snapshots |
manage.py migrate error, migrations:check failure, missing migration |
migration / schema |
hogli migrations:check locally |
| OpenAPI schema diff, generated API types out of sync |
codegen drift |
hogli build:openapi |
Cannot connect, ECONNREFUSED, address already in use, OOM, runner killed, setup step timeout |
infra / runner |
get the base rate before calling it transient (below) |
startup_failure conclusion, a job with zero recorded steps, or a log blob that 404s |
infra / runner |
no log to read; check https://www.githubstatus.com/ and the runs around it |
apt-get, uv sync, pnpm install, docker pull, setup action failures |
environment / setup |
diff .nvmrc, pyproject.toml, package.json, Dockerfiles |
hogli lint:skills, hogli build:skills failure |
skills build |
run the same hogli command locally |
SDK compat check, ci-survey-sdk-check, cross-version failure |
SDK compatibility |
check SDK version matrix for the affected package |
If multiple signals match, choose the most specific class. For example, prefer
codegen drift over lint, migration over typecheck, and snapshot / visual over a
generic Playwright test failure.
Base rate for infra and setup failures
"Transient" is a claim about how often the job fails, so do not assert it from
one run. A job that dies before its tests run leaves no test-level evidence: no
FAILED line, so no fingerprint and no span, so it never appears as a row in
broken_tests or in the flaky-tests tool. It is still visible as a job
conclusion, which is what the digest's master-failures section groups by, and
engineering-analytics-run-failure-logs still returns its failing lines because
it reads by run, not by test. Start there, not from a test.
Unlike the span-derived test reads, this one can give you a real rate: the
warehouse records every job attempt, greens included, so the denominator is
honest. Query 7 in
products/engineering_analytics/skills/investigating-ci-failures/references/investigation-queries.md
is copy-ready; that skill also owns the wider investigation.
Read the result as:
- Low percentage, recent hours mostly green — transient. Report and move on.
For a queued PR, recommend re-enqueueing rather than a code change; posting
/trunk merge yourself needs approval, per the Safety rules above.
- Recent hours entirely red — an outage, not a flake. Say so, and stop
telling people to retry. Check https://www.githubstatus.com/ before
attributing it to this repository; a platform incident makes every other
signal a symptom.
- A burst of runs failing together within a couple of minutes — one shared
cause, not several bugs. Look for a bad commit many merges inherited, or a
GitHub dispatch overflow, which fails runs as
startup_failure before they
start and so leaves no log at all.
- Steady over days — a standing defect somebody owns. Worth a ticket even
though each occurrence looks like noise.
Local reproduction
Run only the narrowest command that exercises the failure. If the command shape
is unclear, read .agents/skills/hogli/SKILL.md and hogli <command> --help.
| Class |
Repro guidance |
| code regression |
hogli test path/to/test.py::TestClass::test_method or hogli test <file.test.ts> |
| flaky test |
Hand off to the fixing-flaky-tests skill. |
| lint |
Use the failing formatter/linter on touched files, e.g. hogli format:python. |
| typecheck |
Run the failing checker, e.g. pnpm --filter=@posthog/frontend typescript:check. |
| snapshot / visual |
Run the specific Playwright or Storybook workflow; read playwright-test if needed. |
| migration / schema |
hogli migrations:check; run migrations only if the user agrees. |
| codegen drift |
hogli build:openapi. |
| infra / runner |
No local repro. Get the base rate (above), report, and stop. |
| environment / setup |
Reproduce the setup step only if cheap and relevant to changed files. |
| skills build |
hogli lint:skills; if that passes, hogli build:skills. |
Do NOT run hogli test with no arguments. Do NOT run hogli nuke or
hogli dev:reset as a shortcut. Do NOT bypass hooks with --no-verify.
PostHog CI notes
- Most PostHog jobs run on
depot-ubuntu-latest or depot-ubuntu-latest-16.
Depot runs surface logs through the GitHub Actions UI / gh run view just
like standard GitHub-hosted runners, so read them there first.
- When a Depot runner dies mid-job, GitHub keeps no log to read: the job shows
no steps and its log blob 404s. Depot's own dashboard keeps that job's page,
with the verdict GitHub lost (an OOM kill, a lost runner). Open it in a
browser through the chrome-devtools MCP.
status.depot.dev covers the case
where Depot itself is the outage, and the depot-github-runners skill owns
runner troubleshooting beyond triage.
- If a job fails before
Checkout completes (no app code ran), classify as
infra / runner. Do not propose code fixes.
- PostHog CI frequently parallelizes the same test class across N shards
(
backend-tests (3/10) style). Reproduce from the specific failing test
path, not the shard index.
Report shape
Keep the response short. Include one likely-cause sentence and avoid deeper
speculation.
Target: PR #<num> - run <run-id> (<workflow file>)
Failing job: <job name>
Failing step: <step name>
Command: <failing command>
Excerpt:
<up to 40 lines, trimmed around the failure>
Classification: <class from the table>
Shadow run: <yes | no>
Likely cause: <one sentence>
Local repro: <exact command, or "none">
Next action (needs your approval):
- <push fix | rerun job | update snapshot | none>
If the classification is infra / runner or a shadow run, say so and stop;
do not propose a code change. For infra / runner, include the base rate and
whether a retry is warranted.
1---2name: debugging-ci-failures3description: Debugs failing GitHub Actions CI runs for PostHog PRs, commits, and branches, and answers broad CI-health questions ("is CI red?", "is master green today?", "what's broken right now?"). Use when the user asks why CI is red, asks for the current CI or master status, or mentions a failing check, GitHub Actions run, Depot runner, workflow, job, shard, merge queue kick, flaky test, lint failure, typecheck failure, snapshot diff, migration check, generated types drift, or skills build failure. Start with the `hogli ci:insights` digest (cross-run CI history from engineering analytics), then guides read-only inspection, failure classification, smallest local reproduction with hogli, and safe reporting without rerunning CI or posting to GitHub. Running unattended as the "Master-red diagnosis" workflow: see references/master-red-incident.md.4---5
6# Debugging PostHog CI failures
7
8Before you propose a change to CI, check [things already tried](../../../docs/internal/ci-things-already-tried.md) for the idea. It records what was measured, and why some good-sounding changes were reverted or rejected.
9
10Find the first meaningful failure, classify it, reproduce the smallest useful
11case locally when appropriate, and report the result. Avoid public-visible or
12irreversible actions unless the user explicitly asks.
13
14Always start with the `hogli ci:insights` digest. It aggregates across runs and
15branches, which `gh` cannot do cheaply, and tells you whether a failure is
16likely trunk-borne, gate-only, or isolated to a small set of branches. `gh` is
17authoritative for one run's current state and attribution. Use the digest to
18decide _what_ to inspect; use `gh` to confirm _whose_ failure it is and exactly
19what failed in a given run.
20
21This skill triages and classifies. Once a failure is confirmed flaky, hand off
22to the `fixing-flaky-tests` skill, which owns local reproduction, root-cause
23fixing, and N-run validation. For "who broke master" — the culprit commit and
24the commit that fixed it — hand off to the `investigating-ci-failures` skill,
25which owns the green/red boundary analysis. For aggregate pipeline health (is CI
26getting slower, which workflow is the long pole, how long PRs take to merge),
27read `diagnosing-ci-and-merge-bottlenecks`. Both are product skills under
28`products/engineering_analytics/skills/`, not invocable here: read the
29`SKILL.md` at that path.
30
31## Rule out a platform outage first
32
33GitHub Actions goes down often enough that it belongs before any log reading, and
34a platform incident makes every other signal a symptom. It costs two page loads:
35<https://www.githubstatus.com/> for GitHub, <https://status.depot.dev/> for the
36runners. Check them whenever failures are broad — several workflows at once, a
37burst of runs failing together, jobs dying before `Checkout`, or anything red
38across unrelated PRs.
39
40Report an outage as an outage, name the component, and stop recommending reruns.
41
42## Safety rules
43
44Do not do any of these without explicit approval in the current conversation:
45
46- Rerun or cancel a GitHub Actions run.
47- Post a GitHub comment, PR review, or issue comment through any CLI, MCP, or
48 API tool.
49- Push commits, force-push, rename branches, or delete branches.
50- Edit `.github/workflows/` files (CI infra changes need human review).
51- Merge, close, or reopen the PR.
52- Accept or update snapshots.
53
54Read-only `gh` calls and read-only GitHub tools are fine. If you need to
55change local Git state, make sure it is necessary for the task and does not
56overwrite unrelated work.
57
58## Workflow
59
60### 1. Start with CI insights (always first)
61
62`hogli ci:insights` reads PostHog's own engineering analytics — the cross-run
63failure history a single run can't show. Consult it before any raw `gh` log
64archaeology.
65
66```bash
67hogli ci:insights # digest for the current repo + branch
68hogli ci:insights search "<error or test name>" # match a specific failure
69hogli ci:insights view <ref> # one failure in full
70hogli ci:insights view <ref> --logs # ...plus the failing log lines
71```
72
73- Broad question ("is CI red?", "is master green today?", "what's broken right
74 now?"): the no-arg digest answers directly. It gives the default-branch verdict
75 (how many workflows are failing on their latest run, and which), the live
76 failures grouped by state, the jobs red on trunk right now, a grouped feed of
77 default-branch failures, and the PR your branch belongs to. You often do not
78 need a target PR or run at all; report from the digest.
79- Specific failure: run `search "<error>"` to match it before reading logs, then
80 `view <ref>` on a row the digest or search printed. `--logs` prints the thinned
81 failing lines from that failure's latest run, which is usually enough to
82 classify without touching `gh`.
83
84Read each row's `state` as a triage ranking:
85
86| State | Means |
87| ---------------------- | ---------------------------------------------------------------------------- |
88| `breaking_master` | failing on the default branch and that job's latest run is still red |
89| `blocking_merge_queue` | failed only on merge-queue gate branches in the window |
90| `novel_burst` | new within a day, already spreading across branches, not on trunk yet |
91| `potentially_resolved` | hit trunk but that job's latest run is green again |
92| `flaky` | sporadic across two or more branches over more than a day |
93| `pr_only` | limited branch spread; job status may be missing or behind the failure lines |
94
95`potentially_resolved` is a hint, not a conclusion: confirm from run data before
96reporting a failure as already fixed.
97
98`blocking_merge_queue` proves a gate failure happened in the window, not that it
99still blocks landings. Check the current queue run with `gh` before reporting it
100as active. Likewise, confirm `pr_only` from the current run before assigning the
101failure to a PR; it is also the fallback when job status is missing or stale.
102
103Caveats to carry into whatever you report:
104
105- Failure grouping is pytest-only. Jest, Playwright, and cargo failures appear
106 only in the digest's grouped master-failures section, never as a row with a ref.
107- Every count is absolute, never a rate. Passing runs are not in the test-level
108 data, so there is no denominator to quote. Job conclusions do record greens;
109 the base-rate section below is how to get a real rate from them.
110- A run's conclusion can lag until GitHub's `workflow_run` webhook settles it, so
111 during a live incident confirm a specific run against `gh`.
112
113If nobody has signed in on this machine, `hogli ci:insights` exits `78`. Treat
114exit `78` as "no CI insights available" and fall back to the `gh`-based
115inspection below — then tell the user they can run `hogli posthog:login`
116once, which opens a browser and needs no API key. Do not run it yourself: it
117waits on a consent screen you cannot see. Surface what you find per the Safety
118rules — do not auto-apply a fix.
119
120### 2. Find the failing run (for a specific failure)
121
122Determine the target in this order:
123
1241. If the user gave a PR number, run ID, check name, or branch, use it.
1252. Otherwise, infer from the current branch with
126 `gh pr view --json number,headRefName,statusCheckRollup`.
1273. If neither works, ask the user for a PR URL or run ID. Do not guess.
128
129**A PR kicked from the merge queue is the exception: its own checks are the
130wrong target.** Trunk tests each queued PR on a `trunk-merge/pr-<n>/<uuid>`
131branch holding master plus every PR queued ahead of it whose impacted targets
132overlap its own. The lane script over-reports targets on purpose, so in
133practice that is most of the queue. So:
134
135- The failing run is on that branch, never on the PR's head SHA. Take it from
136 `trunk merge status <n>` (`/merging-prs` step 4), not `gh pr checks`. Trunk publishes no check run here.
137 The branch is ephemeral; the run and its logs stay on GitHub, and the
138 warehouse keeps its jobs under that `head_branch` (query 8 in the
139 `investigating-ci-failures` references).
140- The PR's own checks can be green with the failing job **skipped** or
141 narrowed. On the PR, path filters see only that diff and the Django suite runs
142 a selected subset; on the queue branch the diff is every carried PR's and the
143 full matrices always run. A docs-only PR can be kicked by a job its own CI
144 never ran.
145- The branch names one PR but carries many. A failure on it is not evidence
146 against that PR until you find the change that caused it; the branch's other
147 merge commits are the first suspects.
148- In the digest this is the `blocking_merge_queue` state.
149
150Inspect read-only:
151
152```bash
153gh pr checks <pr>
154gh pr view <pr> --json statusCheckRollup
155gh run view <run-id> --json jobs,conclusion,name,workflowName,url
156gh run view <run-id> --log-failed
157```
158
159Use the full job log only when `--log-failed` lacks the failing command or
160enough surrounding output:
161
162```bash
163gh run view <run-id> --log --job <job-id>
164```
165
166Given a run id, the `engineering-analytics-run-failure-logs` MCP tool returns
167every failed job's error region with original line numbers, already thinned.
168One call instead of a jobs listing plus a log download, and it works when the
169job died before any test ran. It is bounded by Logs retention, so fall back to
170`gh` for older runs. Given a PR number instead of a run id,
171`engineering-analytics-ci-failure-logs` does the same across every run that PR
172has pushed, so an earlier push's failure is still there.
173
174Extract these before classifying:
175
176- Workflow name or file, e.g. `.github/workflows/ci-backend.yml`.
177- Job name, e.g. `backend-tests (4/10)`.
178- Step name, e.g. `Run pytest`.
179- Failing command and the smallest useful output excerpt.
180
181When scanning logs, search for `FAIL`, `Error`, `error:`, `assert`,
182`Traceback`, `exit code`, and `##[error]`. Stop at the first failing step that
183explains the run's conclusion. Keep excerpts under 40 lines.
184
185For test-job failures, the `trunk` MCP server's `investigate-ci-failure` tool
186is a shortcut past log scanning: give it the run URL
187(`https://github.com/PostHog/posthog/actions/runs/<run-id>`) and it returns
188structured failing-test details — names, error messages, stdout/stderr — from
189the results CI uploaded to Trunk Flaky Tests, with quarantined known-flaky
190tests already filtered out. It only covers what ran and uploaded: for jobs
191that died before tests (build, setup, lint), use
192`engineering-analytics-run-failure-logs` or `gh run view --log`.
193Authenticate once via `/mcp` → `trunk` (browser OAuth); headless environments
194instead add an `Authorization: Bearer` header with a `TRUNK_API_TOKEN` org
195token to the server entry in `.mcp.json`. To dig into one test's flakiness
196history, hand off to `fixing-flaky-tests`, which covers the `search-test` and
197`fix-flaky-test` tools.
198
199## Classification
200
201| Signal in the log | Class | First action |
202| -------------------------------------------------------------------------------------------------- | ------------------- | ---------------------------------------------------------------------------- |
203| `AssertionError`, test diff, `FAILED test_...` in a committed test file | code regression | reproduce with `hogli test <path>::<test>` |
204| Test failed here, passed on `master` or on rerun in the same PR | flaky test | confirm against `master` history; to fix, use `fixing-flaky-tests` |
205| `ruff`, `oxlint`, `stylelint`, `markdownlint`, `prettier` errors | lint | `hogli lint:python:fix` or `hogli format` on touched files |
206| `mypy`, `pyright`, `tsc`, `typescript:check` errors | typecheck | run the same checker locally, not the full suite |
207| Chromatic / Storybook / Playwright visual diff, snapshot mismatch | snapshot / visual | surface the diff URL; do NOT auto-accept snapshots |
208| `manage.py migrate` error, `migrations:check` failure, missing migration | migration / schema | `hogli migrations:check` locally |
209| OpenAPI schema diff, generated API types out of sync | codegen drift | `hogli build:openapi` |
210| `Cannot connect`, `ECONNREFUSED`, `address already in use`, OOM, runner killed, setup step timeout | infra / runner | get the base rate before calling it transient (below) |
211| `startup_failure` conclusion, a job with zero recorded steps, or a log blob that 404s | infra / runner | no log to read; check <https://www.githubstatus.com/> and the runs around it |
212| `apt-get`, `uv sync`, `pnpm install`, docker pull, setup action failures | environment / setup | diff `.nvmrc`, `pyproject.toml`, `package.json`, Dockerfiles |
213| `hogli lint:skills`, `hogli build:skills` failure | skills build | run the same `hogli` command locally |
214| SDK compat check, `ci-survey-sdk-check`, cross-version failure | SDK compatibility | check SDK version matrix for the affected package |
215
216If multiple signals match, choose the most specific class. For example, prefer
217codegen drift over lint, migration over typecheck, and snapshot / visual over a
218generic Playwright test failure.
219
220## Base rate for infra and setup failures
221
222"Transient" is a claim about how often the job fails, so do not assert it from
223one run. A job that dies before its tests run leaves no test-level evidence: no
224`FAILED` line, so no fingerprint and no span, so it never appears as a row in
225`broken_tests` or in the flaky-tests tool. It is still visible as a job
226conclusion, which is what the digest's master-failures section groups by, and
227`engineering-analytics-run-failure-logs` still returns its failing lines because
228it reads by run, not by test. Start there, not from a test.
229
230Unlike the span-derived test reads, this one can give you a real rate: the
231warehouse records every job attempt, greens included, so the denominator is
232honest. Query 7 in
233`products/engineering_analytics/skills/investigating-ci-failures/references/investigation-queries.md`
234is copy-ready; that skill also owns the wider investigation.
235
236Read the result as:
237
238- **Low percentage, recent hours mostly green** — transient. Report and move on.
239 For a queued PR, recommend re-enqueueing rather than a code change; posting
240 `/trunk merge` yourself needs approval, per the Safety rules above.
241- **Recent hours entirely red** — an outage, not a flake. Say so, and stop
242 telling people to retry. Check <https://www.githubstatus.com/> before
243 attributing it to this repository; a platform incident makes every other
244 signal a symptom.
245- **A burst of runs failing together within a couple of minutes** — one shared
246 cause, not several bugs. Look for a bad commit many merges inherited, or a
247 GitHub dispatch overflow, which fails runs as `startup_failure` before they
248 start and so leaves no log at all.
249- **Steady over days** — a standing defect somebody owns. Worth a ticket even
250 though each occurrence looks like noise.
251
252## Local reproduction
253
254Run only the narrowest command that exercises the failure. If the command shape
255is unclear, read `.agents/skills/hogli/SKILL.md` and `hogli <command> --help`.
256
257| Class | Repro guidance |
258| ------------------- | ------------------------------------------------------------------------------------ |
259| code regression | `hogli test path/to/test.py::TestClass::test_method` or `hogli test <file.test.ts>` |
260| flaky test | Hand off to the `fixing-flaky-tests` skill. |
261| lint | Use the failing formatter/linter on touched files, e.g. `hogli format:python`. |
262| typecheck | Run the failing checker, e.g. `pnpm --filter=@posthog/frontend typescript:check`. |
263| snapshot / visual | Run the specific Playwright or Storybook workflow; read `playwright-test` if needed. |
264| migration / schema | `hogli migrations:check`; run migrations only if the user agrees. |
265| codegen drift | `hogli build:openapi`. |
266| infra / runner | No local repro. Get the base rate (above), report, and stop. |
267| environment / setup | Reproduce the setup step only if cheap and relevant to changed files. |
268| skills build | `hogli lint:skills`; if that passes, `hogli build:skills`. |
269
270Do NOT run `hogli test` with no arguments. Do NOT run `hogli nuke` or
271`hogli dev:reset` as a shortcut. Do NOT bypass hooks with `--no-verify`.
272
273## PostHog CI notes
274
275- Most PostHog jobs run on `depot-ubuntu-latest` or `depot-ubuntu-latest-16`.
276 Depot runs surface logs through the GitHub Actions UI / `gh run view` just
277 like standard GitHub-hosted runners, so read them there first.
278- When a Depot runner dies mid-job, GitHub keeps no log to read: the job shows
279 no steps and its log blob 404s. Depot's own dashboard keeps that job's page,
280 with the verdict GitHub lost (an OOM kill, a lost runner). Open it in a
281 browser through the chrome-devtools MCP. `status.depot.dev` covers the case
282 where Depot itself is the outage, and the `depot-github-runners` skill owns
283 runner troubleshooting beyond triage.
284- If a job fails before `Checkout` completes (no app code ran), classify as
285 `infra / runner`. Do not propose code fixes.
286- PostHog CI frequently parallelizes the same test class across N shards
287 (`backend-tests (3/10)` style). Reproduce from the specific failing test
288 path, not the shard index.
289
290## Report shape
291
292Keep the response short. Include one likely-cause sentence and avoid deeper
293speculation.
294
295```text
296Target: PR #<num> - run <run-id> (<workflow file>)
297Failing job: <job name>
298Failing step: <step name>
299Command: <failing command>
300Excerpt:
301 <up to 40 lines, trimmed around the failure>
302
303Classification: <class from the table>
304Shadow run: <yes | no>
305Likely cause: <one sentence>
306Local repro: <exact command, or "none">
307Next action (needs your approval):
308 - <push fix | rerun job | update snapshot | none>
309```
310
311If the classification is `infra / runner` or a shadow run, say so and stop;
312do not propose a code change. For `infra / runner`, include the base rate and
313whether a retry is warranted.