Skip to content
Draft
5 changes: 5 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -264,6 +264,11 @@ jobs:
# the teardown below keeps always() and still reaps the containers.
if: ${{ !cancelled() }}
run: python3 scripts/ci/fat_driver.py wait --results "$RUNNER_TEMP/fat/results.json"
# PMAT-5740 (#5740): a red section names its first failing line in this job log;
# the driver prints only the result, its cause sits in a ::group a cut-off log loses.
- name: First failing line of each failed section
if: ${{ failure() }}
run: cargo run -q -p aprender-ci-tools -- section-first-fail --results "$RUNNER_TEMP/fat/results.json"
# #4948: a cancelled run never reaches a section's own container removal,
# and its fat-<run>-<section> container then sits on `tail -f /dev/null`
# for days. always() runs on cancel too; on the passing path the driver
Expand Down
83 changes: 83 additions & 0 deletions contracts/ci-fat-driver-first-failing-line-v1.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
# ci-fat-driver-first-failing-line-v1 — PMAT-5740 (GH #5740): a failed x86-main
# section names its first failing line in the job log.
#
# Before: the driver printed `section N: failure in Ns` and `::error::section N:
# failure`; the cause sat in the section's own log, dumped only inside a ::group.
# A red whose group was cut off was not measured.
#
# Verification level:
# * L1 — `aprender-ci-tools section-first-fail` exists (crates/aprender-ci-tools/src/section_first_fail.rs).
# * L2 — falsified by 7 Rust tests (section_first_fail::tests); mutations `starts_with("::error")`
# -> `"::errorX"` (3 tests fail) and dropping `|| coe` (1 fails), each restored GREEN.
# * L3/L4 — NOT APPLICABLE: a CI log-reporting rule, not a kernel.
# * WHAT BLOCKS: unchanged. One ci.yml step (if: failure()) prints a log line; the driver,
# scripts/ci/fat_driver.py, is not edited (its complexity is already over the hook).
name: ci-fat-driver-first-failing-line
version: "1.0.0"
scope: >
What the section driver prints into the job log when a section ends in any
result other than success or skipped. Out of scope: the verdict rule and the
full per-section log dump.
status: active

metadata:
kind: pattern
version: "1.0.0"
created: '2026-10-19'
last_modified: '2026-10-19'
author: PAIML Engineering
references:
- 'crates/aprender-ci-tools/src/section_first_fail.rs first_failing_line, report, report_results'
- '.github/workflows/ci.yml x86-main step First failing line of each failed section'
description: >
After the driver's wait step fails, a step prints one `::error::section N first failing line: …`
beside its result line: the first `::error` line of its log, else the first
panic or `error:` line. A clean or missing log prints nothing.

equations:
failed_section_names_its_cause:
formula: |
result(n) not in {success, skipped} and log(n) has a failing line => job log has "section n first failing line: <that line>"
the line is the first ::error line, else the first panic / error: line
domain: 'one section log'
codomain: 'a single line of at most 300 chars, or none'
invariants:
- 'first_failing_line prefers the first ::error line over a later or earlier panic line'
- 'a clean or absent log yields the empty string'
preconditions:
- 'the section has ended'
postconditions:
- 'the verdict and results output are unchanged'

proof_obligations:
- id: CFF-OB-001
statement: "A failed section's first failing line is printed into the job log."
discharged_by: falsification_tests[0]

falsification_tests:
- id: CFF-F-001
rule: the first failing line of a failed section reaches the job log
prediction: >-
On a planted log, ::error:: lines yield the first one, a bare panic yields
that line, a clean log and a missing log yield none; results mode reads only
failed, blocking sections (7 tests).
test: 'cargo test -p aprender-ci-tools --lib section_first_fail'
if_fails: >-
a red whose ::group dump is cut off names no cause and is not measured
mutation: >-
starts_with("::error") -> "::errorX" fails 3 tests; dropping the continue_on_error skip fails 1; each restored GREEN (2026-10-19).

- id: CFF-F-002
rule: a rustc killed by a signal under sccache is named, not shown as a compile error (GH #5750)
prediction: >-
A log with `could not compile` then an sccache `(exit status: 2)` yields
`rustc killed by signal ...` naming the crate; an `(exit status: 1)` compile
error keeps its own `error:` line.
test: 'cargo test -p aprender-ci-tools --lib section_first_fail'
if_fails: >-
an OOM-killed shard reads as a compile error and nobody looks at the runner
mutation: >-
"(exit status: 2)" -> "(exit status: 3)" fails 1 test; restored GREEN (2026-10-19). Dropping the real-error-first guard fails 1 test (a real error before the exit-2 line is not masked); restored GREEN.

non_goals:
- "Changing which sections block, or the per-section log dump."
8 changes: 8 additions & 0 deletions contracts/contracts.nt
Original file line number Diff line number Diff line change
Expand Up @@ -9546,6 +9546,14 @@
<https://ont.paiml.dev/v1alpha1/contract/ci-determinism-wait-v1> <https://ont.paiml.dev/v1alpha1/name> "ci-determinism-wait"^^<http://www.w3.org/2001/XMLSchema#string> .
<https://ont.paiml.dev/v1alpha1/contract/ci-determinism-wait-v1> <https://ont.paiml.dev/v1alpha1/status> "active"^^<http://www.w3.org/2001/XMLSchema#string> .
<https://ont.paiml.dev/v1alpha1/contract/ci-determinism-wait-v1> <https://ont.paiml.dev/v1alpha1/version> "1.0.0"^^<http://www.w3.org/2001/XMLSchema#string> .
<https://ont.paiml.dev/v1alpha1/contract/ci-fat-driver-first-failing-line-v1> <http://www.w3.org/1999/02/22-rdf-syntax-ns#type> <http://www.w3.org/ns/prov#Entity> .
<https://ont.paiml.dev/v1alpha1/contract/ci-fat-driver-first-failing-line-v1> <http://www.w3.org/1999/02/22-rdf-syntax-ns#type> <https://ont.paiml.dev/v1alpha1/Contract> .
<https://ont.paiml.dev/v1alpha1/contract/ci-fat-driver-first-failing-line-v1> <https://ont.paiml.dev/v1alpha1/file> "contracts/ci-fat-driver-first-failing-line-v1.yaml"^^<http://www.w3.org/2001/XMLSchema#string> .
<https://ont.paiml.dev/v1alpha1/contract/ci-fat-driver-first-failing-line-v1> <https://ont.paiml.dev/v1alpha1/id> "ci-fat-driver-first-failing-line-v1"^^<http://www.w3.org/2001/XMLSchema#string> .
<https://ont.paiml.dev/v1alpha1/contract/ci-fat-driver-first-failing-line-v1> <https://ont.paiml.dev/v1alpha1/kind> "pattern"^^<http://www.w3.org/2001/XMLSchema#string> .
<https://ont.paiml.dev/v1alpha1/contract/ci-fat-driver-first-failing-line-v1> <https://ont.paiml.dev/v1alpha1/name> "ci-fat-driver-first-failing-line"^^<http://www.w3.org/2001/XMLSchema#string> .
<https://ont.paiml.dev/v1alpha1/contract/ci-fat-driver-first-failing-line-v1> <https://ont.paiml.dev/v1alpha1/status> "active"^^<http://www.w3.org/2001/XMLSchema#string> .
<https://ont.paiml.dev/v1alpha1/contract/ci-fat-driver-first-failing-line-v1> <https://ont.paiml.dev/v1alpha1/version> "1.0.0"^^<http://www.w3.org/2001/XMLSchema#string> .
<https://ont.paiml.dev/v1alpha1/contract/ci-gate-integrity-v1> <http://www.w3.org/1999/02/22-rdf-syntax-ns#type> <http://www.w3.org/ns/prov#Entity> .
<https://ont.paiml.dev/v1alpha1/contract/ci-gate-integrity-v1> <http://www.w3.org/1999/02/22-rdf-syntax-ns#type> <https://ont.paiml.dev/v1alpha1/Contract> .
<https://ont.paiml.dev/v1alpha1/contract/ci-gate-integrity-v1> <https://ont.paiml.dev/v1alpha1/file> "contracts/ci-gate-integrity-v1.yaml"^^<http://www.w3.org/2001/XMLSchema#string> .
Expand Down
1 change: 1 addition & 0 deletions crates/aprender-ci-tools/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ pub mod package_include_diff;
pub mod publishable_crates;
pub mod pyjson;
pub mod pystr;
pub mod section_first_fail;
pub mod tarball_build_errors;
pub mod tarball_shrink_report;
pub mod tarball_workspace;
22 changes: 20 additions & 2 deletions crates/aprender-ci-tools/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@

use aprender_ci_tools::{
coverage_report_scope, dag_status, git_patch_id, ladder_cells_produce, llama_fit_verdict,
nextest_fail_fast, package_include_diff, publishable_crates, tarball_build_errors,
tarball_shrink_report, tarball_workspace,
nextest_fail_fast, package_include_diff, publishable_crates, section_first_fail,
tarball_build_errors, tarball_shrink_report, tarball_workspace,
};
use clap::{ArgGroup, Parser, Subcommand};
use std::io::{Read, Write};
Expand Down Expand Up @@ -113,6 +113,17 @@ enum Cmd {
/// The nextest config, e.g. .config/nextest.toml.
toml: PathBuf,
},
/// `::error::section <stem> first failing line: <line>` for every section log that has
/// one (PMAT-5740): the first `::error` line, else the first panic or `error:` line.
/// Always exits 0; a log with no failing line prints nothing.
SectionFirstFail {
/// The driver's results.json: read only its failed, blocking sections' logs, which sit
/// beside it. Exit 1 when it cannot be read.
#[arg(long)]
results: Option<PathBuf>,
/// Section logs, e.g. "$RUNNER_TEMP"/fat/*.log.
logs: Vec<PathBuf>,
},
}

fn cargo_metadata() -> Result<String, String> {
Expand Down Expand Up @@ -222,6 +233,13 @@ fn run(cmd: Cmd) -> Result<String, Refusal> {
Cmd::LadderCellsProduce { cmd } => {
ladder_cells_produce::run(&cmd).map_err(|(code, reason)| (String::new(), code, reason))
}
Cmd::SectionFirstFail { results, logs } => {
let mut out = section_first_fail::report(&logs);
if let Some(r) = results {
out.push_str(&section_first_fail::report_results(&r).map_err(nothing_printed)?);
}
Ok(out)
}
Cmd::NextestFailFast { reader, toml } => {
// clap's value_parser admits only the two names parse() knows.
let reader = nextest_fail_fast::Reader::parse(&reader)
Expand Down
Loading
Loading