From 2e9a37aee4f1dba2860910d075018926aa187218 Mon Sep 17 00:00:00 2001 From: 0thernet Date: Sun, 4 Oct 2026 16:58:57 -0400 Subject: [PATCH] Keep existing vulnerabilities in the issue, not a red main A run on main that failed while any vulnerability remained would turn every commit red in about 50 repositories and send a failure email each day, on top of the tracking issue that already records them. Runs on main now stay green once the issue is current, and fail only on scanner or API errors. A pull request still fails when it adds a vulnerability. A manual run on another branch, or a repository with issues turned off (two forks), still fails while vulnerabilities remain, because nothing else would record them. Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- actions/dependency-audit/README.md | 7 ++++++- actions/dependency-audit/index.mjs | 10 +++++++--- actions/dependency-audit/index.test.mjs | 24 +++++++++++++++++++----- scripts/sync-dependency-audit.py | 2 +- 4 files changed, 33 insertions(+), 10 deletions(-) diff --git a/actions/dependency-audit/README.md b/actions/dependency-audit/README.md index 468bfbf..a01a2d6 100644 --- a/actions/dependency-audit/README.md +++ b/actions/dependency-audit/README.md @@ -27,7 +27,12 @@ The workflow checks out the repository and runs the action on `ubuntu-slim`. | Event | Scope | Fails when | Tracking issue | | --- | --- | --- | --- | | Pull request that changes a lockfile | Every lockfile, compared with the same lockfiles on the base commit | The pull request adds a known vulnerability | Not changed | -| Push to `main` that changes a lockfile, daily schedule, manual run | Every lockfile on `main` | Any known vulnerability is present | Opened or updated while vulnerabilities remain; closed with a comment when none remain | +| Push to `main` that changes a lockfile, daily schedule, manual run on `main` | Every lockfile on `main` | Never because of findings | Opened or updated while vulnerabilities remain; closed with a comment when none remain | +| Manual run on another branch, or any run in a repository with issues turned off | Every lockfile on that branch | Any known vulnerability is present | None | + +Existing vulnerabilities live in the issue rather than in a failing run on `main`, so +they do not turn every commit red or send a failure email each day. A scanner or API +error fails any run. The run summary lists the scanned lockfiles and one row per vulnerable package version and advisory: severity, the advisory link on osv.dev, fixed versions above diff --git a/actions/dependency-audit/index.mjs b/actions/dependency-audit/index.mjs index df37d80..4b9fcea 100644 --- a/actions/dependency-audit/index.mjs +++ b/actions/dependency-audit/index.mjs @@ -349,7 +349,8 @@ export async function fetchBaseLockfiles({ github, repository, ref, paths, direc export async function syncIssue({ github, repository, findings, report }) { const vulnerable = findings.filter(finding => !finding.informational); - const open = await github(`/repos/${repository}/issues?state=open&labels=${issueLabel}&per_page=100`); + const open = await github(`/repos/${repository}/issues?state=open&labels=${issueLabel}&per_page=100`, { allow: [410] }); + if (open.status === 410) return { action: 'disabled', number: null }; const existing = list(open.data).filter(issue => !issue.pull_request && issue.title.startsWith('Dependency audit:')); const [current, ...duplicates] = existing.sort((left, right) => left.number - right.number); for (const duplicate of duplicates) { @@ -423,16 +424,19 @@ export async function runAction(environment = process.env, dependencies = {}) { const vulnerable = findings.filter(finding => !finding.informational); const added = introduced === null ? [] : introduced.filter(finding => !finding.informational); const defaultBranch = event.repository?.default_branch; - let issue = { action: 'none', number: null }; + const untracked = { action: 'none', number: null }; + let issue = untracked; if (introduced === null && defaultBranch && environment.GITHUB_REF === `refs/heads/${defaultBranch}` && ['schedule', 'workflow_dispatch', 'push'].includes(environment.GITHUB_EVENT_NAME)) { issue = await syncIssue({ github, repository, findings, report }); if (issue.number !== null) log(`Tracking issue #${issue.number} ${issue.action}`); + if (issue.action === 'disabled') log('Issues are disabled in this repository, so the run fails while vulnerabilities remain.'); } if (environment.GITHUB_OUTPUT) { await appendFile(environment.GITHUB_OUTPUT, `vulnerabilities=${rows(vulnerable).length}\nintroduced=${rows(added).length}\n`, 'utf8'); } - const failed = introduced === null ? vulnerable.length > 0 : added.length > 0; + const issueKept = issue !== untracked && issue.action !== 'disabled'; + const failed = introduced === null ? vulnerable.length > 0 && !issueKept : added.length > 0; log(introduced === null ? (vulnerable.length === 0 ? 'No known vulnerabilities.' : `${countLine(vulnerable)}.`) : (added.length === 0 ? 'This pull request adds no known vulnerabilities.' : `This pull request adds ${countLine(added)}.`)); diff --git a/actions/dependency-audit/index.test.mjs b/actions/dependency-audit/index.test.mjs index a7f582f..3146a71 100644 --- a/actions/dependency-audit/index.test.mjs +++ b/actions/dependency-audit/index.test.mjs @@ -273,7 +273,7 @@ test('a pull request audit scans head and base and fails only on what it adds', assert.equal(result.issue.action, 'none'); }); -test('a scheduled audit on the default branch fails on any vulnerability and updates the tracking issue', async t => { +test('a scheduled audit on the default branch records vulnerabilities in the tracking issue and stays green', async t => { const { environment, summary } = await actionFixture(t, { repository: { default_branch: 'main' } }, 'schedule'); const calls = []; const fetchImpl = async (url, options = {}) => { @@ -284,8 +284,9 @@ test('a scheduled audit on the default branch fails on any vulnerability and upd return Response.json({ number: 11 }, { status: 201 }); }; const pin = { ...scanner, sha256: createHash('sha256').update('binary').digest('hex') }; - const result = await runAction(environment, { fetchImpl, platform: 'linux', arch: 'x64', scan: async () => sample, pin, lockfiles: () => ['bun.lock'], log: () => {} }); - assert.equal(result.failed, true); + const dependencies = { fetchImpl, platform: 'linux', arch: 'x64', scan: async () => sample, pin, lockfiles: () => ['bun.lock'], log: () => {} }; + const result = await runAction(environment, dependencies); + assert.equal(result.failed, false, 'the issue, not a red run on main, carries existing vulnerabilities'); assert.deepEqual(result.issue, { action: 'created', number: 11 }); assert.deepEqual(calls.slice(1), [ ['GET', 'https://api.github.com/repos/hraness/example/issues'], @@ -293,9 +294,22 @@ test('a scheduled audit on the default branch fails on any vulnerability and upd ['POST', 'https://api.github.com/repos/hraness/example/issues'], ]); assert.match(await readFile(summary, 'utf8'), /Run: https:\/\/github\.com\/hraness\/example\/actions\/runs\/42/u); + const branch = await actionFixture(t, { repository: { default_branch: 'main' } }, 'workflow_dispatch', 'refs/heads/feature'); - const other = await runAction(branch.environment, { fetchImpl, platform: 'linux', arch: 'x64', scan: async () => sample, pin, lockfiles: () => [], log: () => {} }); - assert.equal(other.issue.action, 'none', 'only the default branch owns the tracking issue'); + const manual = await runAction(branch.environment, dependencies); + assert.equal(manual.issue.action, 'none', 'only the default branch owns the tracking issue'); + assert.equal(manual.failed, true, 'without a tracking issue, the run itself reports existing vulnerabilities'); + + const disabled = await actionFixture(t, { repository: { default_branch: 'main' } }, 'schedule'); + const noIssues = await runAction(disabled.environment, { ...dependencies, fetchImpl: async (url, options) => ( + String(url).includes('/issues?') ? new Response('Issues are disabled', { status: 410 }) : fetchImpl(url, options)) }); + assert.deepEqual(noIssues.issue, { action: 'disabled', number: null }); + assert.equal(noIssues.failed, true, 'a repository with issues disabled falls back to a failing run'); + + const clean = await actionFixture(t, { repository: { default_branch: 'main' } }, 'schedule'); + const advisoriesOnly = { results: [sample.results[2]].map(result => ({ ...result, packages: result.packages.slice(0, 1) })) }; + const quiet = await runAction(clean.environment, { ...dependencies, scan: async () => advisoriesOnly }); + assert.equal(quiet.failed, false); }); test('the action entrypoint fails visibly outside a Git checkout', async t => { diff --git a/scripts/sync-dependency-audit.py b/scripts/sync-dependency-audit.py index 3a4c07d..299b52d 100644 --- a/scripts/sync-dependency-audit.py +++ b/scripts/sync-dependency-audit.py @@ -155,7 +155,7 @@ def inspect(repo: dict, expected: str, globs: list[str]) -> dict: - Adds or updates `.github/workflows/dependency-audit.yml` from `templates/dependency-audit.yml` in hraness/.github, pinned to action commit `{sha}`. - The audit scans every tracked lockfile with a pinned OSV-Scanner on pull requests that change a lockfile, on pushes to `main`, daily, and on demand. -- A pull request fails the audit only when it adds a known vulnerability; the scheduled run on `main` fails while any is present and keeps one `dependency-audit` issue current. +- A pull request fails the audit only when it adds a known vulnerability. The run on `main` keeps one `dependency-audit` issue current with every known vulnerability and closes it when none remain. - The check is advisory: do not add it to required checks. #### Test plan