vouchington-tooling 0.19.0 → 0.19.2
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
CHANGED
|
@@ -96,9 +96,10 @@ Name mode takes one or more `--name <artifact>` flags (mutually exclusive with `
|
|
|
96
96
|
the run's artifacts once, and extracts every requested name that is present into `<dir>/<name>`.
|
|
97
97
|
Names are matched literally, in request order, with repeats downloaded once; empty names, `.`, `..`,
|
|
98
98
|
and names containing `/`, `\`, or a line break are rejected before anything is listed. Requested
|
|
99
|
-
names absent from the run are
|
|
100
|
-
|
|
101
|
-
|
|
99
|
+
names absent from the run are expected (callers pass a superset such as retry-attempt variants) and
|
|
100
|
+
are skipped with a single bounded `::notice::` (the absent count, the first three names, then
|
|
101
|
+
`and N more`) and never one line per name. `availability=available` means at least one requested
|
|
102
|
+
artifact was downloaded; `availability=unavailable` (exit 0) means none was
|
|
102
103
|
present. A present artifact whose download fails is a hard failure: the helper stops at the first
|
|
103
104
|
one, prints `download failed artifact=<name> exit=<n>`, exits non-zero (a downloader exit of 3 is
|
|
104
105
|
reported as 1 so it cannot be mistaken for absence), and writes no `availability` output.
|
|
@@ -327,7 +328,10 @@ or derive a configuration or documentation package spec from that manifest inste
|
|
|
327
328
|
|
|
328
329
|
`dependency-license-policy` keeps legal policy in the consumer. `collectPnpmLicenseReport` creates
|
|
329
330
|
an isolated, script-free temporary workspace and store, expands pnpm's supported architectures to
|
|
330
|
-
every `os`, `cpu`, and `libc` selector represented in the lockfile,
|
|
331
|
+
every `os`, `cpu`, and `libc` selector represented in the lockfile, drops every `engines` constraint
|
|
332
|
+
from the audit copy of the lockfile, and validates the JSON report. Dropping `engines` keeps
|
|
333
|
+
`pnpm fetch` from skipping optional packages that exclude the running Node.js; pnpm 12's `fetch`
|
|
334
|
+
ignores `force`, so their licenses would otherwise report as Unknown.
|
|
331
335
|
Pass explicit denied SPDX IDs and prefixes, exact aliases, and justified allowlist scopes to
|
|
332
336
|
`evaluatePnpmLicenseReport`. Unknown, malformed, and custom SPDX references fail closed. Allowlist
|
|
333
337
|
scopes are either intentionally global or an exact package-name set; the library returns structured
|
|
@@ -26,7 +26,10 @@ export function collectPnpmLicenseReport(repoRoot, options = {}) {
|
|
|
26
26
|
const auditWorkspace = prepareWorkspace(repoRoot, readFile(lockfilePath, 'utf8'), readFile(workspacePath, 'utf8'));
|
|
27
27
|
try {
|
|
28
28
|
const storeConfig = `--config.store-dir=${join(auditWorkspace.cwd, '.pnpm-store')}`;
|
|
29
|
-
const fetchResult = execute('pnpm', [storeConfig, '
|
|
29
|
+
const fetchResult = execute('pnpm', [storeConfig, 'fetch', '--ignore-scripts'], {
|
|
30
|
+
cwd: auditWorkspace.cwd,
|
|
31
|
+
encoding: 'utf8',
|
|
32
|
+
});
|
|
30
33
|
assertCommandSucceeded('pnpm fetch', fetchResult);
|
|
31
34
|
const result = execute('pnpm', [storeConfig, 'licenses', 'list', '--json'], {
|
|
32
35
|
cwd: auditWorkspace.cwd,
|
|
@@ -2,8 +2,21 @@ export interface PnpmLicenseAuditWorkspace {
|
|
|
2
2
|
readonly cleanup: () => void;
|
|
3
3
|
readonly cwd: string;
|
|
4
4
|
}
|
|
5
|
-
|
|
5
|
+
/** The rendered `pnpm-lock.yaml` and `pnpm-workspace.yaml` of a license audit workspace. */
|
|
6
|
+
export interface PnpmLicenseAuditFiles {
|
|
7
|
+
readonly lockfile: string;
|
|
8
|
+
readonly workspace: string;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Renders the audit copies of `pnpm-lock.yaml` and `pnpm-workspace.yaml` so `pnpm fetch` downloads
|
|
12
|
+
* every package in the lockfile, including optional ones that don't match the host.
|
|
13
|
+
*
|
|
14
|
+
* The workspace supports every `os`, `cpu`, and `libc` in the lockfile. The lockfile drops every
|
|
15
|
+
* `engines` constraint because pnpm 12's `fetch` ignores `force` and skips an optional package whose
|
|
16
|
+
* `engines` exclude the running Node.js, which would leave that package's license Unknown.
|
|
17
|
+
*/
|
|
18
|
+
export declare function renderPnpmLicenseAuditFiles(lockfileSource: string, workspaceSource: string, paths: {
|
|
6
19
|
lockfile: string;
|
|
7
20
|
workspace: string;
|
|
8
|
-
}):
|
|
21
|
+
}): PnpmLicenseAuditFiles;
|
|
9
22
|
export declare function preparePnpmLicenseAuditWorkspace(repoRoot: string, lockfileSource: string, workspaceSource: string): PnpmLicenseAuditWorkspace;
|
|
@@ -1,20 +1,20 @@
|
|
|
1
1
|
import { copyFileSync, existsSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs';
|
|
2
2
|
import { tmpdir } from 'node:os';
|
|
3
3
|
import { join } from 'node:path';
|
|
4
|
-
import {
|
|
4
|
+
import { isMap, parseDocument, stringify as stringifyYaml } from 'yaml';
|
|
5
5
|
const PLATFORM_KEYS = ['os', 'cpu', 'libc'];
|
|
6
|
-
function
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
}
|
|
11
|
-
catch (error) {
|
|
6
|
+
function parseYamlDocument(source, path) {
|
|
7
|
+
const document = parseDocument(source);
|
|
8
|
+
const [error] = document.errors;
|
|
9
|
+
if (error)
|
|
12
10
|
throw new Error(`failed to parse ${path}: ${String(error)}`, { cause: error });
|
|
13
|
-
|
|
14
|
-
|
|
11
|
+
return document;
|
|
12
|
+
}
|
|
13
|
+
function assertYamlObject(value, path) {
|
|
14
|
+
if (typeof value !== 'object' || value === null || Array.isArray(value)) {
|
|
15
15
|
throw new Error(`expected ${path} to contain a YAML object`);
|
|
16
16
|
}
|
|
17
|
-
return
|
|
17
|
+
return value;
|
|
18
18
|
}
|
|
19
19
|
function getStringList(value, path) {
|
|
20
20
|
if (typeof value === 'string')
|
|
@@ -24,13 +24,7 @@ function getStringList(value, path) {
|
|
|
24
24
|
}
|
|
25
25
|
return value;
|
|
26
26
|
}
|
|
27
|
-
|
|
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
|
-
}
|
|
27
|
+
function collectSupportedArchitectures(packages, lockfilePath) {
|
|
34
28
|
const supportedArchitectures = {
|
|
35
29
|
cpu: ['current'],
|
|
36
30
|
libc: ['current'],
|
|
@@ -44,27 +38,52 @@ export function renderPnpmLicenseAuditWorkspace(lockfileSource, workspaceSource,
|
|
|
44
38
|
const value = snapshot[key];
|
|
45
39
|
if (value === undefined)
|
|
46
40
|
continue;
|
|
47
|
-
for (const platform of getStringList(value, `${
|
|
41
|
+
for (const platform of getStringList(value, `${lockfilePath} packages.*.${key}`)) {
|
|
48
42
|
values.add(platform);
|
|
49
43
|
}
|
|
50
44
|
}
|
|
51
45
|
supportedArchitectures[key].push(...[...values].filter((value) => value !== 'current').sort());
|
|
52
46
|
}
|
|
53
|
-
return
|
|
47
|
+
return supportedArchitectures;
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Renders the audit copies of `pnpm-lock.yaml` and `pnpm-workspace.yaml` so `pnpm fetch` downloads
|
|
51
|
+
* every package in the lockfile, including optional ones that don't match the host.
|
|
52
|
+
*
|
|
53
|
+
* The workspace supports every `os`, `cpu`, and `libc` in the lockfile. The lockfile drops every
|
|
54
|
+
* `engines` constraint because pnpm 12's `fetch` ignores `force` and skips an optional package whose
|
|
55
|
+
* `engines` exclude the running Node.js, which would leave that package's license Unknown.
|
|
56
|
+
*/
|
|
57
|
+
export function renderPnpmLicenseAuditFiles(lockfileSource, workspaceSource, paths) {
|
|
58
|
+
const lockfileDocument = parseYamlDocument(lockfileSource, paths.lockfile);
|
|
59
|
+
const lockfile = assertYamlObject(lockfileDocument.toJS(), paths.lockfile);
|
|
60
|
+
const workspace = assertYamlObject(parseYamlDocument(workspaceSource, paths.workspace).toJS(), paths.workspace);
|
|
61
|
+
const packageNodes = lockfileDocument.get('packages');
|
|
62
|
+
if (!isMap(packageNodes)) {
|
|
63
|
+
throw new Error(`expected ${paths.lockfile} to contain a packages object`);
|
|
64
|
+
}
|
|
65
|
+
const supportedArchitectures = collectSupportedArchitectures(lockfile.packages, paths.lockfile);
|
|
66
|
+
for (const { value } of packageNodes.items)
|
|
67
|
+
if (isMap(value))
|
|
68
|
+
value.delete('engines');
|
|
69
|
+
return {
|
|
70
|
+
lockfile: lockfileDocument.toString({ lineWidth: 0 }),
|
|
71
|
+
workspace: stringifyYaml({ ...workspace, packages: [], supportedArchitectures }),
|
|
72
|
+
};
|
|
54
73
|
}
|
|
55
74
|
export function preparePnpmLicenseAuditWorkspace(repoRoot, lockfileSource, workspaceSource) {
|
|
56
75
|
const auditRoot = mkdtempSync(join(tmpdir(), 'dependency-license-audit-'));
|
|
57
76
|
try {
|
|
58
|
-
|
|
59
|
-
copyFileSync(join(repoRoot, filename), join(auditRoot, filename));
|
|
60
|
-
}
|
|
77
|
+
copyFileSync(join(repoRoot, 'package.json'), join(auditRoot, 'package.json'));
|
|
61
78
|
const npmrc = join(repoRoot, '.npmrc');
|
|
62
79
|
if (existsSync(npmrc))
|
|
63
80
|
copyFileSync(npmrc, join(auditRoot, '.npmrc'));
|
|
64
|
-
|
|
81
|
+
const files = renderPnpmLicenseAuditFiles(lockfileSource, workspaceSource, {
|
|
65
82
|
lockfile: join(repoRoot, 'pnpm-lock.yaml'),
|
|
66
83
|
workspace: join(repoRoot, 'pnpm-workspace.yaml'),
|
|
67
|
-
})
|
|
84
|
+
});
|
|
85
|
+
writeFileSync(join(auditRoot, 'pnpm-lock.yaml'), files.lockfile, 'utf8');
|
|
86
|
+
writeFileSync(join(auditRoot, 'pnpm-workspace.yaml'), files.workspace, 'utf8');
|
|
68
87
|
}
|
|
69
88
|
catch (error) {
|
|
70
89
|
rmSync(auditRoot, { force: true, recursive: true });
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "vouchington-tooling",
|
|
3
|
-
"version": "0.19.
|
|
3
|
+
"version": "0.19.2",
|
|
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": {
|
|
@@ -126,14 +126,14 @@ download_pattern() {
|
|
|
126
126
|
done
|
|
127
127
|
}
|
|
128
128
|
|
|
129
|
-
|
|
129
|
+
notice_absent() {
|
|
130
130
|
local requested="$1" count=$(($# - 1)) list='' name
|
|
131
131
|
shift
|
|
132
132
|
for name in "${@:1:3}"; do
|
|
133
133
|
list="${list:+$list, }$name"
|
|
134
134
|
done
|
|
135
135
|
[ "$count" -le 3 ] || list="$list and $((count - 3)) more"
|
|
136
|
-
echo "::
|
|
136
|
+
echo "::notice::Optional same-run artifacts absent: $count of $requested requested (${list//\%/%25})" >&2
|
|
137
137
|
}
|
|
138
138
|
|
|
139
139
|
download_names() {
|
|
@@ -152,7 +152,7 @@ download_names() {
|
|
|
152
152
|
if contains "$name" "${listed[@]}"; then present+=("$name"); else absent+=("$name"); fi
|
|
153
153
|
done
|
|
154
154
|
echo "[optional-run-artifacts] selection selector=name requested=${#names[@]} present=${#present[@]} absent=${#absent[@]}" >&2
|
|
155
|
-
[ "${#absent[@]}" -eq 0 ] ||
|
|
155
|
+
[ "${#absent[@]}" -eq 0 ] || notice_absent "${#names[@]}" "${absent[@]}"
|
|
156
156
|
[ "${#present[@]}" -gt 0 ] || return 3
|
|
157
157
|
for name in "${present[@]}"; do
|
|
158
158
|
echo "[optional-run-artifacts] attempt selector=name artifact=$name" >&2
|