Skip to content

fix(ci): SHA-pin pages.yml actions and refresh stale standards reusable pins#293

Merged
hyperpolymath merged 2 commits into
mainfrom
ci/pin-and-refresh
Jul 21, 2026
Merged

fix(ci): SHA-pin pages.yml actions and refresh stale standards reusable pins#293
hyperpolymath merged 2 commits into
mainfrom
ci/pin-and-refresh

Conversation

@hyperpolymath

Copy link
Copy Markdown
Owner

Clears the last two red governance jobs. Follow-up to #292 — opened as a new PR off main per this repo's squash-merge convention (.claude/CLAUDE.md), not as extra commits on the merged branch.

Every fix was replicated locally against the real gate script before pushing.

Why these only surfaced now

governance has 10 jobs. BROKEN-M1 (allowed_actions: selected + empty patterns_allowed) made the whole workflow startup_failure with zero jobs, so all 10 were invisible. With the policy set to all, 8 report green and these 2 report honestly. #292 then cleared the SPDX arm and the retired-workflow rule, revealing what sat behind them.

1 · Workflow security linter — action pinning

pages.yml held the only 4 tag-ref uses: in the repo (out of 66). It had never actually run — its one and only run was the BROKEN-M1 startup_failure — so no linter had ever inspected it.

Action Pinned to Change
actions/checkout de0fac2e (v6.0.2) v4 → v6.0.2, matching the pin 41 other workflows here already use
actions/upload-pages-artifact 56afc609 (v3.0.1) pin-only — exactly what @v3 resolves to today
actions/deploy-pages d6db9016 (v4.0.5) pin-only — exactly what @v4 resolves to today

Two are pure pins with no behaviour change. checkout moves for repo consistency; worth flagging that pages.yml is the only container job here, so this is the first time that pin runs inside idris2-pack.

Preconditions verified before pinning a never-run workflow green: hyperpolymath/ddraig-ssg is public with Ddraig.idr at root, and Pages is build_type=workflow on boj-server.net.

2 · Check Workflow Staleness — reusable pin recency

The gate allows ≤50 commits behind standards HEAD OR ≤14 days old. d135b05 was 75 commits / 26 days behind — outside on both arms. Refreshed to 8813ecf.

Scoped to exactly the two reusables the gate names. check-workflow-staleness.sh inspects only governance-reusable, hypatia-scan-reusable and scorecard-reusable — which is why mirror.yml sits at the same d135b05 and was not flagged. It is left alone deliberately rather than swept along.

Evidence, not assumption

governance-reusable — identical 10 jobs before and after, so no new required check appears. The three gates whose behaviour changes were each run locally against this tree:

Gate Result
Guix primary / Nix fallback PASSguix.scm present, so the Nix fallback (and the absent flake.nix) is moot
Code quality + docs PASS — README + LICENSE + CONTRIBUTING all present
banned languages (+ new Groovy) 0 tracked .groovy/.gradle files

Both gates fetch their scripts from standards@main rather than from the pin, so these are the scripts CI will actually run.

scorecard-reusable — the entire d135b05..8813ecf diff is an SPDX correction (PMPL-1.0-or-laterMPL-2.0) plus timeout-minutes: 15. Called out explicitly because scorecard.yml has no pull_request trigger, so this PR's checks do not exercise it. The diff is the evidence; its first real run is the next push to main.

Local verification

  • check-workflow-staleness.shexit 0
  • 66/66 uses: SHA-pinned
  • 25/25 workflows carry SPDX + top-level permissions:
  • all 25 parse as YAML

Not in scope

SonarQube and Hypatia fail independently of these changes; triaged separately.

🤖 Generated with Claude Code

