Debugging Pipeline Failures
Debugging a pipeline failure turns "the build is red" into an explained failure: which job, step and test failed, what the first error was, whether it happens again on the same commit, and which change caused it. The lesson on automation reporting and debugging applied the ISTQB debugging cycle (reproduce the failure, diagnose its cause, fix it) to one failed test; at pipeline level the commit, the runner and the run history become part of the evidence. The tester often drives the reproduction and the first diagnosis, then hands the developer the evidence.
Example: a nightly end-to-end run fails in one of four shards. The log names the test, the trace shows the data the page received, the test fails on your laptop once the browser uses the runner’s time zone, and a bisect names the commit that broke the date format.
Why it matters
A red pipeline blocks every merge and release behind it, and a wrong classification costs more than slow debugging. Calling a product defect "flaky" lets it ship; calling an environment problem a product bug sends developers hunting for a defect that is not in the code. Re-running until green does both: it hides the evidence and teaches the team that red means nothing.
For a tester, this is where trust is earned. A report that names the failing test, the first error, the artifacts, a reproduction command and the first bad commit gets fixed quickly; "the pipeline is red, can someone look?" does not.
Start with the log and the artifacts
As in the lesson on automation reporting and debugging, start from the first error and its evidence; in a pipeline, that means first finding the first failed job and the first failed step inside it. Search the log for the test name or the error marker (##[error] in a raw GitHub Actions log) instead of scrolling, and remember that runner timestamps are usually in UTC.
Then download the job’s artifacts before their retention period ends, such as the JUnit XML report, the Playwright trace and the application logs, and read them as that lesson showed. Compare them with the job’s last green run: a new runner image, a changed dependency or fewer tests executed is a clue before you read any code.
# GitHub CLI: logs of the failed steps only, then the artifacts of one job
gh run view 9876543 --log-failed
gh run download 9876543 -n e2e-results-3
# open the trace recorded for the failing test
npx playwright show-trace test-results/invoice-order-date/trace.zipReproduce the failure locally
A failure you can reproduce on your own machine can be studied with a debugger, extra logging and as many runs as you need. Reproduction is a matter of parity: match the CI job in everything that can change the result, then change one thing at a time. If the test still passes locally, the remaining difference is your best clue.
- Commit: the exact SHA from the job log. A pull-request run on GitHub Actions tests a temporary merge into the target branch, not your branch head; that commit is on no branch, so fetch it with
git fetch origin pull/<number>/mergeand check that the SHA matches the log. - Image and versions: the job’s container image if it has one, dependencies from the lockfile (
npm ci, notnpm install) and the browser builds your Playwright version pins. - Environment variables: the non-secret values the job sets, including
CI=true, which many test configurations read; your own test credentials, never production secrets. - Time zone, locale and seed: runners often use UTC and an English locale, and tests that generate random data should log their seed so the data can be replayed.
- Subset and order: the same shard and worker count, then the failing test alone and repeated, to tell "fails alone" from "fails only next to other tests".
git checkout 9f8e7d6 # the SHA the job tested
npm ci && npx playwright install --with-deps chromium
export CI=true TEST_SEED=48151 # non-secret values copied from the job
npx playwright test --shard=3/4 --workers=2
npx playwright test invoice.spec.ts --repeat-each=10Timeouts, fail-fast and re-runs
A timeout means something the test waited for never happened: an element, a response, a service. Raising the limit rarely fixes it; the call log or the trace shows what was awaited. Layer the limits: a test timeout (30 seconds by default in Playwright) fails one named test and the report is still written, while a job timeout (timeout-minutes on GitHub Actions, 360 minutes if unset) cancels the whole job and may leave no report. Keep test timeouts well below the job timeout.
Fail-fast stops at the first failure (in Playwright, -x, short for --max-failures=1); run-all collects every failure. Fail fast in cheap early stages such as linting and unit tests. In expensive stages and nightly runs, run everything: one failing test and forty failing tests tell different stories, and forty usually point to a shared cause such as the environment. A GitHub Actions matrix cancels its other jobs when one fails unless you set fail-fast: false.
Re-run as narrowly as you can: only the failed jobs (gh run rerun <run-id> --failed, where the run ID is the long number in the run’s URL, not its # number) or only the failing test. A GitHub Actions re-run uses the same commit, so a pass proves non-determinism, not a fix; record it as evidence.
Retries and quarantine, in brief: an automatic retry turns a run green when a flaky test passes on a later attempt, and quarantine moves a known-flaky test into a non-blocking job where it keeps running. Both are containment, not a cure: allow at most one retry, report "passed on retry" as flaky, and give every quarantined test an owner, a ticket and a deadline. The flaky tests guide covers the policy, and the CI/CD testing guide’s triage flow shows where a re-run fits.
jobs:
e2e:
runs-on: ubuntu-latest
timeout-minutes: 30 # job limit: cuts off a hang
strategy:
fail-fast: false # one failing shard does not cancel the others
matrix:
shard: [1, 2, 3, 4]
steps:
# checkout and install steps omitted
- run: npx playwright test --shard=${{ matrix.shard }}/4Bisecting a regression across pipeline runs
When a scheduled run on main turns red after many merges, the pipeline history gives you the last green and the first red run, each with its commit. First check the test’s history in the CI test reports: a test that has failed now and then for weeks is a flaky candidate, not a new regression. If it fails every time from one run on, binary search finds the change: test the middle commit, keep the half where pass turns into fail, and repeat. Sixteen candidate commits need at most four test runs.
git bisect run automates the search: its command exits with 0 for a good commit, 125 to skip a commit that cannot be tested, and another code from 1 to 127 for a bad one. Trust the result only if the test fails every time on a bad commit, so repeat it at each step, and if the environment stays fixed. If the last green commit now fails too, the cause is outside the code, such as a runner image, an external service or expired test data.
# last green nightly: a1b2c3d first red nightly: 9f8e7d6
git bisect start 9f8e7d6 a1b2c3d
# TZ=UTC gives the browser the runner's time zone (Linux and macOS)
git bisect run sh -c 'npm ci || exit 125
TZ=UTC npx playwright test -g "order date" --repeat-each=3'
# git prints "<sha> is the first bad commit"
git bisect resetWorked example: a red nightly run
Nightly run 412 on main is red; run 411 was green, with nine commits in between. Fail-fast is off, so all four shards finished, and shard 3 has one failure in invoice.spec.ts, "invoice shows the order date": expected 1 Oct 2026, received 30 Sep 2026. It failed on its first attempt and on its retry, so the report lists it as failed, not flaky. The trace shows that the API returned createdAt as 2026-09-30T22:40:00Z, which is 01:40 on 1 October in Riyadh, the store’s time zone: the data is right and the display is wrong.
On the tester’s laptop in Riyadh the test passes. With the job’s commit, lockfile, browser build and seed, one difference remains: the runner’s browser uses UTC. With Playwright’s timezoneId set to UTC, the test fails 10 times out of 10. With TZ=UTC in the bisect command, so that every commit runs in the runner’s time zone, git bisect run names c41d9a0, "Format dates with Intl.DateTimeFormat", which dropped the store’s time zone. The tester files a product defect with the run link, the test, the trace, the reproduction setting and the first bad commit, and proposes pinning timezoneId in the test configuration so that local runs behave like CI.
| Setting | CI: nightly run 412, shard 3 | Laptop: first run | Laptop: time zone matched |
|---|---|---|---|
| Commit tested | 9f8e7d6 | 9f8e7d6 | 9f8e7d6 |
| Dependencies | lockfile, npm ci | lockfile, npm ci | lockfile, npm ci |
| Browser build | Chromium pinned by Playwright | Chromium pinned by Playwright | Chromium pinned by Playwright |
| Test data seed | 48151 | 48151 | 48151 |
| Browser time zone | UTC | Asia/Riyadh (UTC+3) | UTCset with timezoneId |
| Invoice-date test | shows 30 Sep 2026Fail | shows 1 Oct 2026Pass | shows 30 Sep 2026 in 10 of 10 runsFail |
- Differs from the CI job: the clue
Common mistakes
- Letting the job’s artifacts expire before downloading them, so the trace and the reports of a failure you could not reproduce are gone.
- Re-running the whole workflow instead of only the failed jobs, which costs runner time and repeats jobs that already passed.
- Reproducing on a different commit, such as your branch head instead of the merge commit CI tested, or with
npm installinstead of the lockfile. - Passing the
#run number togh run reruninstead of the run ID from the run’s URL, so the command does not re-run the run you meant. - Leaving fail-fast on in an expensive matrix, so one failure cancels the shards whose results you needed for triage.
- Bisecting a flaky test or a changed environment: the search then points at an innocent commit.
The QA perspective
Agree with the team who looks at a red pipeline and how fast; some teams rotate the role, sometimes pairing a tester with a developer. Start from the run link and write down what you find as you go: job, step, first error, artifacts, reproduction command, re-run result and classification.
Ask for what makes the next failure cheaper: artifacts uploaded even when tests fail, seeds and versions printed in the log, test timeouts below the job timeout. Keep the failure log the CI/CD testing guide recommends; within a few weeks it shows whether product defects, flaky tests or environments dominate, which is the evidence you need to get the pipeline fixed.
Practice exercise
Triage a job that timed out
A pull request’s pipeline is red. Read the run summary and the log below, then classify the failure, name the missing evidence, plan the reproduction and say what you would change in the pipeline so that the next failure leaves better evidence.
Pull request #214, workflow "ci", run #7731 (run ID 11407731 in its URL)
Commit 5be20d7 (merge of feature/refunds into main)
unit-tests passed 3m 10s
api-tests failed 20m 00s
e2e (1/2) skipped needs: api-tests
e2e (2/2) skipped needs: api-tests
api-tests log, last lines (UTC):
12:04:31 PASS orders.create > returns 201 for a valid order (0.8s)
12:04:32 RUN refunds.create > refunds a captured payment
12:23:58 ##[error]The job running on runner ci-runner-07 has exceeded the
maximum execution time of 20 minutes.
12:23:58 ##[error]The operation was canceled.
Artifacts: unit-reports (api-reports missing)
api-tests on main, last two runs: passed, passed- Identify the failed job, the step and the last test that started.
- Classify the failure as far as the evidence allows, and say what the skipped jobs tell you.
- Explain why there is no API test report and how to get one next time.
- Write how you would reproduce the failure and how you would re-run it.
Show the model answer
The api-tests test step hung: refunds.create > refunds a captured payment started at 12:04:32 and never reported a result; about 19 minutes later the 20-minute job timeout cancelled the job. It is a hang, not an assertion failure. The e2e jobs were skipped because they need api-tests, so they say nothing about the product.
Still open: a product defect (the refund call never returns), an environment problem (the payment stub, a database lock) or a flaky hang. The job is green on main and the pull request changes refunds, so the change is the first suspect, but only reproduction can decide.
The job was cancelled before the test runner wrote its report. Set a per-test timeout well below the job timeout (for example 60 seconds) and client timeouts on the tests’ HTTP calls, so that a hang fails one named test with a report; upload reports in a step that runs even when tests fail; and log request ids and timestamps to match them with the server logs.
Re-run only the failed job once (gh run rerun 11407731 --failed, with the run ID, not #7731): a pass makes it a flaky candidate but does not prove the change safe. Locally, fetch the merge commit (git fetch origin pull/214/merge), check out 5be20d7, run npm ci, start the job’s services, run refunds.create alone with the short timeout and read the application and stub logs while it hangs. No bisect is needed: the only candidate change is this pull request.
Frequently asked questions
Should I re-run a failed job before investigating?
Read the first error and download the artifacts first, then re-run once on the same commit. The re-run tells you whether the failure is deterministic; it is evidence for triage, not a fix, and a pass does not prove the change safe.
What if I cannot reproduce the failure locally?
Close the remaining differences one at a time: container image, browser build, environment variables, time zone, seed, shard and worker count. If the test still passes, collect more evidence in CI instead: traces on every attempt, more logging, a re-run with debug logging, and repetitions there to measure how often it fails.
When is bisecting worth it?
When the failure is deterministic and several commits lie between the last green and the first red run, which is typical for scheduled runs on the main branch. In a single pull request the change itself is the suspect, and with a flaky test or a changed environment bisecting points at the wrong commit.
References
- ISTQB Certified Tester Foundation Level (CTFL) v4.0The foundation syllabus of software testing: test levels and types, test design techniques, test management and tools.
- GitHub Actions documentationHow workflows, jobs, runners and artifacts are defined in GitHub Actions — where automated tests run on each push or pull request.
- Reference — GitThe official reference for every Git command and its options, grouped by task — setup, branching, sharing, patching, inspection and debugging.
- Playwright DocumentationOfficial documentation for Playwright, an end-to-end test framework that drives Chromium, WebKit and Firefox.
Practise with Mutqan tools
Compare two texts line by line or word by word and highlight differences.
Pretty-print XML with configurable indentation, and minify it back.
Convert a date and time between timezones (Riyadh, Amman, Delhi, UTC, New York, London…).
Format and validate YAML with consistent indentation and quoting.
Go deeper
Your progress on this topic
Not started
Mark the topic complete when you have studied it and done the exercise.
Sign in to track your progress
Mark topics complete, continue where you left off and unlock specialisation exams.