A GitHub Action that acts as a supply-chain security gate by failing if newly added or updated packages were published less than a configurable number of days ago.
| Ecosystem | Lockfiles | Registry |
|---|---|---|
| npm | pnpm-lock.yaml, package-lock.json, yarn.lock, bun.lock |
npm registry |
| python | uv.lock, *.py.lock (script lockfiles), pylock.toml (PEP 751) |
PyPI |
| rust | MODULE.bazel with crate.spec() |
crates.io |
| java | MODULE.bazel with maven.install() + JSON lock files |
Maven Central / custom repos |
| bazel | MODULE.bazel.lock |
Bazel Central Registry (BCR) |
| actions | .github/workflows/*.yml, action.yml |
GitHub API |
| multitool | multitool.hub() lockfiles via MODULE.bazel |
Archive Last-Modified headers |
- uses: runloopai/lisan-al-gaib-action@main
with:
ecosystems: npmThe action auto-detects the base ref to diff against based on the GitHub event:
| Event | Base ref |
|---|---|
pull_request / pull_request_target |
PR base SHA |
push |
payload.before SHA |
merge_group |
Merge group base SHA |
release |
target_commitish |
schedule, workflow_dispatch, workflow_call, workflow_run |
HEAD~1 |
Falls back to HEAD~1, then origin/main, then the empty tree (initial commit) if the resolved ref doesn't exist.
You can always override with the base-ref input.
| Input | Required | Default | Description |
|---|---|---|---|
ecosystems |
Yes | Comma-separated list: npm, python, rust, java, bazel, actions, multitool |
|
min-age-days |
No | 14 |
Minimum days since publication to pass |
warn-age-days |
No | 21 |
Age threshold for warnings (between min and warn = warning, above = pass) |
base-ref |
No | auto-detect | Git ref to diff against |
node-lockfiles |
No | auto-detect | Newline-separated glob patterns for Node.js lockfiles |
python-lockfiles |
No | auto-detect | Newline-separated glob patterns for Python lockfiles |
module-bazel |
No | MODULE.bazel |
Path to root MODULE.bazel (for rust/java/bazel/multitool ecosystems) |
workflow-files |
No | auto-detect | Newline-separated glob patterns for workflow files (for actions ecosystem) |
strict-third-party |
No | false |
Fail (instead of warn) on archive overrides without Last-Modified and third-party branch-pinned actions |
bypass-keyword |
No | "" |
If the PR body contains this string on a line by itself, failures are downgraded to warnings |
check-all-on-new-workflow |
No | true |
Check all packages (not just changed) when the workflow file is newly added |
github-token |
No | ${{ github.token }} |
GitHub token for API queries (actions and bazel ecosystems) |
npm-registry-url |
No | https://registry.npmjs.org |
npm registry URL |
pypi-registry-url |
No | https://pypi.org |
PyPI registry URL |
crates-registry-url |
No | https://crates.io |
crates.io registry URL |
maven-registry-url |
No | https://repo1.maven.org/maven2 |
Maven Central registry URL |
target-licenses |
No | auto |
SPDX license(s) your project is distributed under. Deps must be compatible with the target. Supports per-ecosystem YAML map, special aliases (open-source, open-source-no-strong-copyleft, open-source-no-relinkable-copyleft, open-source-no-network-copyleft), or auto to detect from package.json/LICENSE (falls back to open-source-no-relinkable-copyleft if detection fails). Empty string disables license checking. |
allowed-licenses |
No | "" |
Deprecated — use target-licenses. Ignored when target-licenses is set. |
age-overrides |
No | "" |
YAML map of ecosystem → list of package names to skip age checking for specific packages |
license-overrides |
No | "" |
YAML map of ecosystem → package → SPDX license or "ignore" to override or skip license checking |
license-heuristics |
No | false |
When true, infer licenses from LICENSE/README file text using heuristic matching. When false, only use registry metadata and GitHub API. |
bcr-url |
No | https://bcr.bazel.build |
Bazel Central Registry URL |
| Output | Description |
|---|---|
total-checked |
Number of packages checked |
total-failures |
Number of packages that failed the age gate |
total-warnings |
Number of packages in the warning zone |
license-violations |
Number of packages with incompatible licenses |
name: Dependency Check
on: pull_request
permissions:
contents: read
jobs:
check:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- uses: runloopai/lisan-al-gaib-action@main
with:
ecosystems: npm- uses: runloopai/lisan-al-gaib-action@main
with:
ecosystems: npm,python,rust,java
min-age-days: "7"
warn-age-days: "14"name: Weekly Dependency Scan
on:
schedule:
- cron: "0 9 * * 1"
jobs:
scan:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 2
- uses: runloopai/lisan-al-gaib-action@main
with:
ecosystems: npm,python- uses: runloopai/lisan-al-gaib-action@main
with:
ecosystems: npm
node-lockfiles: |
apps/*/pnpm-lock.yaml
packages/*/package-lock.json- uses: runloopai/lisan-al-gaib-action@main
with:
ecosystems: actionsActions pinned to a branch (e.g. @main) are skipped. Actions pinned to a tag (e.g. @v4) or commit SHA are checked against the GitHub API for their publish/commit date.
- uses: runloopai/lisan-al-gaib-action@main
with:
ecosystems: bazelParses MODULE.bazel.lock for resolved module versions and queries the Bazel Central Registry. Handles overrides from MODULE.bazel:
git_override: checks the commit/tag/branch date via GitHub APIarchive_override: checks the archive URL'sLast-Modifiedheaderlocal_path_override: skippedsingle_version_override/multiple_version_override: checked against BCR with the overridden version
- uses: runloopai/lisan-al-gaib-action@main
with:
ecosystems: npm
# Auto-detect your project's license from package.json or LICENSE file.
# Falls back to open-source-no-relinkable-copyleft if detection fails.
target-licenses: auto
# Or specify your project's license explicitly
# target-licenses: "MIT"
# Per-ecosystem targets (YAML map)
# target-licenses: |
# "*": Apache-2.0
# rust: Apache-2.0, MIT
# npm: MIT
# Special aliases:
# target-licenses: "open-source" # any OSI-approved license
# target-licenses: "open-source-no-relinkable-copyleft" # OSI minus LGPL/GPL/AGPL (allows MPL, CDDL, EPL)
# target-licenses: "open-source-no-strong-copyleft" # OSI minus GPL/AGPL (allows LGPL, MPL, CDDL, EPL)
# target-licenses: "open-source-no-network-copyleft" # OSI minus AGPL
# Disable license checking
# target-licenses: ""For every analyzed dependency, the action fetches the license from the package registry (npm, PyPI, crates.io, Maven POM, GitHub API, BCR metadata) and checks directional compatibility — whether the dependency's license allows incorporation into a project under your target license. This uses a full SPDX compatibility matrix (permissive → copyleft flow, GPL version compatibility, weak copyleft rules, etc.). Incompatible licenses produce error annotations and fail the check.
- uses: runloopai/lisan-al-gaib-action@main
with:
ecosystems: npm,python
age-overrides: |
npm:
- some-legacy-package
python:
- internal-tool
license-overrides: |
npm:
custom-pkg: MIT
actions:
owner/repo: ignoreWhen license violations or unknown licenses are detected, the action suggests a license-overrides block as a colored git diff of your workflow file.
- uses: runloopai/lisan-al-gaib-action@main
with:
ecosystems: multitoolParses multitool.hub() calls from MODULE.bazel (following include() statements), finds the referenced lockfiles, and diffs HEAD vs base to detect changed tool binaries. Each binary's publish date is checked via the Last-Modified header of its download URL.
- uses: runloopai/lisan-al-gaib-action@main
with:
ecosystems: npm
npm-registry-url: "https://npm.pkg.github.com"When violations are detected, the action suggests package manager-level settings to prevent installing young packages:
| Package manager | Config file | Setting |
|---|---|---|
| pnpm | pnpm-workspace.yaml |
minimumReleaseAge: 20160 (minutes) |
| yarn | .yarnrc.yml |
npmMinimalAgeGate: "14d" |
| bun | bunfig.toml |
[install] minimumReleaseAge = 1209600 (seconds) |
| uv | pyproject.toml or uv.toml |
[tool.uv] exclude-newer = "14 days" |
- Resolve base ref from the GitHub event context (PR base, push before, etc.)
- Detect changed lockfiles by diffing HEAD against the base ref
- Parse lockfiles using structured parsers (
lockparsefor npm/pnpm/yarn/bun,smol-tomlfor Python,web-tree-sitterfor Bazel/Starlark) - Compare HEAD vs base lockfile contents to find new or version-changed packages
- Query registries for each changed package's publish date
- Report results as GitHub annotations (errors/warnings) and a job summary table
For Rust and Java ecosystems, the action parses MODULE.bazel using a tree-sitter Starlark grammar, resolving recursive include() statements to find all crate.spec() and maven.install() blocks.
For the Bazel ecosystem, it parses MODULE.bazel.lock (JSON) to find resolved module versions and extracts override directives (git_override, archive_override, etc.) from MODULE.bazel files.
For the Actions ecosystem, it parses workflow YAML files for uses: directives, determines whether each ref is a tag or commit SHA (branches are skipped), and queries the GitHub API for the associated date.
For the Multitool ecosystem, it finds multitool.hub() calls in MODULE.bazel, reads the referenced lockfiles (JSON), and checks each binary's download URL for a Last-Modified header.
If you need to merge a PR with a dependency that fails the age gate (e.g., a critical 0-day vulnerability fix), set the bypass-keyword input:
- uses: runloopai/lisan-al-gaib-action@main
with:
ecosystems: npm
bypass-keyword: "DEPENDENCY-AGE-BYPASS"The bypass is detected from any of the following (whichever matches first):
- PR body — include the keyword on its own line:
This PR updates lodash to fix CVE-2025-XXXX. DEPENDENCY-AGE-BYPASS - PR label — add a label named exactly
DEPENDENCY-AGE-BYPASSto the PR - Commit message — include the keyword on its own line in the HEAD commit message (useful for
push,workflow_dispatch, and other non-PR events)
The action will still report the failures as warnings but will not fail the check.
Note: If using label-based bypass, add
labeledandunlabeledto thepull_requestevent types so the workflow re-runs when labels change:on: pull_request: types: [opened, reopened, synchronize, edited, labeled, unlabeled]The
editedtype ensures the workflow re-runs when the PR body is changed to add the keyword.
You can run the check on a local repository without GitHub Actions:
# Install dependencies
pnpm install
# Compare against remote default branch (e.g., origin/main)
pnpm local -- --ecosystems npm
# Compare dirty (uncommitted) changes against HEAD
pnpm local -- --diff --ecosystems npm
# Compare against a specific ref
pnpm local -- --base-ref origin/release-2.0 --ecosystems npm,python
# Check ALL dependencies (not just changed)
pnpm local -- --all --ecosystems npm
# With custom thresholds
pnpm local -- --ecosystems npm --min-age-days 7 --warn-age-days 14| Option | Description |
|---|---|
--ecosystems <list> |
Comma-separated ecosystems (default: npm) |
--base-ref <ref> |
Git ref to diff against (default: remote default branch) |
--diff |
Compare working tree against HEAD |
--all |
Check all dependencies (uses empty tree as base) |
--min-age-days <n> |
Minimum age in days (default: 14) |
--warn-age-days <n> |
Warning threshold in days (default: 21) |
--github-token <t> |
GitHub token (default: $GITHUB_TOKEN env var) |
You can also run directly with Node after building:
pnpm build
node out/cli.js --ecosystems npm --diffLisan al-Gaib (لسان الغيب, "Voice from the Outer World") is a messianic figure in Frank Herbert's Dune universe — the prophesied leader who would deliver the Fremen from oppression. In the story, the Lisan al-Gaib defeats Shai-Hulud, the colossal sandworms that dominate the deserts of Arrakis.
The name is fitting for this action because Shai-Hulud is also the name given to a series of devastating npm supply chain attacks that began in September 2025. The Shai-Hulud worm compromised over 500 npm packages — collectively downloaded 132 million times per month — by hijacking maintainer credentials and automatically republishing all of a victim's packages with malicious payloads. Like its namesake sandworm, the malware burrowed through the ecosystem, using TruffleHog to scan for secrets (GitHub tokens, AWS/GCP/Azure keys) and self-replicating across every package owned by a compromised maintainer. A second wave in November 2025 impacted tens of thousands of GitHub repositories.
Just as the Lisan al-Gaib tamed the sandworms, this action guards your repository against the supply chain threats that Shai-Hulud exploited — by ensuring that newly published packages have had time to be vetted by the community before they enter your dependency tree.