GHA Cache Auditor
Find GitHub Actions cache keys that miss inputs affecting cached artifacts.
A YAML linter can validate this workflow, while the same node_modules cache
is still shared by two different Node.js runtimes:
jobs:
test:
runs-on: ubuntu-latest
strategy:
matrix:
node: [22, 24]
steps:
- uses: actions/setup-node@v6
with:
node-version: ${{ matrix.node }}
- uses: actions/cache@v4
with:
path: node_modules
key: deps-${{ hashFiles('package-lock.json') }}
$ gha-cache-audit .
GHA-CACHE-001 HIGH
Potential cross-runtime cache reuse
.github/workflows/test.yml:15 (job: test)
Cache path:
node_modules
Cache key:
deps-${{ hashFiles('package-lock.json') }}
node_modules can be restored across different runtime or architecture configurations.
Those configurations vary in this job, but neither the key nor cache path distinguishes them.
Missing key dependency:
matrix.node
Suggested:
Include ${{ matrix.node }} in the cache key.
An appropriate key for this example is
deps-${{ runner.os }}-${{ matrix.node }}-${{ hashFiles('package-lock.json') }}.
Install and run
Python 3.11+ is required. Install the CLI from PyPI with uv:
uv tool install gha-cache-auditor
gha-cache-audit .
With pip, install it into the active environment:
python -m pip install gha-cache-auditor
To install from a checkout instead:
uv tool install .
gha-cache-audit .
gha-cache-audit .github/workflows --format json
gha-cache-audit . --workflow-dir ./ci --format json
gha-cache-audit ./ci/test.yml --root . --format json
gha-cache-audit . --format sarif > cache-audit.sarif
gha-cache-audit . --min-confidence medium
pip install . also installs the CLI from a checkout. No token, GitHub App,
workflow execution, or network access is needed during analysis. The auditor
never modifies workflows.
It scans immediate .yml/.yaml children of .github/workflows, a supplied
workflow file, or an explicitly supplied workflow directory using
--workflow-dir; with that option, the positional path is the repository root.
A positional directory without --workflow-dir is treated as a repository
root or the conventional .github/workflows directory. File paths in reports
are relative to the inferred repository root. Dependency files are checked
there, not relative to the workflow YAML. Monorepo installed directories are
associated with dependency files in their own parent directory.
For a standalone workflow file outside .github/workflows, use --root when
the repository root cannot be inferred from a checkout or the current directory.
Exit codes: 0 no findings at the selected confidence, 1 findings,
2 parse/configuration errors or explicitly unsupported workflow structures.
Diagnostics are included in JSON and SARIF, including when findings also exist.
Default confidence is high; medium includes both levels.
Rules and confidence
- GHA-CACHE-001: installed dependencies shared across varying runtime or
architecture inputs established by
setup-node/setup-python. High. - GHA-CACHE-002: installed dependencies lack OS partitioning in an observed
multi-OS matrix, or omit an explicitly configured artifact input. Linux/macOS
variation and explicitly enabled cross-OS archives are high; Windows mixing
without that flag is medium because archive versions can partition caches.
A single fixed OS without
runner.osis medium because a later workflow revision can move the job to another OS while preserving its key. - GHA-CACHE-003: an installed dependency directory has one identifiable local
lockfile/requirements file and the key does not hash it. High when the
platform is known; medium when unknown runner OS makes case-only hash matching
ambiguous. Competing lockfiles are ambiguous and skipped. A package manifest
is not a lockfile.
requirements.txtis considered only when an install command explicitly names it. An explicit npm install without package-lock use is not charged with apackage-lock.jsondependency. - GHA-CACHE-004:
dist/build, a build command in the same directory and existingsrcor known configuration files not fully hashed by the key. For.next/cache, only an identifiable build configuration file is checked. Medium: the tool cannot prove whether later commands rebuild the output. - GHA-CACHE-005: a restore prefix drops runtime/platform partitioning retained by the primary key. Medium: later installation might repair restored files. Dropping only the lockfile hash is deliberately not reported.
Recognized installed artifacts: node_modules, .venv, venv, including nested
paths. Dependency files: npm, pnpm, yarn, pip requirements, uv, Poetry and Pipenv.
Runtime axes are inferred from setup configuration, not guessed from matrix names.
A single fixed OS does not establish cross-platform cache reuse.
Analysis model
The parser records cache definitions, source lines, commands, setup inputs,
matrix rows and environment aliases. An expression tokenizer distinguishes
references, string literals, function calls and hashFiles patterns. Artifact
classification supplies relevant runtime, platform and dependency-file inputs.
The analyzer compares those inputs with key and path dependencies, checking
whether two static matrix rows can differ in a required input while all known
key inputs remain unchanged. Correlated matrix.include rows are therefore not
mistaken for independent dimensions. exclude removes rows before comparison.
Workflow/job/step env aliases, bracket notation such as matrix['node'], and
setup runtime outputs are understood. Static matrix expansion is capped at 256
rows. Workflow files are capped at 2 MB. Input code is never evaluated.
The cache service also uses paths and archive properties in a hidden cache version, so a key string alone is not the whole cache identity. actions/cache documentation.
Setup actions and intentional non-findings
Explicit built-in caching in actions/setup-node and actions/setup-python is
inventoried in JSON as an implicit cache with a managed key. The MVP trusts these
actions' cache implementations and does not reverse-engineer their key for an
arbitrary pinned revision. setup-node caches package downloads, not
node_modules; Node version sharing is expected. setup-python's pip key includes
OS, architecture and Python version.
setup-node,
setup-python implementation.
Package download stores (~/.npm, pnpm store, pip/uv cache) are intentionally
not treated as installed dependency trees. Their own content/version addressing
makes unconditional runtime/lockfile warnings misleading. .next/cache is
incremental and can be useful after source changes, so this rule does not require
its key to hash every source file.
Limitations prioritize fewer false positives over coverage:
- Dynamic matrices in cache-bearing jobs and reusable workflow calls are reported as incomplete; reusable workflow files with ordinary jobs can be analyzed directly. Inputs and secrets are not resolved across callers.
- Unknown key references (including arbitrary step outputs), conditional or
multiple runtime setup steps, conditional jobs with caches, conditional cache
steps, and opaque paths are conservatively skipped with a diagnostic and exit
code 2. Deep or repeatedly expanded environment aliases are also bounded and
reported as incomplete. Statically
true,falseandalways()conditions are handled directly. - No shell interpretation, transitive task graph, remote actions, containers,
arbitrary package-manager scripts or dynamic
GITHUB_ENVevaluation. - An expression dependency is not proof of an injective expression. Complex expressions may hide a collision that this tool misses.
- File glob support is a conservative approximation, not full
@actions/glob. Ordered positive patterns and!exclusions are recognized; unusual patterns can be missed. Windows drive-qualified and UNChashFilespatterns are reported as incomplete. A leading/is treated as repository-root-relative, as GitHub Actions does. Runtime version files and arbitrary configuration-file build graphs are not inferred. Use explicit artifact inputs where necessary. - Cache paths that resolve outside the repository root are reported as incomplete and skipped; the auditor does not scan files outside the checkout.
- No finding does not prove a cache is safe. Findings describe potential reuse, not proof that a cached directory necessarily contains incompatible files.
Configuration and suppressions
Optional .gha-cache-audit.toml at repository root (or --config PATH):
[[artifacts]]
path = ".cache/custom-compiler"
depends-on = ["runner.os", "matrix.compiler", "compiler.lock"]
[[suppressions]]
rule = "GHA-CACHE-005"
file = ".github/workflows/test.yml"
path = "node_modules"
reason = "Installation always repairs a partial cache match."
Artifact paths match exactly. Suppression file/path fields accept shell-style
globs and default to *; each suppression requires a non-empty reason.
Suppressions filter findings, never parsing diagnostics. Rule IDs are stable.
JSON includes compact cache inventories, evidence, missing inputs and optional
suggestions. Raw commands and environment mappings are excluded from the
inventory so multiple caches do not duplicate large job bodies.
SARIF 2.1.0 includes rules, locations, confidence and diagnostic notifications.
GitHub Action
The composite action.yml installs this checkout and runs the same CLI. It uses
a temporary virtual environment and the existing Python 3.11+ on the runner;
provision Python first on self-hosted runners. No repository files are edited.
steps:
- uses: actions/checkout@v5
- uses: ./ # auditor repository checkout; for consumers use OWNER/REPO@REF
with:
path: .
# Optional: set this when workflows live outside .github/workflows.
# workflow-dir: ./ci
format: text
min-confidence: high
For consumers, use the repository and a full release tag, for example
0then0/gha-cache-audit@vX.Y.Z. Releases use vMAJOR.MINOR.PATCH tags and the
tag must match [project].version in pyproject.toml. Pushing a matching tag
runs the release jobs after the test and Action smoke jobs pass. A read-only job
builds and checks the wheel and source distribution, then creates SHA-256
checksums. Separate jobs publish the distributions to PyPI and attach them,
along with the checksums, to a GitHub Release with generated release notes.
PyPI publishing uses GitHub OIDC Trusted Publishing with the pypi environment;
it does not require a stored PyPI API token. The GitHub Release job alone needs
the repository's default GITHUB_TOKEN with contents: write. Failed draft
uploads can be retried.
To publish a release, update the project version and lockfile, commit the change, then push an annotated version tag that points to that commit:
git tag -a vX.Y.Z -m "Release vX.Y.Z"
git push origin vX.Y.Z
The Action returns the CLI's nonzero exit status so findings fail the job.
With format: sarif, its report output is a file in the runner's temporary
directory. Upload it in a following if: always() step using
github/codeql-action/upload-sarif and the required security-events: write
permission. Upload and permissions remain controlled by the caller.
Demo and development
examples/demo is a small repository-shaped fixture with unsafe Node.js caching.
Run gha-cache-audit examples/demo to reproduce the high-confidence finding.
uv sync --locked
uv run --no-sync ruff check .
uv run --no-sync ruff format --check .
uv run --no-sync python -m unittest discover -s tests -v
Ruff is the only additional development dependency: it checks Python errors,
sorts imports and formats code. To apply formatting, run
uv run --no-sync ruff format .; to fix supported lint issues, run
uv run --no-sync ruff check --fix .. CI runs the same checks using the lockfile.
Tests use only the standard library plus the runtime YAML parser. Fixtures cover positive and negative rules, aliases, built-in caches, matrix correlations, malformed workflows, multiple cache blocks, suppressions, CLI and SARIF. Contributions should include a minimal positive fixture and a nearby negative case, especially when introducing a new inference. Keep rule IDs stable, explain confidence and avoid guesses about commands that the analyzer cannot model.
Licensed under Apache-2.0; see LICENSE.
Release files for gha-cache-auditor 0.1.4
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| gha_cache_auditor-0.1.4.tar.gz | 40.4 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| gha_cache_auditor-0.1.4-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 69.1 kB
Release files / gha_cache_auditor-0.1.4.tar.gz
| Download URL | gha_cache_auditor-0.1.4.tar.gz |
|---|---|
| Size | 40.4 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
5cf7a58cd9eee014bd39eb0874c2014ad812be285968869e7df569e211c914bc
|
|
BLAKE2b-256 checksum How to use checksums |
d61cd672a7d61d0208a4d8f528b3b91ef1f54fae36997af4462a252e9ce67f9f
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Sep 24, 2026.
Transparency logRelease files / gha_cache_auditor-0.1.4-py3-none-any.whl
| Download URL | gha_cache_auditor-0.1.4-py3-none-any.whl |
|---|---|
| Size | 28.7 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
daea00a78fe42272c3f958de94ed9a055279420d63d2f3ce0d3aad1ac4abda32
|
|
BLAKE2b-256 checksum How to use checksums |
8462f44c4e0566f047fb57af130082069745a05ac88cee50b582bc978008eab1
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Sep 24, 2026.
Transparency log