#88)
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.yml` → **0
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](https://claude.com/claude-code)
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Closes the two remaining red workflows on
mainand removes a stray licencefile. Three independent defects, each root-caused against the live runs.
1.
comprehensive-quality.yml— duplicate top-level key (parse-dead)The file declared
permissions: read-alltwice, at lines 4 and 12. GitHubActions rejects a workflow with duplicate top-level keys, so it never parsed.
The tell: the run reported as the raw path
.github/workflows/comprehensive-quality.ymlinstead of its
name:(Comprehensive Quality Gates) — GitHub never read farenough to find the name — with zero jobs and
log not found.Removed the second occurrence. Both were
read-all, so no semantic change.2.
scorecard.yml— reusable permission escalation (startup_failure)A job-level
permissions:block replaces the default grant rather thanextending it. Granting only
security-events: write+id-token: writethereforeleft
contentsat none.The reusable (
standards/.github/workflows/scorecard-reusable.yml@81dbf2dd)declares
permissions: contents: readatworkflow_calllevel, and a reusablemay not request more permission than its caller holds — so the job never started.
Added
contents: read, with an inline comment so it doesn't regress.This is the estate-wide pattern tracked in standards#527. Note that #75 aimed at
this symptom and did not clear it; this addresses the actual grant.
3.
LICENSES/AGPL-3.0-or-later.txt— removedNothing in the repo declares
AGPL-3.0. The canonical pair isMPL-2.0 + CC-BY-SA-4.0, normalised deliberately in #69, and every manifest
agrees (
Cargo.toml,codemeta.json,CITATION.cff;LICENSEis the MPL-2.0text). The repo's own machine-readable policy is explicit:
Sweep residue from
b9aaa6b, landed onmainvia #83.Verification
comprehensive-quality.ymlparses toComprehensive Quality Gates, 11 jobs.scorecard.ymljobanalysisresolves to{contents: read, security-events: write, id-token: write}.Out of scope, but found — worth a follow-up
RSR_COMPLIANCE.adoc:56andRSR_OUTLINE.adoc:74,162describe the licensingas "LICENSE.txt (AGPL + Palimpsest)". That contradicts the repo's actual
MPL-2.0 licensing and its own no-AGPL policy, and
LICENSE.txtdoes notexist (the file is
LICENSE). Looks like unedited RSR template boilerplate.Left alone deliberately — rewriting compliance docs is a judgement call.
pages.yml,casket-pages.yml,jekyll-gh-pages.yml)all deploy on push to
main— three deployers racing for one environment.Draft pending CI. Context:
dev-notes/betlang-sitrep-2026-07-27.md.🤖 Generated with Claude Code