Actions Up scans your workflows and composite actions to discover every referenced GitHub Action, then checks for newer releases.
Interactively upgrade and pin actions to exact commit SHAs for secure, reproducible CI, or preserve tag-style references when you need to stay on tags.
.github/workflows/*.yml) and
composite actions (.github/actions/*/action.yml and root
action.yml/action.yaml)--style preserve
Keeping GitHub Actions updated is critical and time-consuming. Actions Up scans all workflows, highlights available updates, and can pin actions to SHAs for reproducibility.
| Without Actions Up | With Actions Up |
|---|---|
| Check each action manually | Scan all workflows in seconds |
| Risk using vulnerable versions | SHA pinning for maximum security |
| 30+ minutes per repository | Under 1 minute total |
GitHub Actions run arbitrary code in your CI. If a job has secrets available, any action used in that job can read the environment and exfiltrate those secrets. A compromised action or a mutable version tag is a direct path to leakage.
Actions Up reduces risk by:
Note: secrets are available on push, workflow_dispatch, schedule, and
pull_request_target triggers (and on fork PRs if explicitly enabled). Always
scope workflow permissions to the minimum required.
Quick use (no installation)
npx actions-up
Global installation
npm install -g actions-up
Per-project
npm install --save-dev actions-up
Alternatively, you can install Actions Up with Homebrew
brew install actions-up
Run in your repository root:
npx actions-up
This will:
.github/workflows/*.yml and .github/actions/*/action.yml files,
plus root action.yml/action.yamlSkip all prompts and update everything:
npx actions-up --yes
# or
npx actions-up -y
Check for updates without making any changes:
npx actions-up --dry-run
Output a machine-readable JSON report instead of the interactive UI:
npx actions-up --json
--json is report-only: it never writes files, skips the interactive prompt,
and cannot be combined with --yes.
By default, Actions Up scans .github.
Use --dir to choose another directory, and pass it multiple times to scan
several directories:
npx actions-up --dir .gitea
npx actions-up --dir .github --dir ./other/.github
Use --recursive (-r) to scan YAML workflow/composite-action files
recursively in the selected directories:
npx actions-up -r
npx actions-up --dir ./gh-repo-defaults -r
When --recursive is used without --dir, Actions Up scans from the current
directory (.).
By default, actions pinned to branch refs (e.g., @main, @release/v1) are
skipped to avoid changing intentionally floating references. Skipped entries are
listed in the output. To include them in update checks, pass
--include-branches.
Use --quiet (-q) to hide the skipped and blocked-update warnings (for
example, actions intentionally pinned to branches). Other output — results,
applied updates, and errors — is unchanged.
npx actions-up --yes --quiet
By default, Actions Up allows major updates. Use --mode to limit updates:
npx actions-up --mode minor
npx actions-up --mode patch
In minor and patch modes, Actions Up tries to find the newest compatible tag
first (for example, from @v4 in minor mode it will choose the latest
v4.x.y). If no compatible version exists, that action is skipped.
By default, Actions Up writes updates as pinned SHAs:
npx actions-up --style sha
Use --style preserve to keep the current reference style:
npx actions-up --style preserve
preserve keeps tag references on tags and SHA references on SHAs. Tag refs
also keep their granularity, so actions/checkout@v5 updates to
actions/checkout@v6, while actions/checkout@v5.0 updates to
actions/checkout@v6.0. A SHA-pinned action continues updating to the latest
resolved SHA.
You can integrate Actions Up into your CI/CD pipeline to automatically check for outdated actions on every pull request. This helps maintain security and ensures your team stays aware of available updates.
Create .github/workflows/check-actions-updates.yml.
````yaml name: Check for outdated GitHub Actions on: pull_request: types: [edited, opened, synchronize, reopened]
jobs: check-actions: name: Check for GHA updates runs-on: ubuntu-latest permissions: contents: read pull-requests: write issues: write steps: - name: Checkout repository uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
- name: Install actions-up
run: npm install -g actions-up
- name: Run actions-up check
id: actions-check
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
set -euo pipefail
echo "## GitHub Actions Update Check" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
# Run actions-up and capture machine-readable output
echo "Running actions-up to check for updates..."
actions-up --json > actions-up-report.json
UPDATE_COUNT=$(node -pe "JSON.parse(require('node:fs').readFileSync('actions-up-report.json', 'utf8')).summary.totalUpdates")
# Create formatted output
if [ "$UPDATE_COUNT" -gt 0 ]; then
echo "Found $UPDATE_COUNT GitHub Actions with available updates" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "
" >> $GITHUB_STEP_SUMMARY echo "
Click to see JSON report
" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo 'json' >> $GITHUB_STEP_SUMMARY
cat actions-up-report.json >> $GITHUB_STEP_SUMMARY
echo '' >> $GITHUB_STEP_SUMMARY
echo "
" >> $GITHUB_STEP_SUMMARY
# Create detailed markdown report with better formatting
node --input-type=module <<'EOF'
import { readFileSync, writeFileSync } from 'node:fs'
let report = JSON.parse(readFileSync('actions-up-report.json', 'utf8'))
let lines = [
'## GitHub Actions Update Report',
'',
'### Summary',
`- **Updates available:** ${report.summary.totalUpdates}`,
'',
'### Updates',
'',
]
for (let update of report.updates) {
let file = update.action.file ?? 'unknown'
let currentVersion = update.currentVersion ?? 'unknown'
let latestVersion = update.latestVersion ?? 'unknown'
lines.push(
`- \`${update.action.name}\` in \`${file}\`: \`${currentVersion}\` → \`${latestVersion}\``,
)
}
lines.push('')
lines.push('Run `npx actions-up` locally to review and apply updates.')
writeFileSync('actions-up-report.md', lines.join('\n'))
EOF
echo "has-updates=true" >> $GITHUB_OUTPUT
echo "update-count=$UPDATE_COUNT" >> $GITHUB_OUTPUT
else
echo "All GitHub Actions are up to date!" >> $GITHUB_STEP_SUMMARY
{
echo "## GitHub Actions Update Report"
echo ""
echo "### All GitHub Actions in this repository are up to date!"
echo ""
echo "No action required. Your workflows are using the latest versions of all GitHub Actions."
} > actions-up-report.md
echo "has-updates=false" >> $GITHUB_OUTPUT
echo "update-count=0" >> $GITHUB_OUTPUT
fi
- name: Comment PR with updates
if:
github.event_name == 'pull_request' &&
github.event.pull_request.head.repo.full_name == github.repository
uses: actions/github-script@v7
with:
script: |
const fs = require('fs');
const report = fs.readFileSync('actions-up-report.md', 'utf8');
const hasUpdates = '${{ steps.actions-check.outputs.has-updates }}' === 'true';
// Check if we already commented
const comments = await github.paginate(github.rest.issues.listComments, {
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.issue.number
});
const botComment = comments.find(comment =>
comment.user.type === 'Bot' &&
comment.body.includes('GitHub Actions Update Report')
);
const commentBody = `${report}
---
*Generated by [actions-up](https://github.com/azat-io/actions-up) | Last check: ${new Date().toISOString()}*`;
// Only comment if there are updates or if we previously commented
if (hasUpdates || botComment) {
if (botComment) {
// Update existing comment
await github.rest.issues.updateComment({
owner: context.repo.owner,
repo: context.repo.repo,
comment_id: botComment.id,
body: commentBody
});
console.log('Updated existing comment');
} else {
// Create new comment only if there are updates
await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.issue.number,
body: commentBody
});
console.log('Created new comment');
}
} else {
console.log('No updates found and no previous comment exists - skipping comment');
}
// Add or update PR labels based on status
const labels = await github.rest.issues.listLabelsOnIssue({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.issue.number
});
const hasOutdatedLabel = labels.data.some(label => label.name === 'outdated-actions');
if (hasUpdates && !hasOutdatedLabel) {
// Add label if updates are found
try {
await github.rest.issues.addLabels({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.issue.number,
labels: ['outdated-actions']
});
console.log('Added outdated-actions label');
} catch (error) {
console.log('Could not add label (might not exist in repo):', error.message);
}
} else if (!hasUpdates && hasOutdatedLabel) {
// Remove label if no updates
try {
await github.rest.issues.removeLabel({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.issue.number,
name: 'outdated-actions'
});
console.log('Removed outdated-actions label');
} catch (error) {
$ claude mcp add actions-up \
-- python -m otcore.mcp_server <graph>