vouchington-tooling 0.17.0 → 0.18.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +15 -0
- package/dist/dependency-license-policy/collect.d.mts +9 -0
- package/dist/dependency-license-policy/collect.mjs +48 -0
- package/dist/dependency-license-policy/index.d.mts +4 -0
- package/dist/dependency-license-policy/index.mjs +3 -0
- package/dist/dependency-license-policy/policy.d.mts +3 -0
- package/dist/dependency-license-policy/policy.mjs +40 -0
- package/dist/dependency-license-policy/report.d.mts +2 -0
- package/dist/dependency-license-policy/report.mjs +30 -0
- package/dist/dependency-license-policy/spdx.d.mts +13 -0
- package/dist/dependency-license-policy/spdx.mjs +40 -0
- package/dist/dependency-license-policy/types.d.mts +43 -0
- package/dist/dependency-license-policy/types.mjs +1 -0
- package/dist/dependency-license-policy/workspace.d.mts +9 -0
- package/dist/dependency-license-policy/workspace.mjs +77 -0
- package/dist/index.d.mts +2 -0
- package/dist/index.mjs +1 -0
- package/package.json +8 -1
- package/scripts/gha/write-github-multiline-output.sh +6 -6
- package/skills/github-issue/SKILL.md +4 -1
- package/skills/review-github-issue-taxonomy/SKILL.md +3 -2
package/README.md
CHANGED
|
@@ -238,6 +238,10 @@ import { runCiLocal } from 'vouchington-tooling/ci-local'
|
|
|
238
238
|
import { rateLimitDelay } from 'vouchington-tooling/gha-rate-limit'
|
|
239
239
|
import { parseCheckpoint } from 'vouchington-tooling/gha-pr-checkpoint'
|
|
240
240
|
import { checkWorkspaceGatesPolicy } from 'vouchington-tooling/workspace-gates'
|
|
241
|
+
import {
|
|
242
|
+
collectPnpmLicenseReport,
|
|
243
|
+
evaluatePnpmLicenseReport,
|
|
244
|
+
} from 'vouchington-tooling/dependency-license-policy'
|
|
241
245
|
import { checkGhaWorkspacePolicy } from 'vouchington-tooling/gha-workspace-policy'
|
|
242
246
|
import { requireUpToDate } from 'vouchington-tooling/require-up-to-date'
|
|
243
247
|
import { runGitleaksDirectoryScan } from 'vouchington-tooling/gitleaks-directory-scan'
|
|
@@ -306,6 +310,17 @@ policy out of this package.
|
|
|
306
310
|
dependency declared by a non-fixture package manifest. Assert dependency membership or placement,
|
|
307
311
|
or derive a configuration or documentation package spec from that manifest instead.
|
|
308
312
|
|
|
313
|
+
`dependency-license-policy` keeps legal policy in the consumer. `collectPnpmLicenseReport` creates
|
|
314
|
+
an isolated, script-free temporary workspace and store, expands pnpm's supported architectures to
|
|
315
|
+
every `os`, `cpu`, and `libc` selector represented in the lockfile, and validates the JSON report.
|
|
316
|
+
Pass explicit denied SPDX IDs and prefixes, exact aliases, and justified allowlist scopes to
|
|
317
|
+
`evaluatePnpmLicenseReport`. Unknown, malformed, and custom SPDX references fail closed. Allowlist
|
|
318
|
+
scopes are either intentionally global or an exact package-name set; the library returns structured
|
|
319
|
+
violations and does not format CI-provider diagnostics.
|
|
320
|
+
When present, the repository `.npmrc` is copied into the owner-private temporary directory so pnpm
|
|
321
|
+
can authenticate to the same registries; normal cleanup removes the copy, and the caller remains
|
|
322
|
+
responsible for terminating the process normally rather than abandoning temporary audit state.
|
|
323
|
+
|
|
309
324
|
`session-friction` is an opt-in capture and reporting library. Callers supply the session id,
|
|
310
325
|
absolute log directory, host-independent observation, and journal loader; it does not inspect host
|
|
311
326
|
environment variables, install hooks, or connect to a journal service by itself. Invoking
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { PnpmExecutor, PnpmLicenseReport } from './types.mts';
|
|
2
|
+
import { type PnpmLicenseAuditWorkspace } from './workspace.mts';
|
|
3
|
+
export interface CollectPnpmLicenseReportOptions {
|
|
4
|
+
readonly execute?: PnpmExecutor;
|
|
5
|
+
readonly prepareWorkspace?: (repoRoot: string, lockfileSource: string, workspaceSource: string) => PnpmLicenseAuditWorkspace;
|
|
6
|
+
readonly readFile?: (path: string, encoding: 'utf8') => string;
|
|
7
|
+
}
|
|
8
|
+
/** Collects licenses for every platform represented in a pnpm lockfile. */
|
|
9
|
+
export declare function collectPnpmLicenseReport(repoRoot: string, options?: CollectPnpmLicenseReportOptions): PnpmLicenseReport;
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { spawnSync } from 'node:child_process';
|
|
2
|
+
import { readFileSync } from 'node:fs';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
import { parsePnpmLicenseReport } from './report.mjs';
|
|
5
|
+
import { preparePnpmLicenseAuditWorkspace } from './workspace.mjs';
|
|
6
|
+
const DEFAULT_OPTIONS = {
|
|
7
|
+
execute: spawnSync,
|
|
8
|
+
prepareWorkspace: preparePnpmLicenseAuditWorkspace,
|
|
9
|
+
readFile: readFileSync,
|
|
10
|
+
};
|
|
11
|
+
function commandFailureOutput(result) {
|
|
12
|
+
return result.stderr.trim() || result.stdout.trim();
|
|
13
|
+
}
|
|
14
|
+
function assertCommandSucceeded(label, result) {
|
|
15
|
+
if (result.error)
|
|
16
|
+
throw result.error;
|
|
17
|
+
if (result.status !== 0) {
|
|
18
|
+
throw new Error(`${label} exited with status ${String(result.status)}: ${commandFailureOutput(result)}`);
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
/** Collects licenses for every platform represented in a pnpm lockfile. */
|
|
22
|
+
export function collectPnpmLicenseReport(repoRoot, options = {}) {
|
|
23
|
+
const { execute, prepareWorkspace, readFile } = { ...DEFAULT_OPTIONS, ...options };
|
|
24
|
+
const lockfilePath = join(repoRoot, 'pnpm-lock.yaml');
|
|
25
|
+
const workspacePath = join(repoRoot, 'pnpm-workspace.yaml');
|
|
26
|
+
const auditWorkspace = prepareWorkspace(repoRoot, readFile(lockfilePath, 'utf8'), readFile(workspacePath, 'utf8'));
|
|
27
|
+
try {
|
|
28
|
+
const storeConfig = `--config.store-dir=${join(auditWorkspace.cwd, '.pnpm-store')}`;
|
|
29
|
+
const fetchResult = execute('pnpm', [storeConfig, '--config.force=true', 'fetch', '--ignore-scripts'], { cwd: auditWorkspace.cwd, encoding: 'utf8' });
|
|
30
|
+
assertCommandSucceeded('pnpm fetch', fetchResult);
|
|
31
|
+
const result = execute('pnpm', [storeConfig, 'licenses', 'list', '--json'], {
|
|
32
|
+
cwd: auditWorkspace.cwd,
|
|
33
|
+
encoding: 'utf8',
|
|
34
|
+
});
|
|
35
|
+
assertCommandSucceeded('pnpm licenses list --json', result);
|
|
36
|
+
try {
|
|
37
|
+
return parsePnpmLicenseReport(JSON.parse(result.stdout));
|
|
38
|
+
}
|
|
39
|
+
catch (error) {
|
|
40
|
+
throw new Error(`pnpm licenses list --json produced unparseable output: ${String(error)}`, {
|
|
41
|
+
cause: error,
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
finally {
|
|
46
|
+
auditWorkspace.cleanup();
|
|
47
|
+
}
|
|
48
|
+
}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
export { collectPnpmLicenseReport, type CollectPnpmLicenseReportOptions } from './collect.mts';
|
|
2
|
+
export { evaluatePackageLicenseExpression, evaluatePnpmLicenseReport } from './policy.mts';
|
|
3
|
+
export { parsePnpmLicenseReport } from './report.mts';
|
|
4
|
+
export type { DependencyLicenseAllowlistEntry, DependencyLicenseEvaluation, DependencyLicensePolicy, DependencyLicenseViolation, PnpmExecutor, PnpmLicenseReport, PnpmLicenseReportEntry, } from './types.mts';
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
import type { DependencyLicenseEvaluation, DependencyLicensePolicy, DependencyLicenseViolation, PnpmLicenseReport } from './types.mts';
|
|
2
|
+
export declare function evaluatePackageLicenseExpression(licenseExpression: string, packageName: string, policy: DependencyLicensePolicy): DependencyLicenseEvaluation;
|
|
3
|
+
export declare function evaluatePnpmLicenseReport(report: PnpmLicenseReport, policy: DependencyLicensePolicy): DependencyLicenseViolation[];
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { collectSpdxAtoms, evaluateSpdxExpression, parseSpdxExpression } from './spdx.mjs';
|
|
2
|
+
function isAllowlisted(atomId, packageName, policy) {
|
|
3
|
+
return (policy.allowlist ?? []).some((entry) => entry.licenseId === atomId &&
|
|
4
|
+
(entry.scope.kind === 'all' || entry.scope.packageNames.includes(packageName)));
|
|
5
|
+
}
|
|
6
|
+
function isDenied(atomId, policy) {
|
|
7
|
+
return (policy.deniedLicenseIds.includes(atomId) ||
|
|
8
|
+
policy.deniedLicensePrefixes.some((prefix) => atomId.startsWith(prefix)));
|
|
9
|
+
}
|
|
10
|
+
export function evaluatePackageLicenseExpression(licenseExpression, packageName, policy) {
|
|
11
|
+
const normalized = policy.knownLicenseAliases?.[licenseExpression] ?? licenseExpression;
|
|
12
|
+
let node;
|
|
13
|
+
try {
|
|
14
|
+
node = parseSpdxExpression(normalized);
|
|
15
|
+
}
|
|
16
|
+
catch {
|
|
17
|
+
return { ok: false, deniedAtoms: [licenseExpression] };
|
|
18
|
+
}
|
|
19
|
+
const isAtomAllowed = (atomId) => isAllowlisted(atomId, packageName, policy) || !isDenied(atomId, policy);
|
|
20
|
+
const ok = evaluateSpdxExpression(node, isAtomAllowed);
|
|
21
|
+
const deniedAtoms = ok ? [] : collectSpdxAtoms(node).filter((atomId) => !isAtomAllowed(atomId));
|
|
22
|
+
return { ok, deniedAtoms };
|
|
23
|
+
}
|
|
24
|
+
export function evaluatePnpmLicenseReport(report, policy) {
|
|
25
|
+
const violations = [];
|
|
26
|
+
for (const [licenseExpression, entries] of Object.entries(report)) {
|
|
27
|
+
for (const entry of entries) {
|
|
28
|
+
const evaluation = evaluatePackageLicenseExpression(licenseExpression, entry.name, policy);
|
|
29
|
+
if (evaluation.ok)
|
|
30
|
+
continue;
|
|
31
|
+
violations.push({
|
|
32
|
+
...evaluation,
|
|
33
|
+
licenseExpression,
|
|
34
|
+
packageName: entry.name,
|
|
35
|
+
...(entry.versions === undefined ? {} : { versions: entry.versions }),
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
return violations;
|
|
40
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
function getStringList(value, path) {
|
|
2
|
+
if (!Array.isArray(value) || !value.every((entry) => typeof entry === 'string')) {
|
|
3
|
+
throw new Error(`expected ${path} to be an array of strings`);
|
|
4
|
+
}
|
|
5
|
+
return value;
|
|
6
|
+
}
|
|
7
|
+
export function parsePnpmLicenseReport(value) {
|
|
8
|
+
if (typeof value !== 'object' || value === null || Array.isArray(value)) {
|
|
9
|
+
throw new Error('expected a JSON object keyed by license expression');
|
|
10
|
+
}
|
|
11
|
+
const report = Object.create(null);
|
|
12
|
+
for (const [licenseExpression, entries] of Object.entries(value)) {
|
|
13
|
+
if (!Array.isArray(entries)) {
|
|
14
|
+
throw new Error(`expected license group ${JSON.stringify(licenseExpression)} to be an array`);
|
|
15
|
+
}
|
|
16
|
+
report[licenseExpression] = entries.map((entry, index) => {
|
|
17
|
+
const path = `license group ${JSON.stringify(licenseExpression)} entry ${String(index)}`;
|
|
18
|
+
if (typeof entry !== 'object' || entry === null || Array.isArray(entry)) {
|
|
19
|
+
throw new Error(`expected ${path} to be an object`);
|
|
20
|
+
}
|
|
21
|
+
const { name, versions } = entry;
|
|
22
|
+
if (typeof name !== 'string')
|
|
23
|
+
throw new Error(`expected ${path}.name to be a string`);
|
|
24
|
+
if (versions === undefined)
|
|
25
|
+
return { name };
|
|
26
|
+
return { name, versions: getStringList(versions, `${path}.versions`) };
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
return report;
|
|
30
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export type SpdxNode = {
|
|
2
|
+
type: 'AND';
|
|
3
|
+
children: SpdxNode[];
|
|
4
|
+
} | {
|
|
5
|
+
type: 'OR';
|
|
6
|
+
children: SpdxNode[];
|
|
7
|
+
} | {
|
|
8
|
+
type: 'ATOM';
|
|
9
|
+
id: string;
|
|
10
|
+
};
|
|
11
|
+
export declare function parseSpdxExpression(expression: string): SpdxNode;
|
|
12
|
+
export declare function evaluateSpdxExpression(node: SpdxNode, isAtomAllowed: (atomId: string) => boolean): boolean;
|
|
13
|
+
export declare function collectSpdxAtoms(node: SpdxNode): string[];
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import parseSpdx from 'spdx-expression-parse';
|
|
2
|
+
function isCustomLicenseReference(licenseId) {
|
|
3
|
+
return licenseId.startsWith('LicenseRef-') || licenseId.startsWith('DocumentRef-');
|
|
4
|
+
}
|
|
5
|
+
function convertParsedNode(parsed) {
|
|
6
|
+
if ('license' in parsed) {
|
|
7
|
+
if (isCustomLicenseReference(parsed.license)) {
|
|
8
|
+
throw new Error(`Custom SPDX license references are not allowed: ${parsed.license}`);
|
|
9
|
+
}
|
|
10
|
+
const id = `${parsed.license}${parsed.plus ? '+' : ''}${parsed.exception ? ` WITH ${parsed.exception}` : ''}`;
|
|
11
|
+
return { type: 'ATOM', id };
|
|
12
|
+
}
|
|
13
|
+
const type = parsed.conjunction === 'and' ? 'AND' : 'OR';
|
|
14
|
+
const children = [convertParsedNode(parsed.left), convertParsedNode(parsed.right)].flatMap((child) => (child.type === type ? child.children : [child]));
|
|
15
|
+
return { type, children };
|
|
16
|
+
}
|
|
17
|
+
export function parseSpdxExpression(expression) {
|
|
18
|
+
if (expression.trim().length === 0) {
|
|
19
|
+
throw new Error('Invalid SPDX license expression: expression is empty');
|
|
20
|
+
}
|
|
21
|
+
try {
|
|
22
|
+
return convertParsedNode(parseSpdx(expression));
|
|
23
|
+
}
|
|
24
|
+
catch (error) {
|
|
25
|
+
throw new Error(`Invalid SPDX license expression: ${String(error)}`, { cause: error });
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
export function evaluateSpdxExpression(node, isAtomAllowed) {
|
|
29
|
+
if (node.type === 'ATOM')
|
|
30
|
+
return isAtomAllowed(node.id);
|
|
31
|
+
if (node.type === 'OR') {
|
|
32
|
+
return node.children.some((child) => evaluateSpdxExpression(child, isAtomAllowed));
|
|
33
|
+
}
|
|
34
|
+
return node.children.every((child) => evaluateSpdxExpression(child, isAtomAllowed));
|
|
35
|
+
}
|
|
36
|
+
export function collectSpdxAtoms(node) {
|
|
37
|
+
if (node.type === 'ATOM')
|
|
38
|
+
return [node.id];
|
|
39
|
+
return node.children.flatMap(collectSpdxAtoms);
|
|
40
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
export interface PnpmLicenseReportEntry {
|
|
2
|
+
readonly name: string;
|
|
3
|
+
readonly versions?: readonly string[];
|
|
4
|
+
}
|
|
5
|
+
/** Shape emitted by `pnpm licenses list --json`. */
|
|
6
|
+
export type PnpmLicenseReport = Record<string, PnpmLicenseReportEntry[]>;
|
|
7
|
+
export type PnpmExecutor = (command: string, args: string[], options: {
|
|
8
|
+
cwd: string;
|
|
9
|
+
encoding: 'utf8';
|
|
10
|
+
}) => {
|
|
11
|
+
error?: Error;
|
|
12
|
+
status: number | null;
|
|
13
|
+
stderr: string;
|
|
14
|
+
stdout: string;
|
|
15
|
+
};
|
|
16
|
+
export interface DependencyLicenseAllowlistEntry {
|
|
17
|
+
/** Exact SPDX atom allowed by this entry. */
|
|
18
|
+
readonly licenseId: string;
|
|
19
|
+
/** Human-readable evidence for the exception. */
|
|
20
|
+
readonly reason: string;
|
|
21
|
+
/** Explicit package scope for this allowance. */
|
|
22
|
+
readonly scope: {
|
|
23
|
+
readonly kind: 'all';
|
|
24
|
+
} | {
|
|
25
|
+
readonly kind: 'exact';
|
|
26
|
+
readonly packageNames: readonly string[];
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
export interface DependencyLicensePolicy {
|
|
30
|
+
readonly allowlist?: readonly DependencyLicenseAllowlistEntry[];
|
|
31
|
+
readonly deniedLicenseIds: readonly string[];
|
|
32
|
+
readonly deniedLicensePrefixes: readonly string[];
|
|
33
|
+
readonly knownLicenseAliases?: Readonly<Record<string, string>>;
|
|
34
|
+
}
|
|
35
|
+
export interface DependencyLicenseEvaluation {
|
|
36
|
+
readonly deniedAtoms: readonly string[];
|
|
37
|
+
readonly ok: boolean;
|
|
38
|
+
}
|
|
39
|
+
export interface DependencyLicenseViolation extends DependencyLicenseEvaluation {
|
|
40
|
+
readonly licenseExpression: string;
|
|
41
|
+
readonly packageName: string;
|
|
42
|
+
readonly versions?: readonly string[];
|
|
43
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export interface PnpmLicenseAuditWorkspace {
|
|
2
|
+
readonly cleanup: () => void;
|
|
3
|
+
readonly cwd: string;
|
|
4
|
+
}
|
|
5
|
+
export declare function renderPnpmLicenseAuditWorkspace(lockfileSource: string, workspaceSource: string, paths: {
|
|
6
|
+
lockfile: string;
|
|
7
|
+
workspace: string;
|
|
8
|
+
}): string;
|
|
9
|
+
export declare function preparePnpmLicenseAuditWorkspace(repoRoot: string, lockfileSource: string, workspaceSource: string): PnpmLicenseAuditWorkspace;
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { copyFileSync, existsSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs';
|
|
2
|
+
import { tmpdir } from 'node:os';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
import { parse as parseYaml, stringify as stringifyYaml } from 'yaml';
|
|
5
|
+
const PLATFORM_KEYS = ['os', 'cpu', 'libc'];
|
|
6
|
+
function parseYamlObject(source, path) {
|
|
7
|
+
let parsed;
|
|
8
|
+
try {
|
|
9
|
+
parsed = parseYaml(source);
|
|
10
|
+
}
|
|
11
|
+
catch (error) {
|
|
12
|
+
throw new Error(`failed to parse ${path}: ${String(error)}`, { cause: error });
|
|
13
|
+
}
|
|
14
|
+
if (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed)) {
|
|
15
|
+
throw new Error(`expected ${path} to contain a YAML object`);
|
|
16
|
+
}
|
|
17
|
+
return parsed;
|
|
18
|
+
}
|
|
19
|
+
function getStringList(value, path) {
|
|
20
|
+
if (typeof value === 'string')
|
|
21
|
+
return [value];
|
|
22
|
+
if (!Array.isArray(value) || !value.every((entry) => typeof entry === 'string')) {
|
|
23
|
+
throw new Error(`expected ${path} to be a string or an array of strings`);
|
|
24
|
+
}
|
|
25
|
+
return value;
|
|
26
|
+
}
|
|
27
|
+
export function renderPnpmLicenseAuditWorkspace(lockfileSource, workspaceSource, paths) {
|
|
28
|
+
const lockfile = parseYamlObject(lockfileSource, paths.lockfile);
|
|
29
|
+
const workspace = parseYamlObject(workspaceSource, paths.workspace);
|
|
30
|
+
const packages = lockfile.packages;
|
|
31
|
+
if (typeof packages !== 'object' || packages === null || Array.isArray(packages)) {
|
|
32
|
+
throw new Error(`expected ${paths.lockfile} to contain a packages object`);
|
|
33
|
+
}
|
|
34
|
+
const supportedArchitectures = {
|
|
35
|
+
cpu: ['current'],
|
|
36
|
+
libc: ['current'],
|
|
37
|
+
os: ['current'],
|
|
38
|
+
};
|
|
39
|
+
for (const key of PLATFORM_KEYS) {
|
|
40
|
+
const values = new Set();
|
|
41
|
+
for (const snapshot of Object.values(packages)) {
|
|
42
|
+
if (typeof snapshot !== 'object' || snapshot === null || Array.isArray(snapshot))
|
|
43
|
+
continue;
|
|
44
|
+
const value = snapshot[key];
|
|
45
|
+
if (value === undefined)
|
|
46
|
+
continue;
|
|
47
|
+
for (const platform of getStringList(value, `${paths.lockfile} packages.*.${key}`)) {
|
|
48
|
+
values.add(platform);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
supportedArchitectures[key].push(...[...values].filter((value) => value !== 'current').sort());
|
|
52
|
+
}
|
|
53
|
+
return stringifyYaml({ ...workspace, packages: [], supportedArchitectures });
|
|
54
|
+
}
|
|
55
|
+
export function preparePnpmLicenseAuditWorkspace(repoRoot, lockfileSource, workspaceSource) {
|
|
56
|
+
const auditRoot = mkdtempSync(join(tmpdir(), 'dependency-license-audit-'));
|
|
57
|
+
try {
|
|
58
|
+
for (const filename of ['package.json', 'pnpm-lock.yaml']) {
|
|
59
|
+
copyFileSync(join(repoRoot, filename), join(auditRoot, filename));
|
|
60
|
+
}
|
|
61
|
+
const npmrc = join(repoRoot, '.npmrc');
|
|
62
|
+
if (existsSync(npmrc))
|
|
63
|
+
copyFileSync(npmrc, join(auditRoot, '.npmrc'));
|
|
64
|
+
writeFileSync(join(auditRoot, 'pnpm-workspace.yaml'), renderPnpmLicenseAuditWorkspace(lockfileSource, workspaceSource, {
|
|
65
|
+
lockfile: join(repoRoot, 'pnpm-lock.yaml'),
|
|
66
|
+
workspace: join(repoRoot, 'pnpm-workspace.yaml'),
|
|
67
|
+
}), 'utf8');
|
|
68
|
+
}
|
|
69
|
+
catch (error) {
|
|
70
|
+
rmSync(auditRoot, { force: true, recursive: true });
|
|
71
|
+
throw error;
|
|
72
|
+
}
|
|
73
|
+
return {
|
|
74
|
+
cwd: auditRoot,
|
|
75
|
+
cleanup: () => rmSync(auditRoot, { force: true, recursive: true }),
|
|
76
|
+
};
|
|
77
|
+
}
|
package/dist/index.d.mts
CHANGED
|
@@ -86,6 +86,8 @@ export { CHECKPOINT_MARKER, isTrustedCheckpointComment, parseCheckpoint, renderC
|
|
|
86
86
|
export type { Checkpoint, CheckpointCodecOptions, GitHubComment, } from './gha-pr-checkpoint/index.mts';
|
|
87
87
|
export { checkWorkspaceGatesPolicy } from './workspace-gates/index.mts';
|
|
88
88
|
export type { WorkspaceGatesOptions } from './workspace-gates/index.mts';
|
|
89
|
+
export { collectPnpmLicenseReport, evaluatePackageLicenseExpression, evaluatePnpmLicenseReport, parsePnpmLicenseReport, } from './dependency-license-policy/index.mts';
|
|
90
|
+
export type { CollectPnpmLicenseReportOptions, DependencyLicenseAllowlistEntry, DependencyLicenseEvaluation, DependencyLicensePolicy, DependencyLicenseViolation, PnpmLicenseReport, PnpmLicenseReportEntry, } from './dependency-license-policy/index.mts';
|
|
89
91
|
export { validateNugetUpdate } from './nuget-central-version/index.mts';
|
|
90
92
|
export { normalizeSwiftSource } from './swift-semantic-equal/index.mts';
|
|
91
93
|
export { isSwiftCodeOffset, parseUniqueSwiftBinaryTargetChecksum, } from './swift-source-offset/index.mts';
|
package/dist/index.mjs
CHANGED
|
@@ -46,6 +46,7 @@ export { assertWorkflowCommandDrift, parseCiLocalArgs, runCiLocal } from './ci-l
|
|
|
46
46
|
export { GitHubRateLimitError, isRateLimited, isRetryableCancellationError, MAX_RATE_LIMIT_WAIT_MS, rateLimitDelay, reserveRateLimitDelay, } from './gha-rate-limit/index.mjs';
|
|
47
47
|
export { CHECKPOINT_MARKER, isTrustedCheckpointComment, parseCheckpoint, renderCheckpoint, sortedCheckpointCandidates, validateCheckpoint, } from './gha-pr-checkpoint/index.mjs';
|
|
48
48
|
export { checkWorkspaceGatesPolicy } from './workspace-gates/index.mjs';
|
|
49
|
+
export { collectPnpmLicenseReport, evaluatePackageLicenseExpression, evaluatePnpmLicenseReport, parsePnpmLicenseReport, } from './dependency-license-policy/index.mjs';
|
|
49
50
|
export { validateNugetUpdate } from './nuget-central-version/index.mjs';
|
|
50
51
|
export { normalizeSwiftSource } from './swift-semantic-equal/index.mjs';
|
|
51
52
|
export { isSwiftCodeOffset, parseUniqueSwiftBinaryTargetChecksum, } from './swift-source-offset/index.mjs';
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "vouchington-tooling",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.18.1",
|
|
4
4
|
"description": "Vouchington CLI and extractable tooling libraries.",
|
|
5
5
|
"homepage": "https://github.com/vouchington/vouchington-tooling/tree/main/packages/vouchington-tooling#readme",
|
|
6
6
|
"bugs": {
|
|
@@ -268,6 +268,11 @@
|
|
|
268
268
|
"import": "./dist/workspace-gates/index.mjs",
|
|
269
269
|
"default": "./dist/workspace-gates/index.mjs"
|
|
270
270
|
},
|
|
271
|
+
"./dependency-license-policy": {
|
|
272
|
+
"types": "./dist/dependency-license-policy/index.d.mts",
|
|
273
|
+
"import": "./dist/dependency-license-policy/index.mjs",
|
|
274
|
+
"default": "./dist/dependency-license-policy/index.mjs"
|
|
275
|
+
},
|
|
271
276
|
"./nuget-central-version": {
|
|
272
277
|
"types": "./dist/nuget-central-version/index.d.mts",
|
|
273
278
|
"import": "./dist/nuget-central-version/index.mjs",
|
|
@@ -328,11 +333,13 @@
|
|
|
328
333
|
"remark": "15.0.1",
|
|
329
334
|
"remark-gfm": "4.0.1",
|
|
330
335
|
"smol-toml": "1.8.0",
|
|
336
|
+
"spdx-expression-parse": "5.0.0",
|
|
331
337
|
"yaml": "2.9.0"
|
|
332
338
|
},
|
|
333
339
|
"devDependencies": {
|
|
334
340
|
"@ast-grep/cli": "0.45.3",
|
|
335
341
|
"@types/picomatch": "^4.0.3",
|
|
342
|
+
"@types/spdx-expression-parse": "4.0.0",
|
|
336
343
|
"agent-blackboard": "^0.5.0"
|
|
337
344
|
},
|
|
338
345
|
"peerDependencies": {
|
|
@@ -26,17 +26,17 @@ delimiter_prefix=$(printf '%s' "$output_name" | tr '[:lower:]-' '[:upper:]_')
|
|
|
26
26
|
delimiter=''
|
|
27
27
|
attempt=1
|
|
28
28
|
while [[ $attempt -le 10 ]]; do
|
|
29
|
-
if !
|
|
30
|
-
echo '
|
|
29
|
+
if ! random_suffix=$(LC_ALL=C od -An -v -N16 -tx1 /dev/urandom | LC_ALL=C tr -d '[:space:]'); then
|
|
30
|
+
echo 'random suffix generation failed while creating a GitHub output delimiter' >&2
|
|
31
31
|
exit 1
|
|
32
32
|
fi
|
|
33
|
-
if [[
|
|
34
|
-
echo '
|
|
33
|
+
if [[ ! "$random_suffix" =~ ^[[:xdigit:]]{32}$ ]]; then
|
|
34
|
+
echo 'random suffix generator returned an invalid GitHub output delimiter suffix' >&2
|
|
35
35
|
exit 1
|
|
36
36
|
fi
|
|
37
37
|
|
|
38
|
-
|
|
39
|
-
delimiter="${delimiter_prefix}_${
|
|
38
|
+
normalized_suffix=$(printf '%s' "$random_suffix" | tr '[:lower:]-' '[:upper:]_')
|
|
39
|
+
delimiter="${delimiter_prefix}_${normalized_suffix}"
|
|
40
40
|
if ! grep -Fq -- "$delimiter" "$payload_file"; then
|
|
41
41
|
break
|
|
42
42
|
fi
|
|
@@ -55,7 +55,10 @@ repository.
|
|
|
55
55
|
areas, validation, and external context. A discovered blocker does not widen implementation scope.
|
|
56
56
|
4. Fetch the complete live taxonomy, including open projects. Apply matching existing labels and a
|
|
57
57
|
selected existing milestone without separate approval. Select an existing open project for
|
|
58
|
-
|
|
58
|
+
strategic, initiative-level tracking and a milestone for repo-local release or sequencing
|
|
59
|
+
tracking — the choice turns on the initiative's nature, not how many repositories it touches, so a
|
|
60
|
+
single-repo strategic initiative can carry a project, and an initiative that needs both a
|
|
61
|
+
cross-cutting strategic view and repo-local sequencing may carry both; an issue belongs to at
|
|
59
62
|
most one project and not every issue needs one. Before adding an item, check its project
|
|
60
63
|
membership and that of any item the project's own automation could pull in alongside it, such as
|
|
61
64
|
a parent issue's sub-issues; skip the add and report the conflict if any of them already belongs
|
|
@@ -14,8 +14,9 @@ local configuration edits or live taxonomy mutation; read local `AGENTS.md` and
|
|
|
14
14
|
deletion, or rule change.
|
|
15
15
|
3. Audit aliases, ambiguity, unused labels, missing descriptions, color consistency, milestone
|
|
16
16
|
overlap, delivery gaps, and path-label coverage. Audit projects too: a milestone theme duplicated
|
|
17
|
-
across repositories is a candidate project, and a project
|
|
18
|
-
|
|
17
|
+
across repositories is a candidate project, and a project holding only routine, non-initiative
|
|
18
|
+
work — regardless of how many repositories it touches — is a candidate milestone; repository count
|
|
19
|
+
alone is not a miscategorization signal. Flag a project with no description, an empty or stale project, a closed
|
|
19
20
|
project with open items, an issue that belongs to more than one project, and a project's own
|
|
20
21
|
automation for auto-adding a parent issue's sub-issues left enabled where the at-most-one-project
|
|
21
22
|
rule applies — it can silently duplicate membership.
|