gh-action-pulse
gh-action-pulse scans a repository for GitHub Actions uses: references, checks them against the GitHub API, and rewrites them to safer or more current references when a better upstream target exists.
It is aimed at repositories that want to keep GitHub Actions dependencies understandable, current, and pinned with more confidence.
Key Features
- Automatic scanning: Detects
uses:statements across workflow and reusable action files. - Reference classification: Distinguishes whether an action is currently pinned to a commit SHA, tag, or branch.
- Recommendation engine: Looks up upstream metadata and recommends an updated reference based on available SemVer tags and branch state.
- Repository redirect handling: Rewrites moved repositories to their canonical name when GitHub reports a redirect.
- Freshness checks: Warns or fails when the newest eligible SemVer tag is older than your configured threshold.
- Node.js runtime check: Recursively verifies that actions, including composite and local composite dependencies, run on at least a configurable minimum Node.js version (
--minimum-nodejs-version, default24), failing with a dedicated exit code (3) when an outdated runtime is detected. - Comment preservation: Keeps extra trailing comments on
uses:lines when rewriting references. - Per-line ignore hints: Skip
--max-age,--min-age, or the Node.js runtime check for a specificuses:line with a# gh-action-pulse: ignore[...]comment. - Per-line override hints: Change
--max-age,--min-age, or--minimum-nodejs-versionfor a specificuses:line with a# gh-action-pulse: override[max-age=200]comment.
How It Works
For each detected uses: line, gh-action-pulse:
- Finds GitHub Actions references in the configured workflow and action directories.
- Queries the GitHub API for the referenced repository.
- Detects whether the current reference is a tag, branch, or SHA.
- Selects the newest SemVer tag that is at least
--min-agedays old (falling back to non-SemVer version tags such asv1orv0.6when the repository has no SemVer tag). - Falls back to a branch recommendation when that is safer or newer than the eligible tag.
- Rewrites redirected repositories to their canonical upstream name.
In practice, this means the tool can:
- convert branch or tag references into pinned SHAs annotated with the matching tag,
- preserve branch intent when no suitable tag exists,
- keep extra trailing comments such as ignore and override hints,
- skip
--max-age,--min-age, or Node.js checks for a specificuses:line when an ignore hint is present, - use a different
--max-age,--min-age, or Node.js minimum for a specificuses:line when an override hint is present, - surface stale upstream action releases with a non-zero exit code.
Example
Before:
- uses: google-github-actions/auth@v2
After:
- uses: google-github-actions/auth@<commit-sha> # v2.1.10
If an action repository has moved, the repository name may also be rewritten to the canonical upstream location.
To skip a check for one uses: line, add a trailing ignore hint. Quoted and unquoted check ids are both accepted:
- uses: actions/setup-node@abc123 # v4.4.0 # gh-action-pulse: ignore[max-age]
- uses: some/old-action@def456 # v1.2.3 # gh-action-pulse: ignore[max-age, min-age, nodejs-version]
The matching step is skipped for that line, the CLI reports the skip, and the run does not fail for that check. ignore[min-age] still rewrites the line, but selects the newest tag without waiting for --min-age. Unknown check ids are reported as warnings and do not skip anything. The hint stays on the line when the reference is rewritten.
To change a threshold for one uses: line instead of skipping the check, add an override hint. Several assignments can be comma-separated inside the brackets:
- uses: actions/setup-node@abc123 # v4.4.0 # gh-action-pulse: override[max-age=200]
- uses: some/old-action@def456 # v1.2.3 # gh-action-pulse: override[max-age=200, min-age=3, nodejs-version=20]
The matching check still runs, but uses the per-line value. ignore[...] on the same line wins over override[...] for that check. Unknown keys and out-of-range values are reported as warnings and are not applied. The hint stays on the line when the reference is rewritten.
Setup
gh-action-pulse talks to the GitHub API.
Set a token explicitly:
export GITHUB_TOKEN=your_github_token_here
If GITHUB_TOKEN is not set, the tool can fall back to using the GitHub CLI authentication flow when gh is available.
The project currently requires Python >= 3.14.
Installation
Install from PyPI with uv
uv tool install gh-action-pulse
Install from PyPI with pipx
pipx install gh-action-pulse
Install the local checkout
uv tool install . --force --reinstall
Using it in GitHub Actions
gh-action-pulse is a CLI. Two complementary workflows cover the usual split:
- Policy check on pull requests and the default branch:
--dry-runfails only when an action is too old, runs on Node.js below 24, or is archived. Proposed SHA pins and redirects do not fail the job. - Scheduled pull request: apply rewrites and open a PR when files actually changed. Routine version bumps on already-pinned actions can stay with Dependabot.
The YAML below is a simplified starting point (pip + GITHUB_TOKEN). This repository runs fuller versions (mise, GitHub App token, extra cleanup): .github/workflows/gh-action-pulse.yml and .github/workflows/gh-action-pulse-pr.yml.
Policy check (CI)
Use a high --max-age so the job stays green while Dependabot or the scheduled workflow refresh pins. It should fail when an upstream tag is genuinely stale (exit 5) or the action (including composite dependencies) still runs on Node.js below 24 (exit 3). Archived upstream repositories also fail the run (exit 4).
--min-age 7 and --minimum-nodejs-version 24 are already the CLI defaults; they are spelled out here so the policy is obvious in the workflow file. --min-age 7 also matches the scheduled recipe and a typical Dependabot cooldown.
name: Actions policy
on:
pull_request:
push:
branches: [main]
permissions:
contents: read
jobs:
gh-action-pulse:
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
with:
python-version: "3.14"
- name: Install gh-action-pulse
run: pip install gh-action-pulse
- name: Check action freshness and Node.js runtime
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
gh-action-pulse --dry-run \
--min-age 7 \
--max-age 365 \
--minimum-nodejs-version 24
Scheduled pull request
Run without --dry-run on a weekly schedule (and on demand). peter-evans/create-pull-request opens a PR only when gh-action-pulse wrote changes; it no-ops on a clean tree.
--min-age 7 waits out brand-new tags. That pairs well with a Dependabot cooldown of about a week, so the two bots are not racing to adopt a tag that appeared yesterday.
name: Update Actions pins
on:
schedule:
- cron: "0 10 * * 3"
workflow_dispatch:
permissions:
contents: write
pull-requests: write
jobs:
gh-action-pulse-pr:
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
ref: ${{ github.event.repository.default_branch }}
- uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
with:
python-version: "3.14"
- name: Install gh-action-pulse
run: pip install gh-action-pulse
- name: Update action references
id: pulse
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
set +e
gh-action-pulse \
--min-age 7 \
--max-age 365 \
--minimum-nodejs-version 24
status=$?
echo "exit-code=${status}" >> "$GITHUB_OUTPUT"
exit 0
- uses: peter-evans/create-pull-request@5f6978faf089d4d20b00c7766989d076bb2fc7f1 # v8.1.1
with:
commit-message: "chore: update GitHub Actions references"
branch: chore/gh-action-pulse
title: "chore: update GitHub Actions references"
body: |
Automated pin and redirect updates from `gh-action-pulse`.
Routine version bumps on already-pinned actions are still
expected from Dependabot.
- name: Fail on gh-action-pulse errors
if: steps.pulse.outputs.exit-code != '0'
env:
PULSE_EXIT_CODE: ${{ steps.pulse.outputs.exit-code }}
run: exit "$PULSE_EXIT_CODE"
Pulse writes rewrites first, then exits non-zero on Node.js or --max-age failures. The steps above still open the PR, then fail the job so a stale or too-old Node.js action stays visible.
Pulse always rewrites files under .github/workflows. The default GITHUB_TOKEN cannot push workflow-file changes, and pull requests it opens do not start other workflows. For a production bot (including this repository), use a GitHub App token with contents: write, pull-requests: write, and workflows: write, or a fine-grained PAT with the equivalent permissions. See .github/workflows/gh-action-pulse-pr.yml for the App-token variant.
Dependabot
Keep a github-actions Dependabot update (optionally grouped minor/patch with a cooldown). Dependabot bumps existing owner/repo@sha pins when a newer release exists. gh-action-pulse additionally:
- converts
owner/repo@v2/@maininto a pinned SHA with a version comment; - rewrites redirected action repositories to their canonical name;
- ignores tags younger than
--min-agewhen choosing a recommendation; - fails the policy job on Node.js runtime,
--max-age, or archived upstreams.
The two overlap on “newer SHA for the same action”. That is expected. Pulse PRs should stay rare if Dependabot is healthy.
CLI Usage
Run against the current repository:
gh-action-pulse
Preview changes without writing files:
gh-action-pulse --dry-run
Require action tags to be at least 14 days old before they can be selected:
gh-action-pulse --min-age 14
Fail when the newest eligible tag is older than 180 days:
gh-action-pulse --min-age 14 --max-age 180
Require actions to run on at least Node.js 20:
gh-action-pulse --minimum-nodejs-version 20
Show more detail while debugging:
gh-action-pulse --log-level DEBUG
Print the installed version:
gh-action-pulse --version
Output
The CLI uses Rich for progress and summaries on stderr:
- a progress bar while enriching actions from the GitHub API (and while checking Node.js runtimes);
- colored phase lines for scan, freshness, and Node.js checks;
- a table of proposed or applied
uses:rewrites (yellow header in--dry-run); - a table of checks skipped by
# gh-action-pulse: ignore[...]hints; - a table of per-line thresholds from
# gh-action-pulse: override[...]hints; - a closing summary panel with update counts, warnings, and the exit code.
Routine per-file and per-action chatter is logged at DEBUG. Use --log-level DEBUG (or WARNING / ERROR) when you need diagnostic detail; warnings and errors still use Rich-formatted logging without repeating the main user-facing summary.
CLI Options
--dry-run(GH_ACTION_PULSE_DRY_RUN): show the updates without writing files.--log-level(GH_ACTION_PULSE_LOG_LEVEL): set the logging level (DEBUG,INFO,WARNING,ERROR,CRITICAL).--min-age(GH_ACTION_PULSE_MIN_AGE): require tags to be at least this many days old before recommending them.--max-age(GH_ACTION_PULSE_MAX_AGE): fail when the chosen--min-age-eligible upstream tag is older than this many days. Use0to disable the check.--minimum-nodejs-version(GH_ACTION_PULSE_MINIMUM_NODEJS_VERSION): fail when an action, or any of its composite/local dependencies, runs on a Node.js major version below this value (default24). Use0to disable the check.--version: print the package version and exit.
CLI flags override the matching environment variables when both are set.
Exit Codes
gh-action-pulse uses its exit code to signal the outcome of a run:
0: the run completed and no failing condition was detected.2: GitHub authentication failed (GITHUB_TOKENcould not be resolved).3: a Node.js runtime problem was detected in the repository. When an action, or any of its composite/local composite dependencies, runs on a Node.js major version below--minimum-nodejs-version(default24), the tool logs an error and exits with status3. Set--minimum-nodejs-version 0to disable this check.4: a referenced upstream action repository is archived.5: a--max-agestaleness failure occurred.6: auses:reference does not exist upstream (neither a branch, a tag nor a commit SHA); every invalid reference is listed with itsfile:line.
When multiple failing conditions are detected in the same run, the exit code with the lowest number is returned: authentication (2), archived repositories (4) and invalid references (6) stop the run early, and among end-of-run checks the Node.js exit code (3) takes precedence over stale tags (5).
Limitations
- Local actions such as
./.github/actions/my-actionare not part of the GitHub API lookup flow. - Recommendations work best with repositories exposing SemVer tags. Without them, the tool uses a degraded mode (non-SemVer version tags, then the pinned tag or branch pinned to its SHA) and lists those actions under "Degraded recommendations" (see rules.md).
- The tool needs GitHub API access, so rate limits and authentication still apply.
Node.js version check (--minimum-nodejs-version)
The Node.js runtime check does not inspect every uses: line in the repository. In practice it:
- only starts from remote GitHub Actions referenced as
owner/repo@ref(orowner/repo/path@ref) in.github/workflowsand.github/actions; - skips local actions such as
uses: ./.github/actions/my-actionbecause they do not match thename@referencepattern used during scanning; - inspects the recommended upstream reference (the one the tool would update to), not the currently pinned reference when a recommendation exists;
- only flags JavaScript actions whose manifest declares
runs.using: nodeXX(for examplenode20,node24); - skips Docker actions (
docker://), unresolvable references, missing manifests, and other non-nodeXXruntimes; - walks composite actions recursively and checks nested
uses:dependencies, including relative./pathsteps inside a remote composite action (resolved within that upstream repository); - does not meaningfully check reusable workflows referenced as
uses: org/repo/.github/workflows/foo.yml@ref, because it looks foraction.yml/action.yamlmanifests rather than workflow files.
Set --minimum-nodejs-version 0 to disable this check entirely.
Ignore hints (# gh-action-pulse: ignore[...])
Ignore hints apply only to the uses: line they are written on. Supported check ids:
max-age: skip the--max-agestale-tag failure for that linemin-age: recommend the newest SemVer tag for that line without waiting for--min-agenodejs-version: skip the--minimum-nodejs-versioncheck for that line, including its composite dependencies
The hint must be a trailing comment on the uses: line itself (a comment on the previous YAML line is not read). Extra comments are preserved when the line is rewritten. A config-file ignore list is not implemented yet; see the roadmap below.
Override hints (# gh-action-pulse: override[...])
Override hints apply only to the uses: line they are written on. Supported assignments (quoted and unquoted keys/values are both accepted):
max-age: use this many days as the stale-tag limit for that linemin-age: wait this many days before recommending a SemVer tag for that linenodejs-version: require this Node.js major version for that line, including its composite dependencies
Several assignments can be comma-separated: override[max-age=200, min-age=3, nodejs-version=20]. Duplicate keys keep the last value. 0 disables that check for the line. An ignore[...] hint for the same check on the same line takes precedence. Unknown keys and values outside the CLI bounds are reported as warnings and ignored.
The hint must be a trailing comment on the uses: line itself. Extra comments are preserved when the line is rewritten.
Roadmap
Possible future improvements:
- Maybe Separate unit tests with appropriate workflow (pytest) if checks takes times
- Add E2E tests with appropriate workflow (pytest and/or bats)
- Change to versioned version of tools in mise.toml when near stable version (could depend on tools)
Potential future CLI options (to get the idea):
--config-file: load configuration, including ignore parameters or specific rules for some workflows (needs thinking).--only-workflow: restrict scanning to a specific workflow.--workflow-omit: exclude specific workflows from scanning.--github-action-omit: exclude specific GitHub Actions from the checks.
Contributing
Contributions are welcome. See CONTRIBUTING.md for the code layout, local setup, and the full list of linters and checks run in CI.
Release files for gh-action-pulse 1.5.1
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| gh_action_pulse-1.5.1-py3-none-any.whl | Python 3 | none | any | Details |
Release files / gh_action_pulse-1.5.1-py3-none-any.whl
| Download URL | gh_action_pulse-1.5.1-py3-none-any.whl |
|---|---|
| Size | 49.1 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
5bd8821ea8702c972f557ce47828e0b2639c189d2d95b91e0b86b5b9127354ba
|
|
BLAKE2b-256 checksum How to use checksums |
ed03aaf841e4af8316d0081c973d8208054e98a335ea33e5b48acd95d96e30da
|
| 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 25, 2026.
Transparency log