pin-deep
Recursive GitHub Actions pin checker. You pinned your Actions — but did the actions you pinned pin their dependencies?
pin-deep starts from a SHA-pinned GitHub Action, fetches its action.yml, and recursively flags every nested uses: that is not pinned to a full commit SHA. Pinning has a depth dimension; this tool checks it.
Why this exists
Pinning uses: to a commit SHA is standard supply-chain hygiene — until you realize the pinned action itself pulls in other actions by tag. The tj-actions incident (CVE-2025-30066) is the textbook case: the compromise reached victims through reviewdog, which referenced the compromised action. Pinning reviewdog itself was not enough, because the hole was one level deeper.
"You pinned your Actions, but the Actions you pinned didn't pin their dependencies."
Differentiation
Tools like zizmor analyze your workflow files statically — they check your uses: lines. pin-deep checks one level further: it resolves what your pinned actions themselves depend on, recursively. Static workflow linters don't do recursive nested checks; pin-deep is the depth dimension of pinning.
Install
pip install pin-deep
Requires Python 3.9+. No dependencies — stdlib only.
Usage
Scan a workflow file:
pin-deep .github/workflows/ci.yml
Scan a single pinned action (starts the recursion from there):
pin-deep reviewdog/action-setup@e04ffabe3898a1af8f75141d79fa1ba7a3636eb3
Example output:
[unpinned] depth=1 actions/checkout@v4
source: action.yml of acme/root@aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
chain: acme/root@aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa -> actions/checkout@v4
note: not pinned to a full commit SHA
Options:
pin-deep --max-depth 5 --format json .github/workflows/ci.yml
pin-deep --strict .github/workflows/ci.yml # also fail on short SHAs and fetch errors
Exit code is 1 when unpinned nested actions are found, so it drops straight into CI:
- run: pip install pin-deep
- run: pin-deep .github/workflows/ci.yml
GITHUB_TOKEN (or --token) is used for the API fallback when fetching action.yml; public repos work without one.
How it works
- Parse every
uses:value from the target (workflow file or bareowner/repo@ref). - Classify each:
pinned(full 40/64-char SHA),unpinned(tag, branch, or no ref),short-sha(warning), or skipped (./local,docker://, dynamic expressions). - For each pinned action, fetch its
action.ymlat that exact SHA and repeat — up to--max-depth(default 3), with a visited set so reference cycles can't loop forever. - Unpinned refs are reported but never recursed into: a moving tag has no fixed content to analyze.
Limitations
- Only recurses into full-SHA pins. Analyzing the children of a floating tag would audit today's state, not the state you ship.
uses:parsing is line-based, not a full YAML parse — exotic YAML layouts may be missed.- Private repos need
GITHUB_TOKEN.
License
MIT — see LICENSE.
Metadata
Release files for pin-deep 0.1.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| pin_deep-0.1.0.tar.gz | 9.1 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| pin_deep-0.1.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 17.2 kB
Release files / pin_deep-0.1.0.tar.gz
| Download URL | pin_deep-0.1.0.tar.gz |
|---|---|
| Size | 9.1 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
782813566ed33a46d7cfcc6807cae429214291e266f80d1477d1da25cfb11d4a
|
|
BLAKE2b-256 checksum How to use checksums |
a56af007b7fe3ff7e4a0a9dbac001e3f06835b4ba6c645c82757e4ef1ba2ff63
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.12.3
|
Release files / pin_deep-0.1.0-py3-none-any.whl
| Download URL | pin_deep-0.1.0-py3-none-any.whl |
|---|---|
| Size | 8.0 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
c01d4db11ec37c2885afb73a56bdb878ec55505f9c5179cd8b7207dd31861cfb
|
|
BLAKE2b-256 checksum How to use checksums |
36f82f240caeb531fe13f786c3cdd022501a92f64c286631f3bc30679c2c8115
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.12.3
|