Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 19 additions & 0 deletions docs/contributing/dependency-policy.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,10 +30,29 @@ Before adding a dependency, consider:

`.github/workflows/dependency-review.yml` reviews dependency changes on pull requests using GitHub's Dependency Review Action.

The workflow contract is:

| Setting | Required behavior |
| --- | --- |
| Trigger | `pull_request`, using the default opened, synchronize, and reopened events; no branch or path filters |
| Permissions | Top-level `contents: read`; no job-level override, write permission, or secrets required |
| Job | `dependency-review`, displayed as `Dependency Review`, on `ubuntu-latest` with a 10-minute timeout |
| Action | `actions/dependency-review-action`, pinned to a reviewed full 40-character commit SHA |
| Failure threshold | `fail-on-severity: high`, covering newly introduced high and critical vulnerabilities |
| Execution | No conditional skips, `continue-on-error`, checkout, install, or arbitrary command steps |

This is an unprivileged PR check, not `pull_request_target`. It compares dependency changes between the PR's base and head; it is not a full audit of every dependency already present in the base branch. There are no severity exceptions or package allowlists configured in this workflow.

The baseline policy fails a pull request when it introduces a dependency with a known vulnerability rated `high` or `critical`. Lower-severity findings remain review signals and may still block a change when the affected dependency is security-sensitive, unnecessary, directly exploitable in the CLI threat model, or otherwise conflicts with maintainer judgment.

Dependency Review is a complement to normal code review. A green dependency-review result does not make a new package automatically acceptable.

Contributors must resolve high/critical findings by removing or replacing the dependency or selecting a reviewed fixed version. Maintainers review the dependency's purpose, ownership, license, transitive footprint, and install scripts as well as the automated result. Lowering the threshold, adding exceptions, skipping a failing job, or granting write permissions is not a routine way to unblock a PR. Any proposed policy/workflow change requires explicit supply-chain/security maintainer review and corresponding documentation and conformance-test updates.

If the check cannot run because of a service, repository configuration, or fork-approval problem, maintainers must resolve that blocker and rerun the check; an absent or skipped result is not a pass. Mandatory merge enforcement is configured separately through the [repository ruleset](repository-ruleset.md), not by this document or the workflow alone. Dependency Review introduces no auto-merge behavior: dependency PRs still require the normal checks and maintainer review.

`tests/dependency-review-policy.test.ts` checks this deliberately small workflow's complete executable shape and the documented settings. It includes negative cases for filters, privileged triggers/permissions, weakened severity, conditional skips, error suppression, extra inputs, and added steps. Run it with `npm test -- tests/dependency-review-policy.test.ts` after the standard clean install. The check is static and does not call GitHub or execute an action.

## Dependabot Updates

`.github/dependabot.yml` monitors both:
Expand Down
151 changes: 151 additions & 0 deletions tests/dependency-review-policy.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
import { readFileSync } from 'node:fs';
import { join } from 'node:path';
import { describe, expect, it } from 'vitest';

const root = join(import.meta.dirname, '..');
const workflow = readFileSync(
join(root, '.github/workflows/dependency-review.yml'),
'utf8',
).replace(/\r\n/g, '\n');
const policy = readFileSync(
join(root, 'docs/contributing/dependency-policy.md'),
'utf8',
);

// A strict, intentionally small YAML subset avoids a new parser dependency.
// Compare the whole executable shape: isolated regex checks could miss an
// override, second trigger, skip condition, or extra step elsewhere in the file.
// Changes to this contract require a corresponding policy/security review.
const expected = `name: Dependency Review
on:
pull_request:
permissions:
contents: read
jobs:
dependency-review:
name: Dependency Review
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- name: Review dependency changes
uses: actions/dependency-review-action@REVIEWED_SHA
with:
fail-on-severity: high`;

function conforms(source: string): boolean {
if (
!/^ {8}uses: actions\/dependency-review-action@[a-f0-9]{40}(?:\s+#.*)?\r?$/m.test(
source,
)
) {
return false;
}
const normalized = source
.split(/\r?\n/)
.map((line) => line.replace(/\s+#.*$/, '').trimEnd())
.filter((line) => line.trim() !== '' && !line.trimStart().startsWith('#'))
.join('\n')
.replace(
/actions\/dependency-review-action@[a-f0-9]{40}(?=\n)/g,
'actions/dependency-review-action@REVIEWED_SHA',
);
return normalized === expected;
}

describe('Dependency Review policy conformance', () => {
it('keeps the complete workflow unprivileged, mandatory and high-severity gated', () => {
expect(conforms(workflow)).toBe(true);
});

it('accepts comments and line endings without changing executable settings', () => {
expect(conforms(`# Review-only workflow\n${workflow}`)).toBe(true);
expect(conforms(workflow.replace(/\r?\n/g, '\r\n'))).toBe(true);
});

it.each([
['privileged trigger', ' pull_request:', ' pull_request_target:'],
[
'path filter',
' pull_request:',
' pull_request:\n paths: [package.json]',
],
[
'branch filter',
' pull_request:',
' pull_request:\n branches: [main]',
],
[
'restricted events',
' pull_request:',
' pull_request:\n types: [opened]',
],
['write permission', ' contents: read', ' contents: write'],
[
'blanket write permission',
'permissions:\n contents: read',
'permissions: write-all',
],
[
'job override',
' steps:',
' permissions:\n contents: write\n steps:',
],
[
'critical-only threshold',
'fail-on-severity: high',
'fail-on-severity: critical',
],
['removed gate', ' fail-on-severity: high', ''],
['skipped job', ' steps:', ' if: false\n steps:'],
[
'ignored failure',
' steps:',
' continue-on-error: true\n steps:',
],
[
'extra action input',
' with:',
' with:\n allow-ghsas: GHSA-example',
],
[
'added execution step',
' steps:',
' steps:\n - run: gh pr merge --auto',
],
])('rejects %s', (_name, from, to) => {
expect(workflow).toContain(from);
expect(conforms(workflow.replace(from, to))).toBe(false);
});

it.each(['v5', 'REVIEWED_SHA'])(
'rejects unpinned action reference %s',
(ref) => {
const unpinned = workflow.replace(
/actions\/dependency-review-action@[a-f0-9]{40}/,
`actions/dependency-review-action@${ref}`,
);
expect(unpinned).not.toBe(workflow);
expect(conforms(unpinned)).toBe(false);
},
);

it('documents the enforced settings and maintainer responsibilities', () => {
for (const setting of [
'`pull_request`',
'`contents: read`',
'`dependency-review`',
'`ubuntu-latest`',
'10-minute timeout',
'`actions/dependency-review-action`',
'40-character commit SHA',
'`fail-on-severity: high`',
]) {
expect(policy).toContain(setting);
}
expect(policy).toContain('high and critical');
expect(policy).toContain('no branch or path filters');
expect(policy).toContain('absent or skipped result is not a pass');
expect(policy).toContain('supply-chain/security maintainer review');
expect(policy).toContain('introduces no auto-merge behavior');
});
});