CI: type=service step records state=failure from pod absence at teardown (exit_code 0) on otherwise-green pipelines #1000

Open
opened 2026-07-31 09:41:55 +00:00 by mos-dt-0 · 0 comments
Collaborator

CI: a type=service step records state=failure from the pod's absence at teardown — exit_code 0, on otherwise-fully-green pipelines

The tell, first

ci-postgres on pipeline 2139 (mosaicstack/stack, repo 47):

state       = failure
exit_code   = 0          <-- nothing reported a non-zero status
start_time  = null
end_time    = null
error       = pods "wp-svc-01kyvq0w222pgvfvm69pdr39gx-ci-postgres" not found

An exit_code of 0 under a failure state means the state was recorded from the pod's absence, not from anything the pod did. The Kubernetes backend went looking for the service pod during reconciliation, did not find it, and wrote failure from that miss. Null start/end times are consistent: no execution window was ever recorded for the thing being judged.

The pipeline still reports status=success, because service steps do not gate the verdict. So the visible artifact is a green pipeline containing a red row that no one can attribute.

Base rate — it is not any one branch

Across the last 40 repo-47 pipelines carrying a ci-postgres step:

count
pipelines with the step 40
ci-postgres = failure 3
…of those, on otherwise fully green runs 2

The two otherwise-green cases are 2139 and #2101, with the identical pods "wp-svc-…-ci-postgres" not found error string. #2101 predates the branch that surfaced this. Roughly 5% of runs, pre-existing, branch-independent.

The service was live — this is not a postgres fault

The test step opens with a pg_isready retry loop (60 iterations) against the service before doing anything else. From the decoded step log for 2139:

+ ready=0
  for i in $(seq 1 60); do if pg_isready -h ci-postgres -p 5432 -U ********; then
    ready=1
ci-postgres:5432 - accepting connections

It answered. No ECONNREFUSED, no exhausted retry budget, no skipped tests. All eight command steps returned success / exit 0. Whatever the orchestrator failed to find at teardown, the database was serving while the tests ran against it.

Why both halves are stated

Neither measurement is sufficient alone, and I want the reasoning on the record rather than the conclusion:

  • The base rate alone would be a dismissal — "it happens sometimes" is how a real service fault gets waved through on the third occurrence.
  • The liveness alone would be an anecdote — one green readiness probe does not establish that the red row is harmless in general.

Together they support the actual claim: the failure is backend bookkeeping at teardown, and the tests that depended on the service were not affected on this run.

Why it is worth fixing rather than tolerating

The failure mode and the safe state produce the same output. A service that genuinely died mid-run and a service whose pod record vanished at teardown both land as state=failure, exit_code=0 on a green pipeline. Today that ambiguity is resolved by hand — decoding the dependent step's log to see whether the service ever answered. That is not a check anyone will run on the hundredth occurrence, and the one time it matters is the time it gets skipped.

The concrete cost is that this row trains readers to ignore a red service step, which is precisely the signal that would report a real service outage.

Suggested direction

Distinguish "the step ran and failed" from "the step's record could not be found at reconciliation" — a distinct state, or at minimum an exit_code that is not 0, so the two are separable without reading logs. A reconciliation miss on a step that was never observed to start is arguably not a step failure at all.

Bounds

Filed at @Mos's scoping call rather than folded into the PR that surfaced it. I have not attempted a fix and have not examined the Woodpecker Kubernetes backend beyond the API records and step logs cited above. The 40-pipeline window is what I sampled; I did not scan further back, so the ~5% figure is a rate over that window and not a long-run estimate.

— mos-dt (sb-it-1-dt). Signed in body; shared account on this host, so the signature is a labelled claim, never provenance.

## CI: a `type=service` step records `state=failure` from the pod's *absence* at teardown — `exit_code 0`, on otherwise-fully-green pipelines ### The tell, first `ci-postgres` on pipeline **2139** (`mosaicstack/stack`, repo 47): ``` state = failure exit_code = 0 <-- nothing reported a non-zero status start_time = null end_time = null error = pods "wp-svc-01kyvq0w222pgvfvm69pdr39gx-ci-postgres" not found ``` **An `exit_code` of 0 under a `failure` state means the state was recorded from the pod's absence, not from anything the pod did.** The Kubernetes backend went looking for the service pod during reconciliation, did not find it, and wrote `failure` from that miss. Null start/end times are consistent: no execution window was ever recorded for the thing being judged. The pipeline still reports `status=success`, because service steps do not gate the verdict. So the visible artifact is a green pipeline containing a red row that no one can attribute. ### Base rate — it is not any one branch Across the **last 40** repo-47 pipelines carrying a `ci-postgres` step: | | count | |---|---| | pipelines with the step | 40 | | `ci-postgres` = `failure` | 3 | | …of those, on **otherwise fully green** runs | **2** | The two otherwise-green cases are **2139** and **#2101**, with the **identical** `pods "wp-svc-…-ci-postgres" not found` error string. #2101 predates the branch that surfaced this. Roughly **5% of runs**, pre-existing, branch-independent. ### The service was live — this is not a postgres fault The `test` step opens with a `pg_isready` retry loop (60 iterations) against the service before doing anything else. From the decoded step log for 2139: ``` + ready=0 for i in $(seq 1 60); do if pg_isready -h ci-postgres -p 5432 -U ********; then ready=1 ci-postgres:5432 - accepting connections ``` It answered. **No `ECONNREFUSED`, no exhausted retry budget, no skipped tests.** All eight command steps returned `success` / `exit 0`. Whatever the orchestrator failed to find at teardown, the database was serving while the tests ran against it. ### Why both halves are stated Neither measurement is sufficient alone, and I want the reasoning on the record rather than the conclusion: - **The base rate alone would be a dismissal** — "it happens sometimes" is how a real service fault gets waved through on the third occurrence. - **The liveness alone would be an anecdote** — one green readiness probe does not establish that the red row is harmless in general. Together they support the actual claim: the `failure` is backend bookkeeping at teardown, and the tests that depended on the service were not affected on this run. ### Why it is worth fixing rather than tolerating The failure mode and the safe state produce the same output. A service that genuinely died mid-run and a service whose pod record vanished at teardown **both** land as `state=failure, exit_code=0` on a green pipeline. Today that ambiguity is resolved by hand — decoding the dependent step's log to see whether the service ever answered. That is not a check anyone will run on the hundredth occurrence, and the one time it matters is the time it gets skipped. The concrete cost is that this row **trains readers to ignore a red service step**, which is precisely the signal that would report a real service outage. ### Suggested direction Distinguish "the step ran and failed" from "the step's record could not be found at reconciliation" — a distinct state, or at minimum an `exit_code` that is not `0`, so the two are separable without reading logs. A reconciliation miss on a step that was never observed to start is arguably not a step failure at all. ### Bounds Filed at @Mos's scoping call rather than folded into the PR that surfaced it. I have **not** attempted a fix and have not examined the Woodpecker Kubernetes backend beyond the API records and step logs cited above. The 40-pipeline window is what I sampled; I did not scan further back, so the ~5% figure is a rate over that window and not a long-run estimate. — mos-dt (sb-it-1-dt). Signed in body; shared account on this host, so the signature is a labelled claim, never provenance.
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: mosaicstack/stack#1000