Skip to content

Commit 56f32c6

Browse files
authored
Merge pull request #134 from ScriptKittyOS/fix/fips-coverage-step
fix(s003): the fips coverage step fails on a red suite
2 parents 06da183 + a050ac6 commit 56f32c6

2 files changed

Lines changed: 51 additions & 2 deletions

File tree

‎.github/workflows/gate.yml‎

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -371,10 +371,16 @@ jobs:
371371
# AC2 and AC3 by test name with this run's id in the log, and AC5's coverage line for
372372
# this leg. The gate above already ran the suite; this repeats the four FIPS tests with
373373
# their names printed, then the whole suite once more under the coverage tool.
374+
#
375+
# Unpiped since the fix after slice 138. It read `mix test --cover | grep -E 'Result:|Total'`,
376+
# and the runner's default `bash -e {0}` has no pipefail, so the step exited with grep's status:
377+
# on job 118381934625 the suite here was 1388/1389 and the step passed, with the failing test's
378+
# name filtered out. The coverage threshold is 0 (mix.exs), so the suite alone decides the
379+
# exit, and the summary's Total line is in the output. test/workflows_test.exs holds the rule.
374380
- name: The FIPS tests by name, and this leg's coverage
375381
run: |
376382
mix test --trace test/fips
377-
mix test --cover | grep -E 'Result:|\| *Total'
383+
mix test --cover
378384
379385
# Slice 137, AC5 and AC7: ML-DSA-87 with the mode off. This image links OpenSSL 3.5, the only
380386
# runtime in CI that signs ML-DSA-87 (the gate runners link 3.0; in the mode, Red Hat's

‎test/workflows_test.exs‎

Lines changed: 44 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ defmodule WorkflowsTest do
44
@moduledoc """
55
The continuous integration definitions are code, and they are checked like code.
66
7-
Three properties. Every workflow and the Dependabot configuration parse as YAML, so a malformed
7+
Four properties. Every workflow and the Dependabot configuration parse as YAML, so a malformed
88
one fails here rather than silently not running: GitHub reports a broken workflow file on its
99
own page and not on the pull request, which is the wrong place for it to be discovered. Every
1010
third-party action is pinned to a full commit hash rather than a tag, because a tag can be moved
@@ -13,6 +13,8 @@ defmodule WorkflowsTest do
1313
block granting no more than read, so that a job wanting to write has to say so itself: Scorecard's
1414
`Token-Permissions` check, which this repository scored **zero** on until 2026-09-23 because two
1515
workflows declared nothing at all and ran with whatever the repository default happened to be.
16+
And no step pipes the test suite or the gate into another command without `pipefail`, because the
17+
runner's default shell takes the last command's exit, so a red suite piped into `grep` passes.
1618
1719
The third property is the one worth a sentence about why a test rather than a habit. A top-level
1820
write is granted to every job the workflow will ever have, including the ones nobody has written
@@ -83,6 +85,47 @@ defmodule WorkflowsTest do
8385
end
8486
end
8587

88+
# Added after slice 138 (its NOTES, follow-up): the fips job's coverage step ran
89+
# `mix test --cover | grep ...` under the runner's default `bash -e {0}`, which has no pipefail,
90+
# so the step's exit was grep's. On job 118381934625 the suite inside it was 1388/1389 and the
91+
# step and the job passed, with the failing test's name filtered out of the log. A step that runs
92+
# the suite or the gate must fail when they do.
93+
test "no step pipes the test suite or the gate into another command without pipefail" do
94+
offenders =
95+
for file <- yaml_files(),
96+
String.contains?(file, "/workflows/"),
97+
workflow = read_yaml(file),
98+
{job_id, job} <- Map.get(workflow, "jobs", %{}),
99+
{step, index} <- Enum.with_index(Map.get(job, "steps", [])),
100+
run = Map.get(step, "run"),
101+
is_binary(run),
102+
pipes_suite?(run),
103+
not pipefail?(step, job, workflow, run) do
104+
"#{file}: job #{job_id}, step #{Map.get(step, "name", "##{index}")}"
105+
end
106+
107+
assert offenders == [],
108+
"these steps pipe `mix test` or `mix gate` into another command, so the step exits " <>
109+
"with that command's status and a red suite passes. Run them unpiped, or give the " <>
110+
"step `shell: bash` (which adds -o pipefail):\n" <> Enum.join(offenders, "\n")
111+
end
112+
113+
# A line that runs `mix test` or `mix gate` and has a single `|` after it (not `||`).
114+
defp pipes_suite?(run) do
115+
run
116+
|> String.split("\n")
117+
|> Enum.any?(&Regex.match?(~r/\bmix\s+(test|gate)\b[^|]*\|(?!\|)/, &1))
118+
end
119+
120+
defp pipefail?(step, job, workflow, run) do
121+
shell =
122+
Map.get(step, "shell") || get_in(job, ["defaults", "run", "shell"]) ||
123+
get_in(workflow, ["defaults", "run", "shell"])
124+
125+
shell == "bash" or String.contains?(run, "set -o pipefail") or
126+
String.contains?(run, "set -eo pipefail")
127+
end
128+
86129
# `permissions:` is either a map of scopes, or one of the shorthands `read-all`/`write-all`.
87130
defp top_level_scopes("read-all"), do: []
88131
defp top_level_scopes("write-all"), do: [{"(every scope)", "write"}]

0 commit comments

Comments
 (0)