Skip to content

Bound component execution in the verification runner so a hang still emits the aggregate report #199

Description

@alexeygrigorev

Grooming decision

Filed as a separate issue, not folded into #195 or #197. #195 is about components that complete with a non-success result under-reporting the counts their own logs hold (ci/evidence.py::machine_output_claim, inside create_report); this is about a component that never completes preventing create_report from being reached at all — a different file, a different layer, and #195's non-goals pin its verdict/report semantics tight. #197 is origin attribution for local evidence, an unrelated mechanism. The three issues are adjacent faces of one principle — the harness must always produce a truthful machine-readable aggregate — and each keeps a small attributable diff.

Problem

When a verification component command hangs, make verification-run never emits a final verification-report.json. Per-component evidence accumulates, but there is no aggregate artifact, so the handoff cannot carry the report path/digest _docs/PROCESS.md requires, and the engineer must hand-assemble the account.

Root cause (verified): ci/runner.py executes each component via subprocess.run(command, ..., check=False, stdout=PIPE, stderr=STDOUT) with no timeout — a hung child blocks the runner indefinitely. The Makefile chain only reaches verification-report-check (which invokes ci.verification report to write verification-report.json) after the runner exits. Other CI modules already bound their waits (ci/screenshot_runtime.py uses process.wait(timeout=...) with a graceful terminate; ci/container_check.py and ci/history.py use HTTP timeouts) — the runner's component execution is the unbounded outlier.

This violates the principle #193's verdict-precedence fix established and #195 carries forward: reports must stay emittable on failure. A hang is a failure mode; today it produces no aggregate at all.

Observed incidents (from #198 engineering, #198 (comment))

  • Attempt 1: disk-full failures plus a wedged playwright teardown, manually terminated — "No final aggregate report file was produced (verification-report-check not reached)."
  • Attempt 2: six components green with envelopes retained, but playwright hangs forever in a driver-level deadlock (test_accessibility.py mobile-viewport core smoke; faulthandler shows the main thread inside the Playwright driver event loop), reproduced identically at clean base b487b1f via stash A/B, and the owner's main-checkout run of the same module stayed wedged for 12 hours. "Because the runner hangs inside this component, make verification-run cannot emit a final verification-report.json."

Scope

  • Bound every runner component execution with an explicit wall-clock timeout: a documented default generous enough for the longest legitimate suite, plus an override (CLI flag and/or plan field — engineering's choice, recorded in the doc).
  • On expiry: terminate and then kill the child, retain whatever partial output exists under the component's output artifact, and record the component's result envelope as non-success with an explicit timeout indication and the exact command.
  • Continue to the remaining components and finish the run so verification-report-check emits verification-report.json with a failure verdict; the timed-out component is an executed rerun with a failure result, never a skip.
  • Document the default and override in _docs/ci/change-selective-ci.md.

Non-goals

Acceptance criteria

  • The runner bounds every component execution with an explicit timeout (documented default plus override); no component command can block the run indefinitely.
  • A timed-out component gets terminate-then-kill handling, its partial output is retained as the component's output artifact, and its result envelope records a non-success result with an explicit timeout indication and the exact command.
  • Remaining components still execute and the run reaches verification-report-check: a final verification-report.json is emitted with a failure verdict and no silently skipped component.
  • Regression tests in tests_ci/: a never-terminating component command under a small configured timeout yields the timeout result envelope and an aggregate report with a failure verdict; a normal green run is unaffected by the timeout plumbing.
  • _docs/ci/change-selective-ci.md records the default and override; make test-ci, lint, format-check, and typecheck green; verification plan per _docs/ci/change-selective-ci.md with all component dispositions recorded.
  • Screenshots: not_applicable — internal CI/process change with no product-page render impact, per _docs/PROCESS.md.

Dependencies: none. Coordination note: after #195 lands, a timed-out component's zero-count fallback falls out naturally from its unparseable-log path; before it, the existing non-success envelope behavior applies — neither ordering blocks this issue.

Normative references: _docs/PROCESS.md (evidence origins, report requirements), _docs/ci/change-selective-ci.md.

Metadata

Metadata

Assignees

No one assigned

    Labels

    P1Important follow-upbugSomething isn't workinginfraArea: infratestingArea: testing

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions