Skip to content

Commit f647dbf

Browse files
committed
feat(stack): add a strict mode to known-failures
`stack known-failures` gains a `--strict` flag. The Model Context Protocol (MCP) tool `eb_stack_known_failures` gains a matching `strict: true` property. With the flag, the command prints the same report. It then exits 1 when a warning remains. A pre-bot script or continuous integration (CI) job can now fail on a known failure that has no remedy yet. In strict mode, the MCP tool returns a tool error whose text lists those warnings. Remedied hits never count. A plan with every hit remedied exits 0. Without `--strict`, the behavior and output stay byte-for-byte the same. A new helper, `KnownFailuresReport::strict_error`, builds one line per remaining warning. Each line names the easyconfig and the architecture. The command-line and MCP layers make the strict decision with that helper. `run_known_failures` and its stdout stay unchanged. The command-line reference, the arch-known-failures how-to, the eessi-extend skill and the changelog document the flag. `llms-full.txt` now matches the docs again.
1 parent 41e1b15 commit f647dbf

9 files changed

Lines changed: 235 additions & 6 deletions

File tree

‎CHANGELOG.md‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -387,6 +387,12 @@ All notable changes to this project are documented here.
387387

388388
### Added
389389

390+
- `stack known-failures` and the MCP tool `eb_stack_known_failures` take a
391+
`--strict` flag (`strict: true`). It prints the same report, then exits 1
392+
(returns a tool error that lists the warnings) when a warning remains, so a
393+
pre-bot script or CI job can fail on a known failure with no remedy in
394+
place. Remedied hits never count, so a plan whose every hit is remedied
395+
exits 0. Without it the behavior and output are unchanged.
390396
- `stack bot-classify` and the MCP tool `eb_stack_bot_classify` read EESSI
391397
bot comments, `_bot_job<id>.result` files and job output, and classify
392398
each bot job as a real failure (with the campaign build-finding class),

‎docs/orgmode/howto/arch-known-failures.org‎

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,24 @@ so a GROMACS without CUDA is not warned about, and ~min_easybuild =
9393
entries, ~in EESSI:~ names the hook that already applies the remedy, such
9494
as ~pre_single_extension_orjson~ for the a64fx orjson test.
9595

96+
* Failing a pre-bot check on a remaining warning
97+
98+
By default a warning is advice and the command exits 0. Pass ~--strict~
99+
to exit 1 while any warning remains, so a pre-bot script or CI job fails
100+
on a known failure that has no remedy in place yet:
101+
102+
#+begin_src bash
103+
eb-stack stack known-failures --strict \
104+
--arch aarch64/a64fx \
105+
--easystack tests/arch_known_failures_fixtures/easystacks/1829/eessi-2026.06-eb-5.4.1-2026.1.yml \
106+
--easyconfigs tests/arch_known_failures_fixtures/easyconfigs
107+
#+end_src
108+
109+
The stdout is the same report as without ~--strict~; the error on stderr
110+
names each remaining warning by easyconfig and target, here
111+
~orjson-3.11.8-GCCcore-15.2.0.eb on aarch64/a64fx~. A remedied hit never
112+
counts, so the merged 1670 easystack above exits 0 under ~--strict~.
113+
96114
* Choosing architectures
97115

98116
~--arch~ repeats and takes a target or a family. ~--arch aarch64~ meets

‎docs/orgmode/reference/cli.org‎

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ are deterministic bundle artifacts.
1515
| 1 | The work was attempted and failed: a missing or unreadable file, a document that does not parse, a check that found errors, a network fetch that failed, a doctor probe, or a campaign whose build failed |
1616
| 2 | The command line was rejected before that work: a missing or unknown flag (clap), or a flag value or combination the command does not accept |
1717

18-
A rejected value names the flag and the value. A missing file names the flag and the path. Status 2 is the same status clap already uses. A recipe that fails ~recipe lint~ or ~recipe check~ is status 1: the files were read and the check failed. ~stack from-commit~ is status 1 when any pin is ~released~, and status 0 when every pin is ~newer~ or ~unknown~ or the files name no pin. A file name with no ~eb-X.Y.Z~, and no ~--easybuild-version~, is status 2. ~stack arch~ is status 1 when an entry is unsupported or flagged, or when ~--probe-sources~ reports ~unreachable~ or ~arch-missing~ sources. ~stack files~ is status 1 when a patch or an easyconfig is missing, and status 0 when the only lines are ~no_url~ or ~unchecked~. ~stack generation~ is status 1 on any finding, and status 2 when an easystack path names no EESSI version and ~--eessi-version~ is absent. ~stack known-failures~ is status 0 when it prints warnings: they are advice for the pull request, not a refusal. ~stack oom~ is status 0 when it warns: the warnings are advice, not a failed check. ~stack bot-classify~ is status 0 for every class, and status 2 when a path is a directory. ~stack batch~ is status 1 when the plan includes, or depends on, an entry the architecture cannot build.
18+
A rejected value names the flag and the value. A missing file names the flag and the path. Status 2 is the same status clap already uses. A recipe that fails ~recipe lint~ or ~recipe check~ is status 1: the files were read and the check failed. ~stack from-commit~ is status 1 when any pin is ~released~, and status 0 when every pin is ~newer~ or ~unknown~ or the files name no pin. A file name with no ~eb-X.Y.Z~, and no ~--easybuild-version~, is status 2. ~stack arch~ is status 1 when an entry is unsupported or flagged, or when ~--probe-sources~ reports ~unreachable~ or ~arch-missing~ sources. ~stack files~ is status 1 when a patch or an easyconfig is missing, and status 0 when the only lines are ~no_url~ or ~unchecked~. ~stack generation~ is status 1 on any finding, and status 2 when an easystack path names no EESSI version and ~--eessi-version~ is absent. ~stack known-failures~ is status 0 when it prints warnings: they are advice for the pull request, not a refusal; with ~--strict~ it is status 1 when a warning remains, and status 0 when every hit is remedied. ~stack oom~ is status 0 when it warns: the warnings are advice, not a failed check. ~stack bot-classify~ is status 0 for every class, and status 2 when a path is a directory. ~stack batch~ is status 1 when the plan includes, or depends on, an entry the architecture cannot build.
1919

2020
~--help~ for every command has an example and a description of every flag. A default that the command applies is printed in that help. A directory that receives several artifacts is ~--out-dir~. A single output file is ~--out~ (~--lock-out~ for the lock ~stack solve~ writes). ~--installed~, ~--easyconfigs~, ~--filter-deps~, and ~--generated-at~ use those names on every command that takes them.
2121

@@ -840,6 +840,19 @@ easyconfig that already contains it, turns the line into one
840840
~remedied:~ line. ~known=~ counts warnings and ~remedied=~ counts
841841
those. The command exits 0 either way.
842842

843+
~--strict~ exits 1 when a warning remains after the report, so a
844+
pre-bot script or CI job can fail on a known failure that has no remedy
845+
in place. The stdout is the same report; the error on stderr names each
846+
remaining warning by easyconfig and target. Remedied hits never count,
847+
so a plan whose every hit is remedied exits 0.
848+
849+
#+begin_src bash
850+
eb-stack stack known-failures --strict \
851+
--arch aarch64/a64fx \
852+
--easystack tests/arch_known_failures_fixtures/easystacks/1829/eessi-2026.06-eb-5.4.1-2026.1.yml \
853+
--easyconfigs tests/arch_known_failures_fixtures/easyconfigs
854+
#+end_src
855+
843856
~--arch~ takes a target (~aarch64/a64fx~) or a family (~aarch64~), and
844857
repeats. A family meets each of its targets. No ~--arch~ checks every
845858
architecture in the table. ~--easyconfigs~ is the robot tree for the

‎llms-full.txt‎

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3857,6 +3857,24 @@ so a GROMACS without CUDA is not warned about, and ~min_easybuild =
38573857
entries, `in EESSI:` names the hook that already applies the remedy, such
38583858
as `pre_single_extension_orjson` for the a64fx orjson test.
38593859

3860+
# Failing a pre-bot check on a remaining warning
3861+
3862+
By default a warning is advice and the command exits 0. Pass `--strict`
3863+
to exit 1 while any warning remains, so a pre-bot script or CI job fails
3864+
on a known failure that has no remedy in place yet:
3865+
3866+
```bash
3867+
eb-stack stack known-failures --strict \
3868+
--arch aarch64/a64fx \
3869+
--easystack tests/arch_known_failures_fixtures/easystacks/1829/eessi-2026.06-eb-5.4.1-2026.1.yml \
3870+
--easyconfigs tests/arch_known_failures_fixtures/easyconfigs
3871+
```
3872+
3873+
The stdout is the same report as without `--strict`; the error on stderr
3874+
names each remaining warning by easyconfig and target, here
3875+
`orjson-3.11.8-GCCcore-15.2.0.eb on aarch64/a64fx`. A remedied hit never
3876+
counts, so the merged 1670 easystack above exits 0 under `--strict`.
3877+
38603878
# Choosing architectures
38613879

38623880
`--arch` repeats and takes a target or a family. `--arch aarch64` meets
@@ -4302,7 +4320,7 @@ are deterministic bundle artifacts.
43024320
| 1 | The work was attempted and failed: a missing or unreadable file, a document that does not parse, a check that found errors, a network fetch that failed, a doctor probe, or a campaign whose build failed |
43034321
| 2 | The command line was rejected before that work: a missing or unknown flag (clap), or a flag value or combination the command does not accept |
43044322

4305-
A rejected value names the flag and the value. A missing file names the flag and the path. Status 2 is the same status clap already uses. A recipe that fails `recipe lint` or `recipe check` is status 1: the files were read and the check failed. `stack from-commit` is status 1 when any pin is `released`, and status 0 when every pin is `newer` or `unknown` or the files name no pin. A file name with no `eb-X.Y.Z`, and no `--easybuild-version`, is status 2. `stack arch` is status 1 when an entry is unsupported or flagged, or when `--probe-sources` reports `unreachable` or `arch-missing` sources. `stack files` is status 1 when a patch or an easyconfig is missing, and status 0 when the only lines are `no_url` or `unchecked`. `stack generation` is status 1 on any finding, and status 2 when an easystack path names no EESSI version and `--eessi-version` is absent. `stack known-failures` is status 0 when it prints warnings: they are advice for the pull request, not a refusal. `stack oom` is status 0 when it warns: the warnings are advice, not a failed check. `stack bot-classify` is status 0 for every class, and status 2 when a path is a directory. `stack batch` is status 1 when the plan includes, or depends on, an entry the architecture cannot build.
4323+
A rejected value names the flag and the value. A missing file names the flag and the path. Status 2 is the same status clap already uses. A recipe that fails `recipe lint` or `recipe check` is status 1: the files were read and the check failed. `stack from-commit` is status 1 when any pin is `released`, and status 0 when every pin is `newer` or `unknown` or the files name no pin. A file name with no `eb-X.Y.Z`, and no `--easybuild-version`, is status 2. `stack arch` is status 1 when an entry is unsupported or flagged, or when `--probe-sources` reports `unreachable` or `arch-missing` sources. `stack files` is status 1 when a patch or an easyconfig is missing, and status 0 when the only lines are `no_url` or `unchecked`. `stack generation` is status 1 on any finding, and status 2 when an easystack path names no EESSI version and `--eessi-version` is absent. `stack known-failures` is status 0 when it prints warnings: they are advice for the pull request, not a refusal; with `--strict` it is status 1 when a warning remains, and status 0 when every hit is remedied. `stack oom` is status 0 when it warns: the warnings are advice, not a failed check. `stack bot-classify` is status 0 for every class, and status 2 when a path is a directory. `stack batch` is status 1 when the plan includes, or depends on, an entry the architecture cannot build.
43064324

43074325
`--help` for every command has an example and a description of every flag. A default that the command applies is printed in that help. A directory that receives several artifacts is `--out-dir`. A single output file is `--out` (`--lock-out` for the lock `stack solve` writes). `--installed`, `--easyconfigs`, `--filter-deps`, and `--generated-at` use those names on every command that takes them.
43084326

@@ -5127,6 +5145,19 @@ easyconfig that already contains it, turns the line into one
51275145
`remedied:` line. `known`` counts warnings and `remedied`` counts
51285146
those. The command exits 0 either way.
51295147

