From 0437eeb504ea9a9dc8c6dcbdd73002ed91b70bdb Mon Sep 17 00:00:00 2001 From: tehw0lf Date: Sat, 25 Jul 2026 23:13:39 +0200 Subject: [PATCH 1/3] feat(security): run npm audit auto-fix on scheduled scans The auto-fix was gated on github.actor == 'dependabot[bot]' and a PR head_ref, so scheduled security scans found vulnerabilities but never fixed them. A newly disclosed advisory sat until the next Dependabot run touched the same lockfile. Add a 'scheduled' mode that opens the fix PR against the default branch instead of a Dependabot branch, so it can be merged directly. It reuses a stable audit-fix/scheduled branch and edits the existing PR rather than opening a new one on every cron tick. Both modes keep npm audit fix without --force: only semver-compatible updates, package.json untouched, remaining findings reported in the PR body. Also move inline ${{ }} interpolations in run blocks to env vars. --- .github/workflows/npm-audit-autofix.yml | 147 ++++++++++++++++----- .github/workflows/security-scan-source.yml | 48 ++++++- CLAUDE.md | 31 +++++ 3 files changed, 186 insertions(+), 40 deletions(-) diff --git a/.github/workflows/npm-audit-autofix.yml b/.github/workflows/npm-audit-autofix.yml index 01dc092..608513e 100644 --- a/.github/workflows/npm-audit-autofix.yml +++ b/.github/workflows/npm-audit-autofix.yml @@ -1,11 +1,28 @@ -name: npm audit auto-fix (Dependabot) +name: npm audit auto-fix on: workflow_call: inputs: + mode: + description: + "dependabot = PR against the Dependabot branch (per-SHA branch); + scheduled = PR against the default branch (reused fix branch)" + default: "dependabot" + required: false + type: string head_ref: - description: "branch that triggered the workflow (github.head_ref from the caller)" - required: true + description: + "branch that triggered the workflow (github.head_ref from the + caller) - required for mode=dependabot, ignored for mode=scheduled" + default: "" + required: false + type: string + base_branch: + description: + "base branch for the fix PR in mode=scheduled (defaults to the + repository default branch)" + default: "" + required: false type: string root_dir: description: "path to project root (same as in security-scan-source)" @@ -46,7 +63,7 @@ on: jobs: audit_fix: - name: npm audit fix → PR on Dependabot branch + name: npm audit fix → PR runs-on: ${{ inputs.runner }} timeout-minutes: 15 permissions: @@ -61,19 +78,32 @@ jobs: working-directory: ${{ inputs.root_dir }} steps: - - name: validate head_ref + - name: validate inputs env: + MODE: ${{ inputs.mode }} HEAD_REF: ${{ inputs.head_ref }} run: | - if [ -z "$HEAD_REF" ]; then - echo "::error::head_ref input is empty – cannot determine Dependabot branch name" - exit 1 - fi + case "$MODE" in + dependabot) + if [ -z "$HEAD_REF" ]; then + echo "::error::head_ref is required for mode=dependabot – cannot determine the Dependabot branch name" + exit 3 + fi + ;; + scheduled) + ;; + *) + echo "::error::invalid mode '$MODE' – expected 'dependabot' or 'scheduled'" + exit 3 + ;; + esac - name: checkout uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: - ref: ${{ inputs.head_ref }} + ref: + ${{ inputs.mode == 'dependabot' && inputs.head_ref || + inputs.base_branch }} token: ${{ secrets.GITHUB_TOKEN }} fetch-depth: 0 @@ -173,42 +203,63 @@ jobs: console.log(`${result.length} package(s) updated`); EOF - - name: compute branch name + - name: compute branch names id: meta if: steps.changes.outputs.has_changes == 'true' env: + MODE: ${{ inputs.mode }} HEAD_REF: ${{ inputs.head_ref }} + BASE_INPUT: ${{ inputs.base_branch }} + DEFAULT_BRANCH: ${{ github.event.repository.default_branch }} SHA: ${{ github.sha }} run: | - DEP_BRANCH="$HEAD_REF" - SHORT_SHA="$SHA" - SHORT_SHA="${SHORT_SHA:0:7}" - FIX_BRANCH="audit-fix/${DEP_BRANCH#dependabot/}-${SHORT_SHA}" - echo "fix_branch=$FIX_BRANCH" >> "$GITHUB_OUTPUT" - echo "dep_branch=$DEP_BRANCH" >> "$GITHUB_OUTPUT" + if [ "$MODE" = "dependabot" ]; then + # per-SHA branch: each Dependabot push gets its own fix PR + BASE_BRANCH="$HEAD_REF" + SHORT_SHA="${SHA:0:7}" + FIX_BRANCH="audit-fix/${HEAD_REF#dependabot/}-${SHORT_SHA}" + else + # stable branch: the scheduled run reuses one PR instead of + # opening a new one on every cron tick + BASE_BRANCH="${BASE_INPUT:-$DEFAULT_BRANCH}" + FIX_BRANCH="audit-fix/scheduled" + fi + + echo "fix_branch=$FIX_BRANCH" >> "$GITHUB_OUTPUT" + echo "base_branch=$BASE_BRANCH" >> "$GITHUB_OUTPUT" - name: commit and push if: steps.changes.outputs.has_changes == 'true' + env: + MODE: ${{ inputs.mode }} + FIX_BRANCH: ${{ steps.meta.outputs.fix_branch }} + BASE_BRANCH: ${{ steps.meta.outputs.base_branch }} run: | git config user.name "github-actions[bot]" git config user.email "github-actions[bot]@users.noreply.github.com" - git checkout -b "${{ steps.meta.outputs.fix_branch }}" + git checkout -b "$FIX_BRANCH" git add package-lock.json + git commit -m "fix(deps): npm audit fix on $BASE_BRANCH" - git commit -m "fix(deps): npm audit fix on ${{ steps.meta.outputs.dep_branch }}" - git push origin "${{ steps.meta.outputs.fix_branch }}" + if [ "$MODE" = "scheduled" ]; then + # the fix branch is disposable and rebuilt from base on every run, + # so overwriting a stale one is the intended update path + git push --force origin "$FIX_BRANCH" + else + git push origin "$FIX_BRANCH" + fi - name: open pull request id: open_pr if: steps.changes.outputs.has_changes == 'true' env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + MODE: ${{ inputs.mode }} + CLEAN: ${{ steps.verify.outputs.clean }} + BASE_BRANCH: ${{ steps.meta.outputs.base_branch }} + FIX_BRANCH: ${{ steps.meta.outputs.fix_branch }} run: | - CLEAN="${{ steps.verify.outputs.clean }}" - DEP_BRANCH="${{ steps.meta.outputs.dep_branch }}" - FIX_BRANCH="${{ steps.meta.outputs.fix_branch }}" - if [ "$CLEAN" = "true" ]; then AUDIT_STATUS="✅ \`npm audit\` is **clean** after this fix – CI should pass." else @@ -227,9 +278,24 @@ jobs: }); ") + if [ "$MODE" = "dependabot" ]; then + TRIGGER="\`npm audit fix\` was run automatically because \`npm audit\` failed on Dependabot PR \`$BASE_BRANCH\`." + MERGE_NOTE="### Merge order + 1. Merge this PR into \`$BASE_BRANCH\` (CI must be green) + 2. Dependabot merges \`$BASE_BRANCH\` into main as usual" + PR_TITLE="fix(deps): npm audit fix for ${BASE_BRANCH}" + else + TRIGGER="\`npm audit fix\` was run automatically because the scheduled security scan found vulnerabilities on \`$BASE_BRANCH\`." + MERGE_NOTE="### Merge order + Merge directly into \`$BASE_BRANCH\` once CI is green. + + > This PR is updated in place by later scheduled runs – the branch \`$FIX_BRANCH\` is force-pushed, so local checkouts need a fresh pull." + PR_TITLE="fix(deps): npm audit fix (scheduled security scan)" + fi + PR_BODY="## 🔒 npm audit auto-fix - \`npm audit fix\` was run automatically because \`npm audit\` failed on Dependabot PR \`$DEP_BRANCH\`. + $TRIGGER ### Updated packages @@ -242,29 +308,38 @@ jobs: ### What changed? Only \`package-lock.json\` – no changes to \`package.json\`. - ### Merge order - 1. Merge this PR into \`$DEP_BRANCH\` (CI must be green) - 2. Dependabot merges \`$DEP_BRANCH\` into main as usual" + $MERGE_NOTE" - PR_URL=$(gh pr create \ - --base "$DEP_BRANCH" \ - --head "$FIX_BRANCH" \ - --title "fix(deps): npm audit fix for ${DEP_BRANCH}" \ - --body "$PR_BODY") + EXISTING=$(gh pr list --head "$FIX_BRANCH" --base "$BASE_BRANCH" \ + --state open --json number --jq '.[0].number // empty') + + if [ -n "$EXISTING" ]; then + gh pr edit "$EXISTING" --title "$PR_TITLE" --body "$PR_BODY" + PR_URL=$(gh pr view "$EXISTING" --json url --jq '.url') + echo "♻️ Existing PR updated: $PR_URL" + else + PR_URL=$(gh pr create \ + --base "$BASE_BRANCH" \ + --head "$FIX_BRANCH" \ + --title "$PR_TITLE" \ + --body "$PR_BODY") + echo "✅ PR opened: $PR_URL" + fi echo "pr_url=$PR_URL" >> "$GITHUB_OUTPUT" - echo "✅ PR opened: $PR_URL" - name: job summary if: always() env: - HEAD_REF: ${{ inputs.head_ref }} + MODE: ${{ inputs.mode }} + BASE_BRANCH: ${{ steps.meta.outputs.base_branch }} run: | { echo "## npm audit auto-fix" echo "| | |" echo "|---|---|" - echo "| Dependabot branch | \`$HEAD_REF\` |" + echo "| Mode | \`$MODE\` |" + echo "| Base branch | \`${BASE_BRANCH:-—}\` |" echo "| Changes found | ${{ steps.changes.outputs.has_changes }} |" if [ "${{ steps.changes.outputs.has_changes }}" = "true" ]; then echo "| Audit clean after fix | ${{ steps.verify.outputs.clean }} |" diff --git a/.github/workflows/security-scan-source.yml b/.github/workflows/security-scan-source.yml index 65c7543..6ecd618 100644 --- a/.github/workflows/security-scan-source.yml +++ b/.github/workflows/security-scan-source.yml @@ -40,6 +40,20 @@ on: default: "moderate" required: false type: string + enable_npm_audit_autofix: + description: + "run npm audit fix and open a PR when npm audit fails (dependabot + PRs and scheduled/manual runs)" + default: true + required: false + type: boolean + audit_fix_base_branch: + description: + "base branch for the scheduled auto-fix PR (defaults to the + repository default branch)" + default: "" + required: false + type: string outputs: semgrep_result: description: "semgrep scan result" @@ -54,14 +68,20 @@ on: description: "pip-audit result" value: ${{ jobs.scan_source.outputs.pip_audit_result }} npm_audit_fix_branch: - description: "fix branch created for dependabot npm audit auto-fix" - value: ${{ jobs.dependabot-audit-fix.outputs.fix_branch }} + description: "fix branch created by the npm audit auto-fix" + value: + ${{ jobs.dependabot-audit-fix.outputs.fix_branch || + jobs.scheduled-audit-fix.outputs.fix_branch }} npm_audit_fix_pr: description: "url of the auto-fix PR" - value: ${{ jobs.dependabot-audit-fix.outputs.fix_pr_url }} + value: + ${{ jobs.dependabot-audit-fix.outputs.fix_pr_url || + jobs.scheduled-audit-fix.outputs.fix_pr_url }} npm_audit_clean_after_fix: description: "whether audit is clean after auto-fix" - value: ${{ jobs.dependabot-audit-fix.outputs.audit_clean_after_fix }} + value: + ${{ jobs.dependabot-audit-fix.outputs.audit_clean_after_fix || + jobs.scheduled-audit-fix.outputs.audit_clean_after_fix }} jobs: scan_source: @@ -209,14 +229,34 @@ jobs: needs: scan_source if: | always() + && inputs.enable_npm_audit_autofix && needs.scan_source.outputs.npm_audit_result == 'failure' && github.actor == 'dependabot[bot]' && inputs.head_ref != '' && !startsWith(inputs.head_ref, 'audit-fix/') uses: ./.github/workflows/npm-audit-autofix.yml with: + mode: "dependabot" head_ref: ${{ inputs.head_ref }} root_dir: ${{ inputs.root_dir }} node_version: "24.16.0" omit_dev: ${{ inputs.npm_audit_omit_dev }} severity_threshold: ${{ inputs.npm_audit_severity_threshold }} + + scheduled-audit-fix: + needs: scan_source + # scheduled/manual runs have no PR branch: fix against the default branch + # so the resulting PR can be merged directly + if: | + always() + && inputs.enable_npm_audit_autofix + && needs.scan_source.outputs.npm_audit_result == 'failure' + && (github.event_name == 'schedule' || github.event_name == 'workflow_dispatch') + uses: ./.github/workflows/npm-audit-autofix.yml + with: + mode: "scheduled" + base_branch: ${{ inputs.audit_fix_base_branch }} + root_dir: ${{ inputs.root_dir }} + node_version: "24.16.0" + omit_dev: ${{ inputs.npm_audit_omit_dev }} + severity_threshold: ${{ inputs.npm_audit_severity_threshold }} diff --git a/CLAUDE.md b/CLAUDE.md index 02b0f93..1671462 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -101,6 +101,37 @@ Scans source code and dependencies **before building**: - Uploads SARIF reports to GitHub Security tab - Fails fast to prevent building vulnerable code +##### npm audit auto-fix (`npm-audit-autofix.yml`) +When `npm audit` fails, this workflow runs `npm audit fix` and opens a PR with the +resulting `package-lock.json` changes. It has two modes, both dispatched +automatically from `security-scan-source.yml`: + +| | `dependabot` | `scheduled` | +|---|---|---| +| Trigger | `github.actor == 'dependabot[bot]'` on a PR | `schedule` or `workflow_dispatch` | +| PR base | the Dependabot branch | default branch (or `audit_fix_base_branch`) | +| Fix branch | `audit-fix/-` (one per push) | `audit-fix/scheduled` (reused) | +| Existing PR | new PR each time | updated in place, branch is force-pushed | + +The `scheduled` mode is the low-intervention path: the daily scan finds a new +advisory, fixes it, and leaves a single mergeable PR against `main`. Because the +fix branch is rebuilt from base on every run, later runs force-push it and edit +the same PR instead of piling up duplicates. + +Both modes run `npm audit fix` **without** `--force`, so only semver-compatible +updates are applied and `package.json` is never touched. Findings that need a +major upgrade stay open and are called out in the PR body. + +**Configuration:** +```yaml +inputs: + enable_npm_audit_autofix: true # Enable/disable both modes (default: enabled) + audit_fix_base_branch: "" # Base for scheduled PRs (default: repo default branch) +``` + +Calling workflows need `contents: write` and `pull-requests: write` for the +auto-fix job to push the branch and open the PR. + #### Layer 2: Post-Build Artifact Scan (`security-scan-artifacts.yml`) Scans **build artifacts** before publishing: - **Trivy**: Comprehensive filesystem scanner for packages and dependencies From 2d17c6415aacf1a4ff0b70b816bc846ad987b448 Mon Sep 17 00:00:00 2001 From: tehw0lf Date: Sat, 25 Jul 2026 23:18:42 +0200 Subject: [PATCH 2/3] feat(security): extend npm audit auto-fix to regular PR branches The auto-fix only triggered for Dependabot PRs and scheduled runs, so a feature branch whose commit broke npm audit got a red scan and no fix. Rename mode 'dependabot' to 'branch' and drop the actor check: any PR branch in this repo now gets a fix PR against itself, per commit. The mechanics were already correct for this, only the gating and the Dependabot-specific PR wording needed to change - the body now adapts based on the actor. Fork PRs are excluded via head.repo.full_name: their GITHUB_TOKEN is read-only so the push would fail, and covering them would require pull_request_target, which runs untrusted code with write permissions. --- .github/workflows/npm-audit-autofix.yml | 43 ++++++++++++++-------- .github/workflows/security-scan-source.yml | 19 ++++++---- CLAUDE.md | 19 ++++++++-- 3 files changed, 54 insertions(+), 27 deletions(-) diff --git a/.github/workflows/npm-audit-autofix.yml b/.github/workflows/npm-audit-autofix.yml index 608513e..e600dca 100644 --- a/.github/workflows/npm-audit-autofix.yml +++ b/.github/workflows/npm-audit-autofix.yml @@ -5,15 +5,16 @@ on: inputs: mode: description: - "dependabot = PR against the Dependabot branch (per-SHA branch); - scheduled = PR against the default branch (reused fix branch)" - default: "dependabot" + "branch = PR against the triggering branch, one per SHA (covers + Dependabot and regular feature branches); scheduled = PR against the + default branch, reusing a single fix branch" + default: "branch" required: false type: string head_ref: description: "branch that triggered the workflow (github.head_ref from the - caller) - required for mode=dependabot, ignored for mode=scheduled" + caller) - required for mode=branch, ignored for mode=scheduled" default: "" required: false type: string @@ -84,16 +85,16 @@ jobs: HEAD_REF: ${{ inputs.head_ref }} run: | case "$MODE" in - dependabot) + branch) if [ -z "$HEAD_REF" ]; then - echo "::error::head_ref is required for mode=dependabot – cannot determine the Dependabot branch name" + echo "::error::head_ref is required for mode=branch – cannot determine the branch to fix" exit 3 fi ;; scheduled) ;; *) - echo "::error::invalid mode '$MODE' – expected 'dependabot' or 'scheduled'" + echo "::error::invalid mode '$MODE' – expected 'branch' or 'scheduled'" exit 3 ;; esac @@ -102,7 +103,7 @@ jobs: uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: ref: - ${{ inputs.mode == 'dependabot' && inputs.head_ref || + ${{ inputs.mode == 'branch' && inputs.head_ref || inputs.base_branch }} token: ${{ secrets.GITHUB_TOKEN }} fetch-depth: 0 @@ -213,11 +214,12 @@ jobs: DEFAULT_BRANCH: ${{ github.event.repository.default_branch }} SHA: ${{ github.sha }} run: | - if [ "$MODE" = "dependabot" ]; then - # per-SHA branch: each Dependabot push gets its own fix PR + if [ "$MODE" = "branch" ]; then + # per-SHA branch: each push to the source branch gets its own fix PR BASE_BRANCH="$HEAD_REF" SHORT_SHA="${SHA:0:7}" - FIX_BRANCH="audit-fix/${HEAD_REF#dependabot/}-${SHORT_SHA}" + SLUG="${HEAD_REF#dependabot/}" + FIX_BRANCH="audit-fix/${SLUG}-${SHORT_SHA}" else # stable branch: the scheduled run reuses one PR instead of # opening a new one on every cron tick @@ -259,6 +261,7 @@ jobs: CLEAN: ${{ steps.verify.outputs.clean }} BASE_BRANCH: ${{ steps.meta.outputs.base_branch }} FIX_BRANCH: ${{ steps.meta.outputs.fix_branch }} + IS_DEPENDABOT: ${{ github.actor == 'dependabot[bot]' }} run: | if [ "$CLEAN" = "true" ]; then AUDIT_STATUS="✅ \`npm audit\` is **clean** after this fix – CI should pass." @@ -278,12 +281,22 @@ jobs: }); ") - if [ "$MODE" = "dependabot" ]; then - TRIGGER="\`npm audit fix\` was run automatically because \`npm audit\` failed on Dependabot PR \`$BASE_BRANCH\`." - MERGE_NOTE="### Merge order + if [ "$MODE" = "branch" ]; then + PR_TITLE="fix(deps): npm audit fix for ${BASE_BRANCH}" + if [ "$IS_DEPENDABOT" = "true" ]; then + TRIGGER="\`npm audit fix\` was run automatically because \`npm audit\` failed on Dependabot PR \`$BASE_BRANCH\`." + MERGE_NOTE="### Merge order 1. Merge this PR into \`$BASE_BRANCH\` (CI must be green) 2. Dependabot merges \`$BASE_BRANCH\` into main as usual" - PR_TITLE="fix(deps): npm audit fix for ${BASE_BRANCH}" + else + TRIGGER="\`npm audit fix\` was run automatically because \`npm audit\` failed on branch \`$BASE_BRANCH\`." + MERGE_NOTE="### Merge order + 1. Merge this PR into \`$BASE_BRANCH\` (CI must be green) + 2. Continue with your own PR as usual + + > A new fix PR is opened per commit. If you already fixed the finding + > yourself, just close this one." + fi else TRIGGER="\`npm audit fix\` was run automatically because the scheduled security scan found vulnerabilities on \`$BASE_BRANCH\`." MERGE_NOTE="### Merge order diff --git a/.github/workflows/security-scan-source.yml b/.github/workflows/security-scan-source.yml index 6ecd618..b1de3ed 100644 --- a/.github/workflows/security-scan-source.yml +++ b/.github/workflows/security-scan-source.yml @@ -42,8 +42,8 @@ on: type: string enable_npm_audit_autofix: description: - "run npm audit fix and open a PR when npm audit fails (dependabot - PRs and scheduled/manual runs)" + "run npm audit fix and open a PR when npm audit fails (any PR branch + in this repo, plus scheduled/manual runs)" default: true required: false type: boolean @@ -70,17 +70,17 @@ on: npm_audit_fix_branch: description: "fix branch created by the npm audit auto-fix" value: - ${{ jobs.dependabot-audit-fix.outputs.fix_branch || + ${{ jobs.branch-audit-fix.outputs.fix_branch || jobs.scheduled-audit-fix.outputs.fix_branch }} npm_audit_fix_pr: description: "url of the auto-fix PR" value: - ${{ jobs.dependabot-audit-fix.outputs.fix_pr_url || + ${{ jobs.branch-audit-fix.outputs.fix_pr_url || jobs.scheduled-audit-fix.outputs.fix_pr_url }} npm_audit_clean_after_fix: description: "whether audit is clean after auto-fix" value: - ${{ jobs.dependabot-audit-fix.outputs.audit_clean_after_fix || + ${{ jobs.branch-audit-fix.outputs.audit_clean_after_fix || jobs.scheduled-audit-fix.outputs.audit_clean_after_fix }} jobs: @@ -225,18 +225,21 @@ jobs: ${{ inputs.root_dir }}/semgrep.sarif if-no-files-found: ignore - dependabot-audit-fix: + branch-audit-fix: needs: scan_source + # any PR branch in this repo - Dependabot and regular feature branches + # alike. Forks are excluded: their GITHUB_TOKEN is read-only, so the push + # would fail, and pushing into a fork is not something we want anyway. if: | always() && inputs.enable_npm_audit_autofix && needs.scan_source.outputs.npm_audit_result == 'failure' - && github.actor == 'dependabot[bot]' && inputs.head_ref != '' && !startsWith(inputs.head_ref, 'audit-fix/') + && github.event.pull_request.head.repo.full_name == github.repository uses: ./.github/workflows/npm-audit-autofix.yml with: - mode: "dependabot" + mode: "branch" head_ref: ${{ inputs.head_ref }} root_dir: ${{ inputs.root_dir }} node_version: "24.16.0" diff --git a/CLAUDE.md b/CLAUDE.md index 1671462..fda589a 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -106,18 +106,29 @@ When `npm audit` fails, this workflow runs `npm audit fix` and opens a PR with t resulting `package-lock.json` changes. It has two modes, both dispatched automatically from `security-scan-source.yml`: -| | `dependabot` | `scheduled` | +| | `branch` | `scheduled` | |---|---|---| -| Trigger | `github.actor == 'dependabot[bot]'` on a PR | `schedule` or `workflow_dispatch` | -| PR base | the Dependabot branch | default branch (or `audit_fix_base_branch`) | -| Fix branch | `audit-fix/-` (one per push) | `audit-fix/scheduled` (reused) | +| Trigger | any PR branch in this repo (Dependabot **and** feature branches) | `schedule` or `workflow_dispatch` | +| PR base | the triggering branch | default branch (or `audit_fix_base_branch`) | +| Fix branch | `audit-fix/-` (one per push) | `audit-fix/scheduled` (reused) | | Existing PR | new PR each time | updated in place, branch is force-pushed | +The `branch` mode covers everyday work: push to a feature branch, `npm audit` +fails, and a fix PR is opened against *your* branch. Merge it and carry on — the +fix travels into `main` with your own PR. The PR body adapts to whether the +trigger was Dependabot or a regular branch. + The `scheduled` mode is the low-intervention path: the daily scan finds a new advisory, fixes it, and leaves a single mergeable PR against `main`. Because the fix branch is rebuilt from base on every run, later runs force-push it and edit the same PR instead of piling up duplicates. +**Fork PRs are skipped.** A `pull_request` from a fork gets a read-only +`GITHUB_TOKEN`, so the push could not succeed; the job is skipped via a +`head.repo.full_name == github.repository` guard rather than failing. Fixing +those requires `pull_request_target`, which runs untrusted PR code with write +permissions — deliberately not done here. + Both modes run `npm audit fix` **without** `--force`, so only semver-compatible updates are applied and `package.json` is never touched. Findings that need a major upgrade stay open and are called out in the PR body. From f73b2afd756b19e0d80115f4422fcd80586b41ea Mon Sep 17 00:00:00 2001 From: tehw0lf Date: Sat, 25 Jul 2026 23:24:54 +0200 Subject: [PATCH 3/3] fix(security): checkout the base branch in scheduled audit-fix mode With mode=scheduled and no base_branch input, the checkout ref expression evaluated to an empty string. actions/checkout then falls back to the ref of the triggering event, not the default branch - so a workflow_dispatch from another branch would fix that branch while opening the PR against the default branch. Apply the same default_branch fallback the compute-branch-names step already uses, keeping checkout and PR base in sync. Reported by CodeRabbit on #116. --- .github/workflows/npm-audit-autofix.yml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/.github/workflows/npm-audit-autofix.yml b/.github/workflows/npm-audit-autofix.yml index e600dca..788dfe0 100644 --- a/.github/workflows/npm-audit-autofix.yml +++ b/.github/workflows/npm-audit-autofix.yml @@ -102,9 +102,12 @@ jobs: - name: checkout uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: + # mode=scheduled must fix the base branch, not the ref that + # triggered the run - an empty ref would make checkout fall back to + # the event ref, which differs from the PR base on workflow_dispatch ref: ${{ inputs.mode == 'branch' && inputs.head_ref || - inputs.base_branch }} + inputs.base_branch || github.event.repository.default_branch }} token: ${{ secrets.GITHUB_TOKEN }} fetch-depth: 0