Follow-up to #292, which greened the SPDX arm of `Workflow security
linter` and removed the retired scorecard-enforcer.yml. Clearing those
revealed the two remaining governance failures. Opened as a new PR off
main per the repo's squash-merge convention (.claude/CLAUDE.md).

1. `Workflow security linter` — action pinning

pages.yml held the only 4 tag-ref `uses:` in the repo (of 66). It had
never actually run -- its single run ever was the BROKEN-M1
startup_failure -- so no linter had ever seen it.

  actions/checkout            -> de0fac2e (v6.0.2), the pin 41 other
                                 workflows here already use
  actions/upload-pages-artifact -> 56afc609 (v3.0.1), = current @V3
  actions/deploy-pages          -> d6db9016 (v4.0.5), = current @v4

The latter two are pin-only: those SHAs are exactly what the tags resolve
to today. checkout moves v4 -> v6.0.2 for repo consistency; pages.yml is
the only container job here, so this is the first time that pin runs
inside idris2-pack. Preconditions checked before pinning it green:
ddraig-ssg is public with Ddraig.idr at root, and Pages is
build_type=workflow on boj-server.net.

2. `Check Workflow Staleness` — reusable pin recency

The gate allows <=50 commits behind standards HEAD OR <=14 days old;
d135b05 was 75 commits / 26 days behind, outside on both arms. Refreshed
to 8813ecf, scoped to exactly the two reusables the gate names --
check-workflow-staleness.sh inspects only governance-reusable,
hypatia-scan-reusable and scorecard-reusable, which is why mirror.yml
sits at the same d135b05 and was NOT flagged. Left alone deliberately.

Measured before moving:

  * governance-reusable: same 10 jobs before and after, so no new
    required check appears. The three gates that change behaviour were
    each run locally against this tree --
      Guix primary / Nix fallback  -> PASS (guix.scm present; the Nix
                                     fallback was never required, so the
                                     absent flake.nix is a non-issue)
      Code quality + docs          -> PASS (README + LICENSE + CONTRIBUTING)
      banned languages + Groovy    -> 0 tracked .groovy/.gradle files
    Both fetch their scripts from standards@main rather than from the
    pin, so these are the scripts CI will actually run.

  * scorecard-reusable: the whole d135b05..8813ecf diff is an SPDX
    correction (PMPL-1.0-or-later -> MPL-2.0) plus timeout-minutes: 15.
    Worth stating because scorecard.yml has no `pull_request` trigger,
    so this PR's checks do not exercise it; the diff is the evidence,
    not a green tick. Its first real run is the next push to main.

Local verification against the real gate scripts:
  check-workflow-staleness.sh   -> exit 0
  66/66 `uses:` SHA-pinned, 25/25 workflows carry SPDX + top-level
  permissions, all 25 parse as YAML.
`Deploy to Cloudflare Workers` fails on main with:

  The action actions/checkout@v4 is not allowed in hyperpolymath/boj-server
  because all actions must be pinned to a full-length commit SHA.

This is GitHub's own `sha_pinning_required: true` repo policy, which was
deliberately preserved when allowed_actions moved selected -> all. Unlike
BROKEN-M1 it is a *correct* refusal: the action really was unpinned.

Worth recording how it was found, because two linters missed it. Both the
`Workflow security linter` and my local replication of it match `uses:`
only at the start of a line (after indentation), so both see

    - name: Checkout
      uses: actions/checkout@v4          # caught

but not the inline list-item form actually used here:

    - uses: actions/checkout@v4          # MISSED

That single blind spot is why governance reported exactly 4 unpinned
actions (all in pages.yml) while the repo in fact had 5, and why the
scan undercounted `uses:` lines 66 vs the true 94. GitHub's policy engine
caught what the linter could not. The upstream pattern in
hyperpolymath/standards has the same gap and should be widened
separately; this commit fixes the instance.

Pinned to de0fac2e (v6.0.2), the same checkout every other workflow in
this repo now uses.

Verified with a corrected pattern matching BOTH forms: 0 of 94 `uses:`
lines are unpinned.

Not addressed here (no CI impact, flagged for follow-up): the deploy step
runs `npx wrangler@latest`, which is an unpinned floating dependency in a
job that holds CLOUDFLARE_API_TOKEN.
@hyperpolymath
hyperpolymath marked this pull request as ready for review July 21, 2026 13:06
@hyperpolymath
hyperpolymath merged commit a74d33d into main Jul 21, 2026
25 of 26 checks passed
@hyperpolymath
hyperpolymath deleted the ci/pin-and-refresh branch July 21, 2026 13:06
@github-actions

Copy link
Copy Markdown

🔍 Hypatia Security Scan

Findings: 220 issues detected

Severity Count
🔴 Critical 15
🟠 High 134
🟡 Medium 71

⚠️ Action Required: Critical security issues found!

View findings
[
  {
    "reason": "Issue in build.yml",
    "type": "missing_timeout_minutes",
    "file": "build.yml",
    "action": "flag",
    "rule_module": "workflow_audit",
    "severity": "medium"
  },
  {
    "reason": "Issue in pages-deploy.yml",
    "type": "missing_timeout_minutes",
    "file": "pages-deploy.yml",
    "action": "flag",
    "rule_module": "workflow_audit",
    "severity": "medium"
  },
  {
    "reason": "Issue in push-email-notify.yml",
    "type": "missing_timeout_minutes",
    "file": "push-email-notify.yml",
    "action": "flag",
    "rule_module": "workflow_audit",
    "severity": "medium"
  },
  {
    "reason": "Issue in instant-sync.yml",
    "type": "secret_action_without_presence_gate",
    "file": "instant-sync.yml",
    "action": "peter-evans/repository-dispatch",
    "rule_module": "workflow_audit",
    "severity": "high"
  },
  {
    "reason": "Issue in codeql.yml",
    "type": "codeql_missing_actions_language",
    "file": "codeql.yml",
    "action": "flag",
    "rule_module": "workflow_audit",
    "severity": "medium"
  },
  {
    "reason": "TypeScript file detected -- banned language",
    "type": "banned_language_file",
    "file": "/home/runner/work/boj-server/boj-server/cartridges/bofig-mcp/adapter/mod.ts",
    "action": "flag",
    "rule_module": "cicd_rules",
    "severity": "critical"
  },
  {
    "reason": "TypeScript file detected -- banned language",
    "type": "banned_language_file",
    "file": "/home/runner/work/boj-server/boj-server/cartridges/hesiod-mcp/adapter/mod.ts",
    "action": "flag",
    "rule_module": "cicd_rules",
    "severity": "critical"
  },
  {
    "reason": "TypeScript file detected -- banned language",
    "type": "banned_language_file",
    "file": "/home/runner/work/boj-server/boj-server/cartridges/academic-workflow-mcp/adapter/mod.ts",
    "action": "flag",
    "rule_module": "cicd_rules",
    "severity": "critical"
  },
  {
    "reason": "TypeScript file detected -- banned language",
    "type": "banned_language_file",
    "file": "/home/runner/work/boj-server/boj-server/cartridges/sanctify-mcp/adapter/mod.ts",
    "action": "flag",
    "rule_module": "cicd_rules",
    "severity": "critical"
  },
  {
    "reason": "TypeScript file detected -- banned language",
    "type": "banned_language_file",
    "file": "/home/runner/work/boj-server/boj-server/cartridges/fireflag-mcp/adapter/mod.ts",
    "action": "flag",
    "rule_module": "cicd_rules",
    "severity": "critical"
  }
]

Powered by Hypatia Neurosymbolic CI/CD Intelligence

hyperpolymath added a commit that referenced this pull request Jul 21, 2026
…p_failure) (#294)

Two independent fixes, both **measured on `main`** after #293 landed.

## 1 · `Scorecards supply-chain security` — the last `startup_failure`

Re-pinning `scorecard-reusable` in #293 did **not** fix it. That is the
useful result: it proves the stale pin was never the cause.

The real cause is the caller's permission block:

```yaml
jobs:
  analysis:
    permissions:
      security-events: write
      id-token: write        # <- `contents` omitted