5148+
`--strict` exits 1 when a warning remains after the report, so a
5149+
pre-bot script or CI job can fail on a known failure that has no remedy
5150+
in place. The stdout is the same report; the error on stderr names each
5151+
remaining warning by easyconfig and target. Remedied hits never count,
5152+
so a plan whose every hit is remedied exits 0.
5153+
5154+
```bash
5155+
eb-stack stack known-failures --strict \
5156+
--arch aarch64/a64fx \
5157+
--easystack tests/arch_known_failures_fixtures/easystacks/1829/eessi-2026.06-eb-5.4.1-2026.1.yml \
5158+
--easyconfigs tests/arch_known_failures_fixtures/easyconfigs
5159+
```
5160+
51305161
`--arch` takes a target (`aarch64/a64fx`) or a family (`aarch64`), and
51315162
repeats. A family meets each of its targets. No `--arch` checks every
51325163
architecture in the table. `--easyconfigs` is the robot tree for the

‎skills/eessi-extend/SKILL.md‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -311,7 +311,9 @@ line, an `ARCH` conditional, or a pin), and the EESSI hook that applies
311311
it, if one does. Put the remedy in the easyconfig, or the warning in the
312312
pull request description, before the bot runs. Do not answer a known
313313
failure with a rerun. A `remedied:` line means a pin or the easyconfig
314-
already carries the fix. The command exits 0. The table is
314+
already carries the fix. The command exits 0; add `--strict` to exit 1
315+
when a warning remains (remedied hits never count), so a pre-bot script
316+
or CI job fails on a known failure with no remedy in place. The table is
315317
`data/arch-known-failures.toml`, and the how-to is
316318
`docs/orgmode/howto/arch-known-failures.org`.
317319

‎src/arch_known_failures.rs‎

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -388,6 +388,22 @@ pub struct KnownHit {
388388
}
389389

