Skip to content

fix(ci): move hashFiles() out of job-level if: — the last red workflow - #88

Merged
hyperpolymath merged 1 commit into
mainfrom
fix/comprehensive-quality-hashfiles
Jul 27, 2026
Merged

fix(ci): move hashFiles() out of job-level if: — the last red workflow#88
hyperpolymath merged 1 commit into
mainfrom
fix/comprehensive-quality-hashfiles

Conversation

@hyperpolymath

Copy link
Copy Markdown
Owner

The final red workflow on main. Everything else is green as of 29abb4c.

Why #87 did not finish the job

#87 fixed a genuine defect — a duplicate top-level permissions: key in
comprehensive-quality.yml — but that was necessary, not sufficient. The file
was rejected at parse time for a second, independent reason, so it still
produced runs with zero jobs and no check run afterwards.

(#87 was squash-merged just before the follow-up commit landed on its branch,
so that second fix never reached main. This PR carries it.)

The remaining defect

.github/workflows/comprehensive-quality.yml:156:9: calling function "hashFiles"
is not allowed here. "hashFiles" is only available in "jobs.<job_id>.steps.*"
contexts

hashFiles() sat in a job-level if:. That is not a permitted context and
it makes Actions reject the entire file — which is why none of the 11 jobs
ever materialised, and why the workflow still shows in the UI as the raw path
.github/workflows/comprehensive-quality.yml rather than its name
(Comprehensive Quality Gates): GitHub has never parsed far enough to read it.

It was also meaningless where it was — at job level nothing is checked out yet,
so the glob could never match. The guard now sits on the two steps that need it,
evaluating after checkout, which is both valid and actually correct.

Verification

actionlint .github/workflows/comprehensive-quality.yml0 findings
(was 1 fatal expression error). Across all workflows, the only remaining
actionlint output is shellcheck style/info in unrelated files.

Expect this run to produce 11 jobs. Job count is the discriminator: zero
jobs means still parse-dead, non-zero means it parses and any failure is a real
quality-gate result to triage on its merits.

Method note worth keeping

A strict YAML duplicate-key scan is not sufficient to prove an Actions
workflow parses — YAML validity and Actions validity are different things, and
my earlier YAML-only check passed this file while GitHub still rejected it.
actionlint models the real rules and catches this class immediately. Given the
estate tracks ~187 unparseable workflow files, it is the right instrument for
that sweep.

Context: dev-notes/betlang-sitrep-2026-07-27.md.

🤖 Generated with Claude Code

The duplicate `permissions:` key was necessary to fix but NOT sufficient:
comprehensive-quality.yml still produced a run with ZERO jobs and no check
run on the previous commit, i.e. it was still rejected at parse time.

Second, independent defect, confirmed with actionlint:

  .github/workflows/comprehensive-quality.yml:156:9: calling function
  "hashFiles" is not allowed here. "hashFiles" is only available in
  "jobs.<job_id>.steps.*" contexts

`hashFiles()` in a job-level `if:` makes Actions reject the WHOLE FILE, so
none of the 11 jobs ever materialised. It was also semantically pointless
there: at job level nothing is checked out yet, so the glob could never
match.

Moved the guard onto the two steps that actually need it, where it runs
after checkout and is a valid context.

Verified: `actionlint .github/workflows/*.yml` now reports zero expression
or syntax errors across all 19 files; the only remaining output is
shellcheck style/info (SC2086, SC2129, SC2035, ...) in unrelated
workflows.

Lesson for the estate: a strict YAML duplicate-key scan is NOT sufficient
to prove an Actions workflow parses. YAML validity and Actions validity
are different things — use actionlint.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@hyperpolymath
hyperpolymath merged commit 704922c into main Jul 27, 2026
38 of 42 checks passed
@hyperpolymath
hyperpolymath deleted the fix/comprehensive-quality-hashfiles branch July 27, 2026 14:07
hyperpolymath added a commit that referenced this pull request Aug 3, 2026
Follow-up to #88. Making `comprehensive-quality.yml` parse turned 0 jobs
into
11 — and two of those jobs then failed **for real**. They are genuine
defects in
gates that had never once executed, not regressions from #88.

Both are still present on `main` today.

## 1. `license` — the glob matched a directory

`head -5 LICENSE*` also matches the **`LICENSES/` directory**. `head`
exits 1 on
a directory, and `2>/dev/null` suppresses the message but **not the exit
code**,
so the step died under `bash -e` immediately after printing "License
file present".

Reproduced locally:

```console
$ bash -e -c 'head -5 LICENSE* 2>/dev/null'; echo "exit=$?"
==> LICENSE <== ... ==> LICENSES <==
exit=1
```

Same family as the `target/release/*` trap: a glob silently matching a
directory.

**A trap inside the fix, worth recording.** The obvious repair —
`[ -f "$f" ] && head -5 "$f"` in a loop — is *also* broken: as the last
statement
in the loop body, a false test makes the whole step exit 1 under `bash
-e`. I hit
exactly that and only caught it because I ran the form before pushing:

```console
$ bash -e -c 'for f in LICENSE LICENSE.txt LICENSE.md; do [ -f "$f" ] && head -5 "$f"; done'; echo "exit=$?"
exit=1          # <- still broken
$ bash -e -c 'for f in LICENSE LICENSE.txt LICENSE.md; do if [ -f "$f" ]; then head -5 "$f"; fi; done'; echo "exit=$?"
exit=0          # <- correct
```

Shipped with the explicit `if`.

## 2. `security` — the semgrep pin did not exist

```
##[error]Unable to resolve action `returntocorp/semgrep-action@73f32468...`,
unable to find version `73f32468...`
```

The repo **is alive** (last push 2024-01-22), so this was an **invented
SHA**, not
a dead action repo — the same class as the estate's ~80 invented pins.

Re-pinned to `v1` = `713efdd345f3035192eaa63f56867b88e63e4e5d`, taken
from the
tags API and confirmed resolvable via `repos/.../commits/<sha>`. I did
not guess.

**Worth recording:** an unresolvable `uses:` ref fails during action
**resolution**, before the step body runs — so the `continue-on-error:
true` on
that step could not and did not rescue it. `continue-on-error` never
protects
against a bad pin.

**Caveat, deliberately not hidden:** this action is deprecated (last
push
2024-01) and the step is non-blocking, so SAST here is advisory only.
Fixing the
pin makes it *resolve*; it does not make it a real gate. Replacing it
with a
maintained scanner is a separate call and I have not made it for you.

## Verification

- `actionlint` → **0 findings** on the file.
- Both shell forms exercised locally under `bash -e` (outputs above).
- New semgrep SHA confirmed to resolve via the API.

## Note on scope

`Test on Racket current` also failed on #88's branch (30m14s, a hang).
That is
`test.yml`, untouched here, and it passes consistently on `main` — so it
is not
caused by this change. Flagging it, not fixing it blind.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant