Skip to content

fix(ci): audit the whole call tree in the no-caching gate - #861

Merged
tobyhede merged 3 commits into
mainfrom
feat/protect-ffi-2-supply-chain-gate
Aug 6, 2026
Merged

fix(ci): audit the whole call tree in the no-caching gate#861
tobyhede merged 3 commits into
mainfrom
feat/protect-ffi-2-supply-chain-gate

Conversation

@tobyhede

@tobyhede tobyhede commented Aug 6, 2026

Copy link
Copy Markdown
Contributor

Stack 2 of 4 — splitting #858. Base: #860.

PR What it does
1 #860 Consume the published protect-ffi 0.31.0
2 this one Close three fail-open holes in the no-caching gate
3 Vendor packages/protect-ffi (subtree, upstream history preserved)
4 Link consumers to the workspace copy

What

scripts/lint-no-workflow-caching.mjs keeps the GitHub Actions cache out of the two credential-bearing workflows, because a poisoned cache entry would execute in a job that can publish to npm. It had three fail-open holes, each of which printed OK:

  1. It stopped at a local composite action. Reproduced against a copy of release.yml with a cache-restoring composite spliced in: exit 0, no output, while the composite it never opened restored two caches.
  2. It skipped a job that delegates to a reusable workflow. Such a job has no steps: at all, so the walker got an empty list and skipped the job entire. Confirmed before the fix: exit 0, OK, nothing scanned. The verdict ignores secrets: deliberately — permissions: is inherited independently and is what mints the OIDC token npm trusted publishing signs with.
  3. A third-party cache action was invisible. useblacksmith/cache@v5 and Swatinem/rust-cache@v2 both passed. Fixed by inverting to an allowlist rather than lengthening a denylist: a denylist fails open on the action nobody has met yet, and cannot cover the class most likely to be added by accident — a setup-<tool> action that caches by default, with no cache: input and no telling name.

Also adds the missing premise assertion to integration-workflow-paths: its requirement set is derived from @/-aliased imports, so "no suite uses that alias" and "every import is covered" were the same green. Mutation-tested — rewriting the suites onto the public entry empties the set, and the check then passed with packages/stack/src/dynamodb/** deleted from the filter, verbatim the #815 gap.

Why it is separable

These are pre-existing holes in a control that already shipped. Nothing here depends on the vendoring.

Verification

Scripts suite 211 passing (51 in the two touched files); all three lint gates OK; biome 0 errors.

Summary by CodeRabbit

  • New Features

    • Workflow caching checks now follow local composite actions and reusable workflows, including nested and cyclic references.
    • Published actions must be included in the audited-action allowlist; unknown actions are rejected by default.
    • The linter now detects broader caching scenarios and reports unresolved workflow references.
  • Bug Fixes

    • Improved handling of action manifests, workflow paths, whitespace, malformed files, and false positives involving inputs.
  • Documentation

    • Updated release guidance to describe recursive workflow auditing and allowlist requirements.

`scripts/lint-no-workflow-caching.mjs` keeps the GitHub Actions cache out of
the two credential-bearing workflows, because a poisoned cache entry would
execute in a job that can publish to npm. It had three fail-open holes, each
of which printed `OK`.

**1. It stopped at a local composite action.** The gate read a step's own
`uses:` and went no further, so a workflow could reach `actions/cache` through
one indirection — `uses: ./.github/actions/x` — and stay green. Reproduced
against a copy of release.yml with a cache-restoring composite spliced in:
exit 0, no output, while the composite it never opened restored two caches.
It now flattens local composites and checks every step inside them, naming the
whole trail so the report points at the file the cache is actually in.

**2. It skipped a job that delegates to a reusable workflow.** Such a job has
no `steps:` at all — it is `jobs.<id>.uses` — so the walker was handed an empty
list and skipped the job entire. Confirmed before the fix against a caller
whose only job was `uses: ./.github/workflows/reusable.yml` with
`secrets: inherit`, the called workflow holding `actions/cache@v4`: exit 0,
`OK`, nothing scanned. The verdict deliberately ignores `secrets:` —
`permissions:` is inherited independently and is what mints the OIDC token npm
trusted publishing signs with, so a call passing no secrets can still publish.

**3. A third-party cache action was invisible.** The rules only recognised an
action literally named `actions/cache*` and one taking a `cache:` input, so
`useblacksmith/cache@v5` and `Swatinem/rust-cache@v2` both passed. The repair
is an inversion rather than a longer denylist: every REMOTE `uses:` reachable
from a targeted workflow must now appear in an `AUDITED_ACTIONS` allowlist, so
an action this gate has never seen is a finding by default whatever it is
called. A denylist fails open on the action nobody has met yet — silently
correct until the day it is silently wrong, and wrong in the direction that
prints `OK`. It also cannot cover the class most likely to be added by
accident: a `setup-<tool>` action that caches BY DEFAULT, with no `cache:`
input to inspect and no "cache" in its name.

The allowlist cannot go stale silently, which is why it was chosen: its
staleness is a build failure naming the exact action and the file it was added
to, so the person adding it is the person told to audit it, in the same PR.
Cost was measured rather than assumed — the two targeted workflows reach four
actions between them.

Also adds the missing premise assertion to `integration-workflow-paths`: its
requirement set is DERIVED from `@/`-aliased imports, so "no suite uses that
alias" and "every import is covered" were the same green. Mutation-tested —
rewriting the suites onto the public entry empties the set, and the check then
passed with `packages/stack/src/dynamodb/**` deleted from the filter, which is
verbatim the #815 gap the file exists to prevent.

Second of four stacked PRs splitting the protect-ffi absorption. Independent
of the vendoring: these are pre-existing holes in a control that already
shipped.
@tobyhede
tobyhede requested a review from a team as a code owner August 6, 2026 00:58
@changeset-bot

changeset-bot Bot commented Aug 6, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 7f819fb

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 11 packages
Name Type
stash Patch
@cipherstash/basic-example Patch
@cipherstash/e2e Patch
@cipherstash/stack Patch
@cipherstash/stack-drizzle Patch
@cipherstash/stack-supabase Patch
@cipherstash/stack-prisma Patch
@cipherstash/wizard Patch
@cipherstash/bench Patch
@cipherstash/test-kit Patch
@cipherstash/prisma-example Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@coderabbitai

coderabbitai Bot commented Aug 6, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: e47f1baf-fe31-4063-8f2a-64421735bfc7

📥 Commits

Reviewing files that changed from the base of the PR and between c716e03 and 7f819fb.

📒 Files selected for processing (11)
  • scripts/__tests__/fixtures/lint-no-workflow-caching/composites/.github/actions/dir-action-yml/action.yml/.gitkeep
  • scripts/__tests__/fixtures/lint-no-workflow-caching/composites/.github/actions/malformed/action.yml
  • scripts/__tests__/fixtures/lint-no-workflow-caching/composites/.github/workflows/composite-dir-action-yml.yml
  • scripts/__tests__/fixtures/lint-no-workflow-caching/composites/.github/workflows/composite-malformed.yml
  • scripts/__tests__/fixtures/lint-no-workflow-caching/malformed.yml
  • scripts/__tests__/fixtures/lint-no-workflow-caching/parent-uses-step.yml
  • scripts/__tests__/fixtures/lint-no-workflow-caching/reusable/.github/workflows/called-malformed.yml
  • scripts/__tests__/fixtures/lint-no-workflow-caching/reusable/.github/workflows/reusable-malformed.yml
  • scripts/__tests__/fixtures/lint-no-workflow-caching/reusable/.github/workflows/reusable-parent-uses.yml
  • scripts/__tests__/lint-no-workflow-caching.test.mjs
  • scripts/lint-no-workflow-caching.mjs
🚧 Files skipped from review as they are similar to previous changes (1)
  • scripts/lint-no-workflow-caching.mjs

📝 Walkthrough

Walkthrough

The PR adds recursive cache and action auditing for local composite actions and reusable workflows. It adds workflow fixtures and tests for nested, cyclic, unresolved, remote, third-party, malformed, and unaudited references. It also updates release guidance and a setup action version.

Changes

Workflow audit

Layer / File(s) Summary
Lint parsing and audit enforcement
scripts/lint-no-workflow-caching.mjs
The linter normalizes uses: references, enforces the AUDITED_ACTIONS allowlist, resolves local references, aggregates findings, and prioritizes unresolved-reference exit code 2.
Composite action traversal
scripts/lint-no-workflow-caching.mjs, scripts/__tests__/fixtures/lint-no-workflow-caching/composites/*
The linter recursively checks composite actions, supports both manifest extensions, detects cycles and unresolved actions, and distinguishes cache inputs from cache operations.
Reusable workflow traversal
scripts/lint-no-workflow-caching.mjs, scripts/__tests__/fixtures/lint-no-workflow-caching/reusable/*
The linter recursively checks local reusable workflows, nested calls, workflow-to-composite calls, remote references, unresolved paths, malformed manifests, and workflow inputs.
Validation coverage and release guidance
scripts/__tests__/lint-no-workflow-caching.test.mjs, scripts/__tests__/integration-workflow-paths.test.mjs, skills/stash-supply-chain-security/SKILL.md, .changeset/olive-moons-shave.md, .github/actions/integration-setup/action.yml, scripts/__tests__/fixtures/lint-no-workflow-caching/*.yml
Tests cover traversal, cache detection, auditing, malformed input, unresolved references, and combined findings. Release guidance documents the updated requirements. The pnpm setup action changes to v6.0.9.

Estimated code review effort: 4 (Complex) | ~60 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Workflow
  participant LintScript
  participant LocalAction
  participant ReusableWorkflow

  Workflow->>LintScript: process jobs and uses references
  LintScript->>LocalAction: resolve and inspect local composite action
  LocalAction-->>LintScript: return nested steps
  LintScript->>ReusableWorkflow: resolve and inspect local workflow call
  ReusableWorkflow-->>LintScript: return nested jobs
  LintScript-->>Workflow: report findings and exit status
Loading

Possibly related PRs

Suggested reviewers: auxesis, coderdan

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main change: auditing the complete call tree in the no-caching CI gate.
Docstring Coverage ✅ Passed Docstring coverage is 91.67% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/protect-ffi-2-supply-chain-gate

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@freshtonic freshtonic left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Verdict: Approve. Verified locally: pnpm run test:scripts 295/295 pass, pnpm run lint:workflow-cache exits 0 on the real release.yml / tests-supply-chain.yml, and biome check on the three changed source files is clean. The core traversal is correct across every shape the fixtures exercise, the fail-closed allowlist is the right design, and the changeset + stash-supply-chain-security skill are both updated as AGENTS.md requires. No blocking issues.

Non-blocking

1. with.cache false-positive on a local-composite invocation. checkStep runs the with.cache: <truthy> rule on every step, including a step whose uses: is a local composite. For a composite, with: is arbitrary inputs — an input named cache is unrelated to the Actions cache. Reproduced against a composite with a cache input invoked as with: { cache: true }:

job "release" step "Call composite with cache input": `with.cache: true` restores the GitHub Actions cache

This is the same false-positive class you deliberately avoid one level up for reusable workflows — job-level with: is kept out of checkStep, and reusable-input-named-cache.yml pins that. It's currently inert (no target workflow reaches a composite) and errs in the fail-closed direction, so not blocking — but consider skipping the with.cache rule when the step is a local-composite uses:, or a one-line comment noting the asymmetry is intentional.

2. Exit-2 suppresses cache offenders on a mixed run. unresolved is printed and process.exit(2) fires before the offenders (exit 1) block. A run that collects both an un-auditable reference and a real cache offender prints only the former. Both fail CI, so it's cosmetic, but the actionable cache finding stays hidden until the path is fixed. Consider printing both lists before exiting with the higher code.

3. Minor scope. The pnpm/action-setup@v6.0.8 → v6.0.9 bump in .github/actions/integration-setup/action.yml is unrelated to the gate. Harmless, noted only for commit atomicity.

Highlights

  • Fail-closed allowlist with a thoroughly documented rationale (why a denylist was rejected; the setup-<tool>-caches-by-default class with no cache: input and no telling name).
  • The module-load assertion that no AUDITED_ACTIONS entry is cache-shaped makes the one careless re-opening edit impossible.
  • Fixtures are exhaustive and adversarial: composite and reusable cycles (asserted by offender count, not just exit), action.yaml vs .yml, leading-whitespace uses:, remote step vs remote reusable workflow, secrets:-agnostic verdict, invalid steps:+uses: job, and multiple third-party cache vendors.
  • The required.size > 0 premise assertion added to integration-workflow-paths.test.mjs closes a genuine "green because it checked nothing" gap, mirroring the #815 fix.

Base automatically changed from feat/protect-ffi-1-consume-0.31 to main August 6, 2026 01:42
…puts

Two review findings on the call-tree traversal.

**`with.cache` fired on a local composite's declared inputs.** `checkStep`
applied the `with.cache: <truthy>` rule to every step, including one whose
`uses:` is a local action — where `with:` is that action's arbitrary declared
inputs. A composite taking a `cache` input that decides whether to reuse a
binary already in the working tree, invoked `with: {cache: true}`, was reported
as "restores the GitHub Actions cache". It is the step-level twin of the false
positive `walkJob` already refuses to make by never running the step rules over
a job-level `with:`, which `reusable-input-named-cache.yml` pins.

The exemption is keyed on the resolved manifest's `runs.using` being
`composite`, not on the `uses:` starting with `./`, because its justification is
"the body is audited instead" rather than "local is trusted". A local `uses:`
resolving to nothing, or to a JS/Docker action with no step list, keeps the
rule: there the gate opens no step list, and a local `uses:` is already exempt
from AUDITED_ACTIONS, so the caller's `with:` is the only signal left — dropping
it for every local reference would make a two-line `action.yml` a supported way
past the gate. `walkSteps` now resolves the action before checking the step
rather than after, so one reading feeds both decisions and the suppression
cannot outlive the audit that justifies it.

`cache-passthrough` pins the fail-closed half: a composite forwarding its
`cache` input into `actions/setup-node` is still one finding, named on the step
inside the composite rather than on the caller that switched it on.

**Exit 2 suppressed the cache offenders on a mixed run.** The un-auditable list
printed and called `process.exit(2)` before the offender block was reached, so a
run collecting both showed only the reference the gate could not open. The
actionable finding — the one with a step to delete — stayed hidden until the path
was fixed, then arrived on the next run looking new. Both lists now print before
either exit. Exit 2 still outranks 1, but no longer because nothing was found
caching: on a mixed run something was. An incomplete scan is simply the more
severe verdict, since the exit 1 reports what this gate could see and the exit 2
says that list may be short.

Scripts suite 216 passing (49 in the touched file, +5); all three lint gates OK;
biome 0 errors.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (3)
scripts/__tests__/lint-no-workflow-caching.test.mjs (1)

168-176: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add a timeout to the run helper so a cycle regression fails instead of hanging.

The comment states the risk correctly. execFileSync at Line 22 has no timeout, so a regression in the visited guard blocks the suite until the CI job limit. A timeout converts that hang into a normal test failure with the existing exit-code assertion.

♻️ Proposed change to the shared run helper (Lines 20-30)
 function run(...targets) {
   try {
-    execFileSync('node', [SCRIPT, ...targets], { encoding: 'utf8' })
+    execFileSync('node', [SCRIPT, ...targets], {
+      encoding: 'utf8',
+      timeout: 30_000,
+    })
     return { exitCode: 0, output: '' }
   } catch (err) {
     return {
       exitCode: err.status,
       output: String(err.stdout) + String(err.stderr),
     }
   }
 }

Note: on a timeout err.status is null and err.signal is set, so the exit-code assertions still fail loudly rather than passing.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@scripts/__tests__/lint-no-workflow-caching.test.mjs` around lines 168 - 176,
Update the shared run helper around execFileSync to pass a finite timeout
option, ensuring cyclic-command regressions fail promptly instead of hanging.
Preserve the existing error handling and exit-code assertions so timeout
failures remain visible to the test.
scripts/lint-no-workflow-caching.mjs (2)

24-27: 🎯 Functional Correctness | 🔵 Trivial | 💤 Low value

LOCAL_USES accepts ../, but the comment states only ./ is valid.

GitHub resolves a local uses: only with the ./ prefix. ../ is invalid to GitHub. The regex allows it, so a ../… reference is treated as local and becomes exempt from AUDITED_ACTIONS. resolveActionFile then probes outside the workspace root. Align the pattern with the documented rule.

♻️ Restrict the pattern to `./`
-const LOCAL_USES = /^\.{1,2}\//
+const LOCAL_USES = /^\.\//
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@scripts/lint-no-workflow-caching.mjs` around lines 24 - 27, Update the
LOCAL_USES pattern in scripts/lint-no-workflow-caching.mjs to match only the ./
prefix, excluding ../ references. Preserve the surrounding local-action
detection and ensure resolveActionFile cannot treat paths outside the workspace
as local actions.

256-263: 🩺 Stability & Availability | 🔵 Trivial | 💤 Low value

Apply the same isFile guard used by resolveWorkflowFile.

resolveWorkflowFile rejects a directory to avoid an unhandled EISDIR at readFileSync. resolveActionFile does not. A directory named action.yml therefore aborts the run at line 317 instead of producing a report. The hazard is the one already documented at lines 344-347.

♻️ Add the file-type check
 function resolveActionFile(workspaceRoot, usesPath) {
   const dir = resolve(workspaceRoot, usesPath)
   for (const name of ['action.yml', 'action.yaml']) {
     const file = join(dir, name)
-    if (existsSync(file)) return file
+    if (existsSync(file) && statSync(file).isFile()) return file
   }
   return null
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@scripts/lint-no-workflow-caching.mjs` around lines 256 - 263, Update
resolveActionFile to apply the same isFile guard used by resolveWorkflowFile
before returning a matching action.yml or action.yaml path. Skip directories
with those names and return null when no regular action file exists, preventing
the later readFileSync path from receiving a directory.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@scripts/lint-no-workflow-caching.mjs`:
- Line 317: Guard all YAML file reads and parses in
scripts/lint-no-workflow-caching.mjs using a shared helper: at lines 317, 399,
and 444, catch failures, record an unresolved entry with the relevant manifest
or workflow path and parse error, and return a null result. At line 317, treat
null as a non-composite manifest so checkStep retains all rules; at lines 399
and 444, treat null as having no jobs and stop that traversal, while the target
workflow path at line 444 must allow scanning subsequent targets.

---

Nitpick comments:
In `@scripts/__tests__/lint-no-workflow-caching.test.mjs`:
- Around line 168-176: Update the shared run helper around execFileSync to pass
a finite timeout option, ensuring cyclic-command regressions fail promptly
instead of hanging. Preserve the existing error handling and exit-code
assertions so timeout failures remain visible to the test.

In `@scripts/lint-no-workflow-caching.mjs`:
- Around line 24-27: Update the LOCAL_USES pattern in
scripts/lint-no-workflow-caching.mjs to match only the ./ prefix, excluding ../
references. Preserve the surrounding local-action detection and ensure
resolveActionFile cannot treat paths outside the workspace as local actions.
- Around line 256-263: Update resolveActionFile to apply the same isFile guard
used by resolveWorkflowFile before returning a matching action.yml or
action.yaml path. Skip directories with those names and return null when no
regular action file exists, preventing the later readFileSync path from
receiving a directory.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: ca33f089-0b0f-4da6-8987-682de24de655

📥 Commits

Reviewing files that changed from the base of the PR and between a79bacc and c716e03.

📒 Files selected for processing (65)
  • .changeset/olive-moons-shave.md
  • .github/actions/integration-setup/action.yml
  • scripts/__tests__/fixtures/lint-no-workflow-caching/audited-actions.yml
  • scripts/__tests__/fixtures/lint-no-workflow-caching/cache-family.yml
  • scripts/__tests__/fixtures/lint-no-workflow-caching/composites/.github/actions/cache-passthrough/action.yml
  • scripts/__tests__/fixtures/lint-no-workflow-caching/composites/.github/actions/cachey-restore/action.yml
  • scripts/__tests__/fixtures/lint-no-workflow-caching/composites/.github/actions/cachey-save/action.yml
  • scripts/__tests__/fixtures/lint-no-workflow-caching/composites/.github/actions/cachey/action.yml
  • scripts/__tests__/fixtures/lint-no-workflow-caching/composites/.github/actions/clean-composite/action.yml
  • scripts/__tests__/fixtures/lint-no-workflow-caching/composites/.github/actions/input-named-cache/action.yml
  • scripts/__tests__/fixtures/lint-no-workflow-caching/composites/.github/actions/js-action/action.yml
  • scripts/__tests__/fixtures/lint-no-workflow-caching/composites/.github/actions/loop-a/action.yml
  • scripts/__tests__/fixtures/lint-no-workflow-caching/composites/.github/actions/loop-b/action.yml
  • scripts/__tests__/fixtures/lint-no-workflow-caching/composites/.github/actions/missing-explicit-false/action.yml
  • scripts/__tests__/fixtures/lint-no-workflow-caching/composites/.github/actions/outer/action.yml
  • scripts/__tests__/fixtures/lint-no-workflow-caching/composites/.github/actions/setup-node-cache/action.yml
  • scripts/__tests__/fixtures/lint-no-workflow-caching/composites/.github/actions/thirdparty-cache/action.yml
  • scripts/__tests__/fixtures/lint-no-workflow-caching/composites/.github/actions/yaml-ext/action.yaml
  • scripts/__tests__/fixtures/lint-no-workflow-caching/composites/.github/workflows/composite-cache-passthrough.yml
  • scripts/__tests__/fixtures/lint-no-workflow-caching/composites/.github/workflows/composite-cache-restore.yml
  • scripts/__tests__/fixtures/lint-no-workflow-caching/composites/.github/workflows/composite-cache-save.yml
  • scripts/__tests__/fixtures/lint-no-workflow-caching/composites/.github/workflows/composite-cache.yml
  • scripts/__tests__/fixtures/lint-no-workflow-caching/composites/.github/workflows/composite-clean.yml
  • scripts/__tests__/fixtures/lint-no-workflow-caching/composites/.github/workflows/composite-cyclic.yml
  • scripts/__tests__/fixtures/lint-no-workflow-caching/composites/.github/workflows/composite-input-named-cache.yml
  • scripts/__tests__/fixtures/lint-no-workflow-caching/composites/.github/workflows/composite-leading-space.yml
  • scripts/__tests__/fixtures/lint-no-workflow-caching/composites/.github/workflows/composite-missing-explicit-false.yml
  • scripts/__tests__/fixtures/lint-no-workflow-caching/composites/.github/workflows/composite-nested.yml
  • scripts/__tests__/fixtures/lint-no-workflow-caching/composites/.github/workflows/composite-setup-node-cache.yml
  • scripts/__tests__/fixtures/lint-no-workflow-caching/composites/.github/workflows/composite-thirdparty-cache.yml
  • scripts/__tests__/fixtures/lint-no-workflow-caching/composites/.github/workflows/composite-unresolvable-with-cache.yml
  • scripts/__tests__/fixtures/lint-no-workflow-caching/composites/.github/workflows/composite-unresolvable.yml
  • scripts/__tests__/fixtures/lint-no-workflow-caching/composites/.github/workflows/composite-yaml-ext.yml
  • scripts/__tests__/fixtures/lint-no-workflow-caching/composites/.github/workflows/local-js-action.yml
  • scripts/__tests__/fixtures/lint-no-workflow-caching/composites/.github/workflows/mixed-unresolved-and-cache.yml
  • scripts/__tests__/fixtures/lint-no-workflow-caching/composites/.github/workflows/third-party-uses.yml
  • scripts/__tests__/fixtures/lint-no-workflow-caching/reusable/.github/actions/cachey/action.yml
  • scripts/__tests__/fixtures/lint-no-workflow-caching/reusable/.github/workflows/called-cache.yml
  • scripts/__tests__/fixtures/lint-no-workflow-caching/reusable/.github/workflows/called-clean.yml
  • scripts/__tests__/fixtures/lint-no-workflow-caching/reusable/.github/workflows/called-composite.yml
  • scripts/__tests__/fixtures/lint-no-workflow-caching/reusable/.github/workflows/called-input-named-cache.yml
  • scripts/__tests__/fixtures/lint-no-workflow-caching/reusable/.github/workflows/called-loop-a.yml
  • scripts/__tests__/fixtures/lint-no-workflow-caching/reusable/.github/workflows/called-loop-b.yml
  • scripts/__tests__/fixtures/lint-no-workflow-caching/reusable/.github/workflows/called-missing-explicit-false.yml
  • scripts/__tests__/fixtures/lint-no-workflow-caching/reusable/.github/workflows/called-outer.yml
  • scripts/__tests__/fixtures/lint-no-workflow-caching/reusable/.github/workflows/called-thirdparty-cache.yml
  • scripts/__tests__/fixtures/lint-no-workflow-caching/reusable/.github/workflows/reusable-both.yml
  • scripts/__tests__/fixtures/lint-no-workflow-caching/reusable/.github/workflows/reusable-cache.yml
  • scripts/__tests__/fixtures/lint-no-workflow-caching/reusable/.github/workflows/reusable-clean.yml
  • scripts/__tests__/fixtures/lint-no-workflow-caching/reusable/.github/workflows/reusable-composite.yml
  • scripts/__tests__/fixtures/lint-no-workflow-caching/reusable/.github/workflows/reusable-cyclic.yml
  • scripts/__tests__/fixtures/lint-no-workflow-caching/reusable/.github/workflows/reusable-explicit-secrets.yml
  • scripts/__tests__/fixtures/lint-no-workflow-caching/reusable/.github/workflows/reusable-input-named-cache.yml
  • scripts/__tests__/fixtures/lint-no-workflow-caching/reusable/.github/workflows/reusable-missing-explicit-false.yml
  • scripts/__tests__/fixtures/lint-no-workflow-caching/reusable/.github/workflows/reusable-nested.yml
  • scripts/__tests__/fixtures/lint-no-workflow-caching/reusable/.github/workflows/reusable-no-secrets.yml
  • scripts/__tests__/fixtures/lint-no-workflow-caching/reusable/.github/workflows/reusable-remote.yml
  • scripts/__tests__/fixtures/lint-no-workflow-caching/reusable/.github/workflows/reusable-thirdparty-cache.yml
  • scripts/__tests__/fixtures/lint-no-workflow-caching/reusable/.github/workflows/reusable-unresolvable.yml
  • scripts/__tests__/fixtures/lint-no-workflow-caching/thirdparty-cache.yml
  • scripts/__tests__/fixtures/lint-no-workflow-caching/unaudited-setup.yml
  • scripts/__tests__/integration-workflow-paths.test.mjs
  • scripts/__tests__/lint-no-workflow-caching.test.mjs
  • scripts/lint-no-workflow-caching.mjs
  • skills/stash-supply-chain-security/SKILL.md

Comment thread scripts/lint-no-workflow-caching.mjs Outdated
…crashing

Three review findings, all in the same class: a file this gate could not read
took the run down with it rather than being reported.

**Unguarded `yaml.load`.** All three read sites — a composite manifest, a called
workflow, and the target workflow itself — threw straight out of the run on
anything unparseable: a stack trace instead of a finding, exit 1 which is
indistinguishable from "found a caching issue", and every remaining target never
scanned. A malformed `release.yml` meant `tests-supply-chain.yml` was not looked
at either. One shared `loadYaml` helper now reports and returns null, and a file
that will not parse lands in the un-auditable list for the reason that list
exists: it hands the traversal no step list, so nothing below it is audited.
Only the first line of the error is kept — js-yaml puts the position there and
follows it with a source snippet whose own indentation would wreck the report's
bullets. Each caller decides what null means: `walkSteps` reads it as a
NON-composite manifest, so `checkStep` keeps every rule including `with.cache`,
and the target loop continues to the remaining targets.

**`LOCAL_USES` accepted `../`, which GitHub does not.** The comment above it
already said "GitHub requires the `./` prefix"; the regex said `{1,2}`. That was
not a harmless widening, because "local" means two things here — exempt from
AUDITED_ACTIONS, and handed to a resolver that `resolve()`s the value against
the workspace root. Confirmed by dropping a workflow holding `actions/cache@v4`
one directory above a fixture root: `uses: ../outside-workflow.yml` opened it,
audited it, and printed the finding with a `../` trail. A file outside the
checkout, read as though it were inside. `../` now gets its own verdict rather
than falling through to the remote branches, which would be fail-closed but
would tell the reader to audit a published action that does not exist.

**`resolveActionFile` lacked the `isFile` guard its sibling has.** A DIRECTORY
named `action.yml` passed a bare `existsSync` and reached `readFileSync` as the
manifest — unhandled EISDIR. `resolveWorkflowFile` has guarded exactly this
since it was written, with a comment explaining why; the asymmetry was the bug.
Guarded, the directory is skipped and the existing "no action.yml or action.yaml
there" report is what comes out.

One further suggestion was checked and not taken: adding a `timeout` to the
test helper's `execFileSync`, on the premise that a regression in cycle
protection would hang the suite. Deleting the `visited` guard and running the
cyclic fixture blows the stack and exits in well under a second — the script is
synchronous end to end and has no way to hang. The comment claiming otherwise
was the source of the suggestion and is corrected instead.

Scripts suite 224 passing (+8); all three lint gates OK; biome 0 errors.
@tobyhede
tobyhede merged commit 0d2f73a into main Aug 6, 2026
17 checks passed
@tobyhede
tobyhede deleted the feat/protect-ffi-2-supply-chain-gate branch August 6, 2026 03:01
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.

2 participants