390390
impl KnownHit {
391+
/// A one-line summary naming the easyconfig, the architectures, and the
392+
/// phase, for the `--strict` error text.
393+
pub fn warning_summary(&self) -> String {
394+
let mut subject = self.file.clone();
395+
if let Some(extension) = &self.extension {
396+
subject.push_str(&format!(" (extension {extension})"));
397+
}
398+
let arches = self
399+
.arches
400+
.iter()
401+
.map(|arch| arch_text(arch))
402+
.collect::<Vec<_>>()
403+
.join(", ");
404+
format!("{subject} on {arches}: known {}", self.phase.label())
405+
}
406+
391407
/// The lines the command prints for this hit, without a final newline.
392408
pub fn message(&self) -> String {
393409
let mut subject = self.file.clone();
@@ -836,6 +852,14 @@ impl KnownFailuresReport {
836852
pub fn warnings(&self) -> impl Iterator<Item = &KnownHit> {
837853
self.hits.iter().filter(|hit| hit.remedied.is_none())
838854
}
855+
856+
/// The `--strict` error: one line per remaining warning, each naming the
857+
/// easyconfig, the architectures, and the phase. `None` when every hit is
858+
/// remedied, so `--strict` exits 0.
859+
pub fn strict_error(&self) -> Option<String> {
860+
let lines: Vec<String> = self.warnings().map(KnownHit::warning_summary).collect();
861+
(!lines.is_empty()).then(|| lines.join("\n"))
862+
}
839863
}
840864

841865
/// Warn about known per-architecture failures a plan includes or depends on.

‎src/main.rs‎

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1271,9 +1271,11 @@ enum StackCommand {
12711271
/// remedy as easyconfig text, and the EESSI hook that applies it, if
12721272
/// one does. A from-commit or include-easyblocks-from-commit pin that
12731273
/// carries the fix, or an easyconfig that already has it, is reported
1274-
/// as remedied. The command exits 0 either way.
1274+
/// as remedied. The command exits 0 either way, unless `--strict` is
1275+
/// given, when a remaining warning makes it exit 1. Remedied hits never
1276+
/// count against `--strict`.
12751277
#[command(
1276-
after_long_help = "Examples:\n eb-stack stack known-failures --arch aarch64/a64fx --easystack eessi-2026.06-eb-5.4.1-2026.1.yml --easyconfigs easybuild-easyconfigs/easybuild/easyconfigs\n eb-stack stack known-failures --easystack eessi-2025.06-eb-5.4.0-2025b.yml --installed installed-zen4.txt --easyconfigs easybuild-easyconfigs/easybuild/easyconfigs"
1278+
after_long_help = "Examples:\n eb-stack stack known-failures --arch aarch64/a64fx --easystack eessi-2026.06-eb-5.4.1-2026.1.yml --easyconfigs easybuild-easyconfigs/easybuild/easyconfigs\n eb-stack stack known-failures --easystack eessi-2025.06-eb-5.4.0-2025b.yml --installed installed-zen4.txt --easyconfigs easybuild-easyconfigs/easybuild/easyconfigs\n eb-stack stack known-failures --strict --arch aarch64/a64fx --easystack eessi-2026.06-eb-5.4.1-2026.1.yml --easyconfigs easybuild-easyconfigs/easybuild/easyconfigs"
12771279
)]
12781280
KnownFailures {
12791281
/// Target, `aarch64/a64fx`, or family, `aarch64`. Repeatable.
@@ -1302,6 +1304,10 @@ enum StackCommand {
13021304
/// easystack file name.
13031305
#[arg(long)]
13041306
easybuild_version: Option<String>,
1307+
/// Exit 1 when a warning remains after the report. Remedied hits
1308+
/// never count. Without it the command exits 0 either way.
1309+
#[arg(long)]
1310+
strict: bool,
13051311
},
13061312
/// Warn about plan entries likely to run out of memory in a bot job.
13071313
///
@@ -2896,6 +2902,7 @@ fn run_stack(command: StackCommand) -> Result<()> {
28962902
known_failures,
28972903
eessi_version,
28982904
easybuild_version,
2905+
strict,
28992906
} => {
29002907
let report = eb_stack::arch_known_failures::run_known_failures(
29012908
&eb_stack::arch_known_failures::KnownFailuresRequest {
@@ -2909,6 +2916,11 @@ fn run_stack(command: StackCommand) -> Result<()> {
29092916
},
29102917
)?;
29112918
print!("{}", report.stdout);
2919+
if strict {
2920+
if let Some(error) = report.strict_error() {
2921+
bail!("{error}");
2922+
}
2923+
}
29122924
Ok(())
29132925
}
29142926
StackCommand::Oom {

‎src/mcp.rs‎

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -471,7 +471,7 @@ fn tool_catalog() -> Vec<Value> {
471471
),
472472
tool_with_optional(
473473
"eb_stack_known_failures",
474-
"Warn about known per-architecture failures a plan includes or depends on, as `stack known-failures` does. Each warning names the easyconfig, the architectures, the symptom, the established easyconfig remedy as text, and the EESSI hook that applies it. A pin or easyconfig that already carries the fix is reported as remedied. Warnings are not a tool error.",
474+
"Warn about known per-architecture failures a plan includes or depends on, as `stack known-failures` does. Each warning names the easyconfig, the architectures, the symptom, the established easyconfig remedy as text, and the EESSI hook that applies it. A pin or easyconfig that already carries the fix is reported as remedied. Warnings are not a tool error, except with strict: true, when a remaining warning returns a tool error whose text lists the warnings.",
475475
&[("easystack", "array_min1")],
476476
&[
477477
("arch", "array"),
@@ -480,6 +480,7 @@ fn tool_catalog() -> Vec<Value> {
480480
("known_failures", "string"),
481481
("eessi_version", "string"),
482482
("easybuild_version", "string"),
483+
("strict", "boolean"),
483484
],
484485
),
485486
tool_with_optional(
@@ -3211,6 +3212,11 @@ fn stack_known_failures_tool(arguments: &Value) -> Result<Value, String> {
32113212
},
32123213
)
32133214
.map_err(workflow_error)?;
3215+
if optional_bool(arguments, "strict") {
3216+
if let Some(error) = report.strict_error() {
3217+
return Err(error);
3218+
}
3219+
}
32143220
Ok(json!({
32153221
"text": report.stdout,
32163222
"hits": report.hits,

0 commit comments

Comments
 (0)