gh-dep-risk is a precompiled GitHub CLI extension that reviewers run on
demand to summarize dependency risk in pull requests.
It is an extension instead of a server so it can reuse gh authentication,
stay on the reviewer's machine or in a one-off workflow run, and avoid any
webhook, queue, database, or dashboard infrastructure.
The animated terminal capture above is an illustrative live E2E PR using Yarn
local fallback. An asciinema-compatible recording is also checked in at
docs/assets/demo.cast; the checked-in examples under
docs/examples are the exact renderer-backed output fixtures.
- on-demand pull request dependency review through GitHub Dependency Review API when GitHub provides dependency-review data for the PR
- local fallback only when Dependency Review is unavailable, such as
403or404, for these static-file targets:- npm:
package.json,package-lock.json - pnpm:
package.json,pnpm-lock.yaml - pnpm workspace discovery:
pnpm-workspace.yaml - yarn:
package.json, classic or modernyarn.lock; Yarn Berry / modern Yarn fallback is direct-dependency only and does not parse.pnp.cjsor reconstruct the PnP graph - Bun:
package.jsondirect dependencies with matching textbun.lockentries;bun.lockbis unsupported, with no resolver or full graph reconstruction - Python:
requirements.txt, PEP 621pyproject.toml, and Poetrypyproject.tomldirect dependency declarations; Poetry may usepoetry.lockand PEP 621 may useuv.lockto enrich direct resolved versions and source metadata, with no resolver or full transitive analysis - Go modules:
go.modrequireandreplacechanges, withgo.sumchecksum changes treated as evidence only; no resolver, full module graph, orgo list/go modexecution
- npm:
- one Go binary
- dependency review API first, local fallback only when dependency review is unavailable
- no server, webhook receiver, GitHub App, DB, queue, dashboard, or broad local fallback beyond the narrow JavaScript, Python, and Go module static-file fallback in this release
Dependency Review API ecosystems surfaced in this release when GitHub provides them are separate from local fallback support:
- Cargo
- Composer
- Go modules
- Maven
- npm
- pip
- pnpm
- Poetry
- RubyGems
- Swift Package Manager
- Yarn
gh auth logingo-gh also respects:
GH_TOKENGITHUB_TOKENGH_REPOGH_HOST
GH_REPO=OWNER/REPO is useful outside a git checkout. GH_HOST is useful for
GitHub Enterprise.
gh extension install rad1092/gh-dep-riskUpgrade later with:
gh extension upgrade dep-riskLinux or macOS:
go build -o gh-dep-risk .
gh extension install .Windows PowerShell:
go build -o gh-dep-risk.exe .
gh extension install .This repo does not install itself automatically. Build the binary at the
repository root first, then run gh extension install . manually.
The installed command remains gh dep-risk.
The repository itself also needs the gh- prefix because GitHub CLI extension
install requires remote extension repositories to start with gh-.
The public repository slug is rad1092/gh-dependency-risk for readability, but
the stable install path intentionally remains rad1092/gh-dep-risk so GitHub
CLI registers the command as gh dep-risk. Installing the readability slug
directly registers the longer command name gh dependency-risk.
The checkout directory name must still start with gh- for local extension
install to work, so use a local folder such as gh-dep-risk when you clone
for extension testing.
gh dep-risk pr 123
gh dep-risk pr https://github.com/OWNER/REPO/pull/123
gh dep-risk pr --format json
gh dep-risk pr 123 --list-targets
gh dep-risk pr 123 --path apps/web
gh dep-risk pr 123 --path package.json --comment
gh dep-risk pr --comment=false
gh dep-risk pr --bundle-dir ./dep-risk-bundle
gh dep-risk pr --comment
gh dep-risk pr --fail-level high
gh dep-risk version
gh dep-risk version --jsonTypical read-only live checks against owned fixture repositories:
gh dep-risk pr 3 --repo rad1092/gh-dep-risk-smoke-matrix --lang en --format json --no-registry
gh dep-risk pr 4 --repo rad1092/gh-dep-risk-smoke-matrix --lang en --format json --no-registry
gh dep-risk pr 10 --repo rad1092/gh-dep-risk-smoke-matrix --lang en --format json --no-registryThe owned smoke matrix covers npm, pnpm workspace, Yarn Classic, Python
requirements, Python PEP 621, Poetry, uv, Go modules, Yarn Berry, Bun text
bun.lock, and bun.lockb unsupported-only behavior. See
docs/smoke-test.md for the full command matrix and the
separate unsupported-only fixture.
Comment smoke is intentionally separate because it writes a PR timeline issue comment:
gh dep-risk pr 1 --repo rad1092/gh-dep-risk-smoke-comments --lang en --comment --no-registryThe comments repository may contain one marker comment per authenticated
identity such as rad1092 locally and github-actions[bot] from Actions.
Command shape:
gh dep-risk pr [<number>|<url>]gh dep-risk version
If the PR argument is omitted, gh dep-risk pr resolves the PR for the current
branch.
--repo owner/repo--format human|json|markdown--lang ko|en--comment--fail-level low|medium|high|critical|none--no-registryto skip npm-compatible registry publish-age lookups--bundle-dir <dir>--path <repo-relative-dir-or-manifest>repeatable--list-targets
--json
gh dep-risk pr also reads a repo-local config file named
.gh-dep-risk.yml from the current working directory when it exists.
Supported keys:
lang: ko|enfail_level: none|low|medium|high|criticalcomment: true|falsepath: apps/weborpath: [apps/web, package.json]no_registry: true|false
Example:
lang: en
fail_level: high
comment: true
path:
- apps/web
- package.json
no_registry: falsePrecedence rules:
- CLI flags override config values
- config values override built-in defaults
- an explicit CLI
--pathreplaces configpath - explicit boolean CLI overrides such as
--comment=falseand--no-registry=falseoverride a config value oftrue
Unknown config keys are rejected with a clear error that includes the config file path. A missing config file is ignored.
These examples are checked in under docs/examples and are derived from deterministic fixtures, render tests, and fixture-backed app tests.
Repository: owner/repo
PR: #123 Update dependencies
Score: 48 (high)
Blast radius: medium
Dependency review available: false
Why risky: left-pad crosses a major version boundary and declares an install script.
<!-- gh-dep-risk -->
## gh-dep-risk
- Repository: `owner/repo`
- PR: [#123](https://github.com/owner/repo/pull/123) Update dependencies
- Score: `48` (`high`)
- Why risky: left-pad crosses a major version boundary and declares an install script.{
"repo": "owner/repo",
"score": 48,
"level": "high",
"blast_radius": "medium",
"dependency_review_available": false
}human: concise reviewer-oriented summaryjson: stable machine-readable schema with repo, PR metadata, score, level, blast radius, dependency review availability, summary bullets, recommended actions, notes, detailed changes, and atargetsarraymarkdown: comment-ready output that always starts with<!-- gh-dep-risk -->
English is the default language. Use --lang ko for Korean.
--bundle-dir writes:
dep-risk-human.txtdep-risk.jsondep-risk.mdmetadata.json
When multiple targets are analyzed, the bundle also includes:
targets/<safe-target-name>/dep-risk.jsontargets/<safe-target-name>/dep-risk.md
The score model stays heuristic, deterministic, and intentionally auditable.
- each dependency change is scored from named risk drivers with fixed weights
- the overall PR score is the highest single-change score plus a small capped bonus for additional risky changes
- this keeps the main driver explainable while still reflecting multi-target or multi-change PRs without turning the score into an opaque sum
Dependency Review API data is always preferred when available. Local fallback is used only when the Dependency Review API is unavailable for the PR.
| Ecosystem / manager | With GitHub Dependency Review | Without GitHub Dependency Review |
|---|---|---|
| npm | Dependency Review data is used when available. | Local fallback analyzes package.json and package-lock.json. |
| pnpm | Dependency Review data is used when available. | Local fallback analyzes package.json, pnpm-lock.yaml, and pnpm-workspace.yaml discovery. |
| Yarn Classic | Dependency Review data is used when available. | Local fallback analyzes package.json and classic yarn.lock. |
| Yarn Berry / modern Yarn | Dependency Review data is used when available. | Local fallback compares direct package.json declarations with matching modern yarn.lock entries. .yarnrc.yml is used for detection/nodeLinker notes only; no .pnp.cjs, cache archive inspection, Yarn command execution, registry lookup, or full PnP graph reconstruction. |
| Bun | Dependency Review data is used when available. | Local fallback compares direct package.json declarations with matching text bun.lock entries. bun.lockb is unsupported; no Bun/npm/node execution, registry lookup, or full dependency graph reconstruction. |
Python requirements.txt / PEP 621 pyproject.toml + optional uv.lock / Poetry pyproject.toml + optional poetry.lock |
Dependency Review data is used when available. | Local fallback compares direct dependency declarations and can use uv.lock or poetry.lock to enrich matching direct resolved versions and source metadata. No resolver, broad lockfile support, or full transitive analysis. |
| Go modules | Dependency Review data is used when available. | Local fallback analyzes static go.mod require/replace changes. go.sum is checksum evidence only; no resolver, full module graph, go list, or go mod execution. |
| Cargo, Composer, Maven, RubyGems, SwiftPM | Dependency Review data may be surfaced when GitHub provides it. | No local fallback in this release. |
gh dep-risk pr resolves the repository from GH_REPO or the current git
remote, fetches PR metadata, and prefers GitHub dependency-review data when it
is available. Repository-tree discovery remains in use for local fallback,
--list-targets, and path validation.
Supported target shapes:
- dependency-review targets for Cargo, Composer, Go modules, Maven, npm, pip, pnpm, Poetry, RubyGems, SwiftPM, and Yarn
- npm root projects with
package.jsonandpackage-lock.json - npm workspaces with a shared root
package-lock.json - pnpm root projects with
package.jsonandpnpm-lock.yaml - pnpm workspaces with
pnpm-workspace.yamland a shared rootpnpm-lock.yaml - Yarn root projects with
package.jsonand classic or modernyarn.lock - Yarn workspaces discovered from
package.jsonworkspaces and a shared rootyarn.lock - Bun root projects with
package.jsonand textbun.lock - Bun workspaces discovered from
package.jsonworkspaces and a shared root textbun.lock - nested standalone subprojects with their own
package.jsonand eitherpackage-lock.json,pnpm-lock.yaml,yarn.lock, orbun.lock - Python
requirements.txtdirect dependency declarations - PEP 621
pyproject.tomldirect dependencies from[project].dependenciesand[project.optional-dependencies], with optionaluv.lockdirect resolved-version and source enrichment - Poetry
pyproject.tomldirect dependencies from[tool.poetry.dependencies],[tool.poetry.dev-dependencies], and[tool.poetry.group.<name>.dependencies], with optionalpoetry.lockdirect resolved-version enrichment - Go modules
go.modrequireandreplacechanges, with optional siblinggo.sumchecksum evidence notes only
Default behavior:
- if one supported target changed,
gh-dep-riskanalyzes that target - if multiple supported targets changed,
gh-dep-riskanalyzes all of them and emits one aggregate result plus per-target detail - if no supported target changed, the command exits with code
2
Useful examples:
gh dep-risk pr 123 --list-targets
gh dep-risk pr 123 --path apps/web
gh dep-risk pr 123 --path package.json --comment
gh dep-risk pr 123 --bundle-dir ./outNotes:
--pathaccepts either an exact manifest path or an owning directory when that directory maps to exactly one detected target, and can be repeated--list-targetsprints a readable target list, validates any--pathfilters, and exits without running PR file analysis or dependency review- npm workspaces reuse the shared root
package-lock.json - pnpm workspaces reuse the shared root
pnpm-lock.yamland usepnpm-workspace.yamlpackage globs for discovery - Yarn Berry / modern Yarn local fallback is static and direct-only: it reads
package.json, matching modernyarn.lockentries, and optional.yarnrc.ymlnodeLinkersettings without runningyarn,npm, ornode - if multiple same-name Yarn Berry lockfile entries exist without an exact descriptor/range match for a direct dependency, local fallback does not guess a resolved version and records an unsupported-entry note
- Yarn Berry / modern Yarn local fallback ignores transitive-only lockfile
updates, checksum-only updates,
.pnp.cjs, PnP loader files, cache archives, plugins, constraints, and full PnP graph reconstruction - Yarn Berry protocols such as
workspace:,portal:,link:,file:,patch:, git, and HTTP(S) sources are surfaced as conservative notes/source validation signals when tied to changed direct dependencies - Bun local fallback is static and direct-only: it reads
package.jsonand matching textbun.lockentries without runningbun,npm, ornode - Bun
bun.lockbbinary lockfiles are not parsed in this phase, andbun.lockb-only changes remain unsupported/no meaningful dependency change - Bun transitive-only and checksum-only lockfile updates do not create dependency-change report entries
- Bun
workspace:,file:,link:, git, GitHub, and HTTP(S) sources are surfaced as conservative source validation notes/actions only when tied to changed direct dependencies - large lockfiles served by the GitHub contents API without inline content are still fetched through the corresponding blob object instead of failing early
- if a lockfile-only workspace change cannot be mapped exactly, the report calls out that attribution is approximate instead of failing
- if both
package-lock.jsonandpnpm-lock.yamlexist for the same target directory,gh-dep-riskwill only auto-pick one when exactly one lockfile is clearly changed in the PR; otherwise it returns an ambiguity error and tells you to narrow the target or remove the unused lockfile - if dependency review is unavailable and the selected target belongs to an ecosystem without local fallback support in this release, the command returns a clear actionable error instead of pretending to analyze it
- Python local fallback is declaration-oriented: unsupported requirement includes, constraints, editable installs, unsupported Poetry dependency shapes, and dependency groups outside the Poetry direct subset are not resolved in this phase
uv.locksupport is limited to enriching PEP 621 direct dependencies with matching direct resolved versions and recognized source metadata; registry, virtual, and workspace sources do not create non-registry source notes- standalone editable
uv.locksources are unsupported;editable=trueis only accepted as apathordirectorysource modifier - unknown
uv.locksource shapes are reported as unsupported dependency entries rather than guessed poetry.locksupport is limited to enriching direct Poetry dependencies with resolved versions and source metadata; it does not reconstruct a full transitive dependency graph- if a direct PEP 621 dependency declaration is unchanged but the matching
uv.lockresolved version or source changes, local fallback reports that direct dependency as updated uv.lockpackage changes without a matching direct dependency declaration inpyproject.tomlare treated as transitive-only and do not create report changes- if a direct Poetry dependency declaration is unchanged but the matching
poetry.lockresolved version changes, local fallback reports that direct dependency as updated poetry.lockpackage changes without a matching direct dependency declaration inpyproject.tomlare treated as transitive-only and do not create report changes- Go modules local fallback is static: it reads
go.modand optional siblinggo.sumcontent from the repository and never runsgo list,go mod,go env, or network module metadata lookups - Go modules local fallback reports
go.modrequireadditions, removals, updates, direct versus// indirectrequirements, andreplaceadditions, removals, or changes; localreplacetargets are surfaced as non-registry source validation notes/actions - Version-specific replace-only entries may use a stable display identity
including the old version, for example
example.com/lib@v1.0.0, because the existing JSON schema has no separate replacement identity field and schema churn is out of scope - Go modules local fallback may note pseudo-versions,
godirective changes,toolchaindirective changes, andgo.sumchecksum evidence changes only when the target also has a meaningfulrequireorreplaceresult go.sumis not treated as a lockfile or dependency tree, sogo.sum-only checksum changes do not create dependency-change report entries- out of scope for now: Python resolver behavior, PyPI/npm/module registry
metadata lookup,
bun.lockb,bunfig.toml, Bun resolver/workspace graph semantics,package.json5,package.yaml, pnpm catalogs, pnpm branch lockfiles, broad non-JS resolver-style fallback, Go module graph reconstruction, full Yarn Plug'n'Play graph resolution,.pnp.cjsparsing, Yarn plugin/constraint interpretation, SARIF output, license risk, and OSV/Socket integration
If dependency review returns 403 or 404, gh-dep-risk falls back to
supported local fallback analysis and explicitly reports
dependency_review_available=false. Local fallback does not merge in missing
Python, Go, Yarn, or Bun entries when Dependency Review is already available.
Registry publish-age lookups are best effort and limited to npm-compatible
registry packages from npm, pnpm, and Yarn-style targets. They are skipped with
--no-registry, while API-provided release-age signals remain available when
GitHub already supplies them. Python, Go, Poetry, uv, and Bun local fallback do
not perform PyPI, Go module proxy, or Bun registry publish-age lookups.
If there is no meaningful supported dependency change, the command exits with
code 2.
--comment uses PR timeline issue comments, not review comments.
The marker comment is:
<!-- gh-dep-risk -->Behavior:
- exactly one marker comment owned by the authenticated user is maintained
- if multiple own marker comments exist, the newest is updated and older own duplicates are deleted
- another author's marker comment is never edited or deleted
- if another author already has a marker comment,
gh-dep-riskwarns on stderr and only manages the current user's own comment
gh dep-risk version prints human-readable build metadata. Release-quality
builds inject:
versioncommitdate
Example:
gh dep-risk version
gh dep-risk version --jsonLocal go build still works with safe defaults. Release-quality binaries should
use ldflags, or the provided Makefile, so the version command does not report
only dev.
0success1general error2no supported dependency change found3final score meets or exceeds--fail-level4authentication required or insufficient permissions
Run tests:
go test ./...Build with local defaults:
go build -o gh-dep-risk .
./gh-dep-risk versionBuild with explicit metadata:
go build -ldflags "-s -w -X github.com/rad1092/gh-dependency-risk/cmd.version=dev-local -X github.com/rad1092/gh-dependency-risk/cmd.commit=$(git rev-parse --short HEAD) -X github.com/rad1092/gh-dependency-risk/cmd.date=$(date -u +%Y-%m-%dT%H:%M:%SZ)" -o gh-dep-risk .Or use the Makefile:
make test
make build VERSION=dev-localWindows PowerShell:
$date = (Get-Date).ToUniversalTime().ToString("yyyy-MM-ddTHH:mm:ssZ")
$commit = git rev-parse --short HEAD
go build -ldflags "-s -w -X github.com/rad1092/gh-dependency-risk/cmd.version=dev-local -X github.com/rad1092/gh-dependency-risk/cmd.commit=$commit -X github.com/rad1092/gh-dependency-risk/cmd.date=$date" -o gh-dep-risk.exe .
.\gh-dep-risk.exe version --jsonLocal extension install remains manual:
gh extension install .You can run the existing CLI engine from GitHub Actions without installing the extension locally.
Use the dep-risk-manual workflow and provide:
pr: required PR number or full PR URLrepo: optional repository overridelangfail_levelcommentno_registry
comment=true is limited to PRs in this repository. For remote comment-upsert
smoke, run the target smoke repository's own workflow instead so its
repository-scoped GITHUB_TOKEN writes to its own PR.
The workflow file must exist on the default branch for the Run workflow button to appear.
gh workflow run .github/workflows/dep-risk-manual.yml -f pr=123
gh workflow run .github/workflows/dep-risk-manual.yml -f pr=https://github.com/OWNER/REPO/pull/123
gh workflow run .github/workflows/dep-risk-manual.yml -f pr=3 -f repo=rad1092/gh-dep-risk-smoke-matrix -f no_registry=true
gh run watch
gh workflow run comment-smoke.yml --repo rad1092/gh-dep-risk-smoke-comments -f pr=1 -f source_ref=main -f no_registry=true
gh run watch --repo rad1092/gh-dep-risk-smoke-commentsEach manual run:
- builds and tests the repo
- builds
gh-dep-riskonce with workflow metadata - runs the CLI once
- uploads the output bundle artifact
- appends aggregate markdown output to the workflow job summary
If comment=true, comment ownership follows the workflow-authenticated identity.
This workflow uses only its repository-scoped GITHUB_TOKEN; in GitHub Actions
that identity is github-actions[bot].
When the workflow is running in a different repository than the target PR,
GITHUB_TOKEN may not be allowed to read the target PR at all, especially for
private cross-repo targets. In that case the workflow can fail before artifact
upload. Cross-repo comment mode is intentionally refused by this workflow.
Remote comment smoke is handled by
rad1092/gh-dep-risk-smoke-comments/.github/workflows/comment-smoke.yml. That
workflow checks out this repository, builds the requested ref, and comments on
its own fixture PR with its own GITHUB_TOKEN, so no PAT secret is needed.
These workflows use Node 24 based GitHub Actions majors. Keep self-hosted
runners current; GitHub's Node 24 migration guidance uses Actions Runner
v2.327.1+ as the baseline. If a self-hosted runner rejects checkout@v5,
upgrade the runner before using these workflows.
Push a v* tag to trigger .github/workflows/release.yml.
The release workflow:
- runs
go test ./... - injects version, commit, and build date metadata into binaries
- uses
cli/gh-extension-precompile@v2 - publishes precompiled binaries for GitHub CLI extension installs
For the exact first release procedure, see RELEASING.md.
This project is licensed under the MIT License.