```

Declaring `permissions:` at **job** level sets every *unlisted* scope to
`none`, and a called workflow can never hold more than the calling job
grants it. `scorecard-reusable` declares top-level `permissions:
contents: read` and its first step is `actions/checkout` — so it was
being handed **`contents: none`**. The call died at startup with zero
jobs, which is exactly why there is no log to read and why it looked
inscrutable.

**Confirmed against the green sibling.** `boj-server-cartridges`'
`scorecard.yml` lists all three — `contents: read`, `security-events:
write`, `id-token: write` — and its runs succeed. This one omitted the
first.

This is root cause **(A)** of the estate's two `startup_failure` shapes
— *reusable permission escalation* — and is unrelated to BROKEN-M1,
which was the other.

## 2 · Revert `pages.yml` / `pages-deploy.yml` checkout to the v4 SHA

#293 pinned these to `de0fac2e` (v6.0.2). On reflection that shipped an
unvalidated version change:

- Both workflows are **`push: [main]`-only** — no `pull_request` — so no
PR can exercise them. The bump's first execution is on `main`.
- `pages.yml` is this repo's **only container job** (`idris2-pack`) and
has **never completed a run**. checkout v6 requires node24 injected into
the container; whether that works against this image is precisely what
could not be observed.

`11d5960a` is exactly what `actions/checkout@v4` resolves to today, so
this is a **pure pin** — the linter only requires a full-length SHA, not
a particular version. A considered v4 → v6 bump belongs in a change
where it can be watched.

## Verification

- 0 of 94 `uses:` unpinned (scanning **both** the bare and `- uses:`
forms — the latter is invisible to the standards linter)
- all three files parse as YAML

## State of `main` after #293

`Governance` is **SUCCESS** — all 9 jobs green, the two that were red
are fixed. Remaining red on `main`:

| Check | Cause | Fixed by |
|---|---|---|
| `Scorecards supply-chain security` | `contents: none` to the called
workflow | **this PR** |
| `deploy` (Cloudflare) | `CLOUDFLARE_API_TOKEN` /
`CLOUDFLARE_ACCOUNT_ID` **absent from repo secrets** — confirmed in the
log: *"necessary to set a CLOUDFLARE_API_TOKEN environment variable"* |
needs a human: add secrets or retire the workflow |
| `SonarQube` | `SONAR_TOKEN` exists but `HTTP 403`; the SonarCloud
project itself is fine (badge endpoint 200), so the token is
expired/wrong-scope | needs a human: regenerate in SonarCloud |

🤖 Generated with [Claude Code](https://claude.com/claude-code)
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