@wpelevator/wp-release 0.15.0 → 0.16.0
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 +246 -37
- package/dist/changeset-config.d.ts +3 -0
- package/dist/changeset-release.d.ts +9 -3
- package/dist/changeset-release.js +23 -14
- package/dist/cli.d.ts +1 -0
- package/dist/cli.js +2 -0
- package/dist/commands/artifact-signing.d.ts +20 -0
- package/dist/commands/artifact-signing.js +42 -0
- package/dist/commands/build.d.ts +32 -0
- package/dist/commands/build.js +91 -0
- package/dist/commands/changeset-add.d.ts +55 -6
- package/dist/commands/changeset-add.js +12 -3
- package/dist/commands/changeset-args.js +2 -2
- package/dist/commands/changeset-project-args.d.ts +63 -0
- package/dist/commands/changeset-project-args.js +114 -0
- package/dist/commands/changeset-status.d.ts +50 -1
- package/dist/commands/changeset-status.js +4 -2
- package/dist/commands/changeset-tag.d.ts +55 -6
- package/dist/commands/changeset-tag.js +7 -4
- package/dist/commands/changeset-version.d.ts +55 -6
- package/dist/commands/changeset-version.js +4 -2
- package/dist/commands/checksums.d.ts +4 -1
- package/dist/commands/checksums.js +27 -6
- package/dist/commands/package-source.d.ts +43 -0
- package/dist/commands/package-source.js +101 -0
- package/dist/commands/verify.d.ts +7 -1
- package/dist/commands/verify.js +53 -17
- package/dist/commands/zip.d.ts +20 -8
- package/dist/commands/zip.js +109 -124
- package/dist/index.d.ts +6 -0
- package/dist/index.js +2 -0
- package/dist/integrity.d.ts +33 -0
- package/dist/integrity.js +115 -0
- package/dist/output-files.d.ts +7 -0
- package/dist/output-files.js +25 -0
- package/dist/package-build.d.ts +33 -0
- package/dist/package-build.js +112 -0
- package/dist/package-files.d.ts +25 -0
- package/dist/package-files.js +24 -0
- package/dist/package-zip.d.ts +14 -2
- package/dist/package-zip.js +129 -38
- package/dist/release-tags.js +6 -0
- package/dist/single-project.d.ts +32 -0
- package/dist/single-project.js +144 -0
- package/dist/summary.d.ts +9 -0
- package/dist/summary.js +25 -0
- package/dist/zip.d.ts +3 -0
- package/dist/zip.js +16 -6
- package/package.json +6 -1
package/dist/commands/verify.js
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
|
+
import { readIntegrity, checkChecksumsFile, resolveManifestCodec, } from "../integrity.js";
|
|
2
|
+
import { loadChangesetConfig } from "../changeset-config.js";
|
|
1
3
|
import { existsSync } from 'node:fs';
|
|
2
4
|
import { mkdir, mkdtemp, readFile, rm } from 'node:fs/promises';
|
|
3
5
|
import { tmpdir } from 'node:os';
|
|
4
6
|
import { basename, isAbsolute, join, posix, resolve } from 'node:path';
|
|
5
|
-
import {
|
|
7
|
+
import { verifyChecksums } from "../checksums.js";
|
|
6
8
|
import { downloadFile, isUrl } from "../download.js";
|
|
7
9
|
import { ignoreOwnFile, ignoreOwnSignature, loadFileRules, parseExclude, } from "../file-selection.js";
|
|
8
10
|
import { ValidationError } from "../errors.js";
|
|
@@ -105,7 +107,7 @@ function getEmbeddedPath(checksumsPath, embedded) {
|
|
|
105
107
|
['.', '..'].includes(path))) {
|
|
106
108
|
throw new ValidationError(`--embedded must be a path inside the plugin with forward slashes, got "${embedded}".`);
|
|
107
109
|
}
|
|
108
|
-
return
|
|
110
|
+
return path;
|
|
109
111
|
}
|
|
110
112
|
/**
|
|
111
113
|
* Get the manifest and signature pairs to look for inside the checked files.
|
|
@@ -180,9 +182,22 @@ async function hasSameContents(original, file) {
|
|
|
180
182
|
*/
|
|
181
183
|
async function checkEmbeddedCopies(input) {
|
|
182
184
|
const { dir, manifest, checksumsPath, embedded, format } = input;
|
|
183
|
-
const
|
|
185
|
+
const path = getEmbeddedPath(checksumsPath, embedded);
|
|
186
|
+
const pairs = getCopyPairs(checksumsPath, path, format).filter((pair) => pair.original !== join(dir, pair.copy) ||
|
|
187
|
+
existsSync(pair.original));
|
|
188
|
+
for (const other of Object.keys(SIGNATURE_FORMATS)) {
|
|
189
|
+
if (other !== format &&
|
|
190
|
+
path &&
|
|
191
|
+
existsSync(getSignaturePath(checksumsPath, other))) {
|
|
192
|
+
pairs.push({
|
|
193
|
+
original: getSignaturePath(checksumsPath, other),
|
|
194
|
+
copy: getSignaturePath(path, other),
|
|
195
|
+
});
|
|
196
|
+
}
|
|
197
|
+
}
|
|
184
198
|
if (embedded) {
|
|
185
|
-
checkExplicitPairs(pairs,
|
|
199
|
+
checkExplicitPairs(pairs.filter((pair) => pair.original !== join(dir, pair.copy) ||
|
|
200
|
+
existsSync(pair.original)), manifest);
|
|
186
201
|
}
|
|
187
202
|
const found = pairs.filter(({ original, copy }) => existsSync(join(dir, copy)) &&
|
|
188
203
|
(embedded ||
|
|
@@ -336,8 +351,10 @@ export default {
|
|
|
336
351
|
accepted as --source-dir <dir>
|
|
337
352
|
|
|
338
353
|
Options:
|
|
339
|
-
--checksums <file>
|
|
340
|
-
|
|
354
|
+
--checksums-file <file> Relative embedded manifest path (default: checksums.json)
|
|
355
|
+
--checksums-format <name> wordpress-json or sha256sum; custom codecs via --config
|
|
356
|
+
--checksums <file> External checksums manifest from "wp-release checksums" or the
|
|
357
|
+
WordPress.org checksums API (optional; otherwise reads embedded manifest)
|
|
341
358
|
--zip <file|url> Check the files of this ZIP, a local file or an
|
|
342
359
|
http(s) URL such as the ZIP that WordPress.org
|
|
343
360
|
builds for a release, instead of <source-dir>
|
|
@@ -381,6 +398,8 @@ file that isn't a matching copy is reported as extra. See --embedded for
|
|
|
381
398
|
where the copies are looked for.`,
|
|
382
399
|
options: {
|
|
383
400
|
checksums: { type: 'string' },
|
|
401
|
+
'checksums-format': { type: 'string' },
|
|
402
|
+
'checksums-file': { type: 'string' },
|
|
384
403
|
zip: { type: 'string' },
|
|
385
404
|
'root-folder': { type: 'string' },
|
|
386
405
|
embedded: { type: 'string' },
|
|
@@ -389,42 +408,59 @@ where the copies are looked for.`,
|
|
|
389
408
|
...sourceDirArgs,
|
|
390
409
|
exclude: { type: 'string', multiple: true },
|
|
391
410
|
},
|
|
392
|
-
async run({ values, positionals, cwd, logger }) {
|
|
411
|
+
async run({ values, positionals, cwd, configFile, logger }) {
|
|
393
412
|
const zip = getString(values, 'zip');
|
|
394
413
|
const sourceArg = positionals.length
|
|
395
414
|
? getSourceDirArg(values, positionals)
|
|
396
415
|
: getString(values, 'source-dir');
|
|
397
416
|
const checksums = getString(values, 'checksums');
|
|
398
|
-
if (!checksums) {
|
|
399
|
-
throw new ValidationError('Pass the checksums manifest with --checksums <file>.');
|
|
400
|
-
}
|
|
401
417
|
if (zip && sourceArg) {
|
|
402
418
|
throw new ValidationError('Pass either --zip or a source directory, not both.');
|
|
403
419
|
}
|
|
404
|
-
|
|
420
|
+
// Only load caller-selected configuration, never configuration inside the package.
|
|
421
|
+
const loaded = configFile
|
|
422
|
+
? await loadChangesetConfig({ cwd, configFile })
|
|
423
|
+
: undefined;
|
|
424
|
+
const integrity = loaded?.config.integrity;
|
|
425
|
+
const codec = getString(values, 'checksums-format') ?? integrity?.codec;
|
|
426
|
+
const file = checkChecksumsFile(getString(values, 'checksums-file') ??
|
|
427
|
+
integrity?.file ??
|
|
428
|
+
resolveManifestCodec(codec).filename);
|
|
405
429
|
const publicKey = getString(values, 'public-key');
|
|
406
430
|
const format = getString(values, 'signature-format') ?? DEFAULT_SIGNATURE_FORMAT;
|
|
407
431
|
if (!isSignatureFormat(format)) {
|
|
408
432
|
throw new ValidationError(`Unknown signature format ${format}, use ${Object.keys(SIGNATURE_FORMATS).join(', ')}.`);
|
|
409
433
|
}
|
|
410
|
-
if (publicKey) {
|
|
411
|
-
await checkSignature(checksumsPath, publicKey, format);
|
|
412
|
-
logger.info(`Signature of ${checksumsPath} is valid.`);
|
|
413
|
-
}
|
|
414
|
-
const manifest = await readChecksums(checksumsPath);
|
|
415
434
|
const target = await resolveTarget({
|
|
416
435
|
sourceDir: resolve(cwd, sourceArg ?? '.'),
|
|
417
436
|
zip,
|
|
418
437
|
cwd,
|
|
419
438
|
rootFolder: getString(values, 'root-folder'),
|
|
420
439
|
});
|
|
440
|
+
const checksumsPath = checksums
|
|
441
|
+
? resolve(cwd, checksums)
|
|
442
|
+
: join(target.dir, file);
|
|
421
443
|
try {
|
|
444
|
+
if (publicKey) {
|
|
445
|
+
await checkSignature(checksumsPath, publicKey, format);
|
|
446
|
+
if (!checksums) {
|
|
447
|
+
for (const other of Object.keys(SIGNATURE_FORMATS)) {
|
|
448
|
+
if (other !== format &&
|
|
449
|
+
existsSync(getSignaturePath(checksumsPath, other))) {
|
|
450
|
+
await checkSignature(checksumsPath, publicKey, other);
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
logger.info(`Signature of ${checksumsPath} is valid.`);
|
|
455
|
+
}
|
|
456
|
+
const manifest = await readIntegrity(checksumsPath, codec);
|
|
422
457
|
return await verifyTarget({
|
|
423
458
|
target,
|
|
424
459
|
manifest,
|
|
425
460
|
checksumsPath,
|
|
426
461
|
exclude: getStrings(values, 'exclude'),
|
|
427
|
-
embedded: getString(values, 'embedded')
|
|
462
|
+
embedded: getString(values, 'embedded') ??
|
|
463
|
+
(checksums ? undefined : file),
|
|
428
464
|
format,
|
|
429
465
|
logger,
|
|
430
466
|
});
|
package/dist/commands/zip.d.ts
CHANGED
|
@@ -8,9 +8,9 @@ export interface ZipResult extends ZipPackageResult {
|
|
|
8
8
|
version: string | null;
|
|
9
9
|
/** Zipped package, with --package. */
|
|
10
10
|
package?: string;
|
|
11
|
-
/**
|
|
11
|
+
/** External signature paths and package-relative embedded manifest signature paths. */
|
|
12
12
|
signatures?: string[];
|
|
13
|
-
/** Base64 public key of the signing key, with
|
|
13
|
+
/** Base64 public key of the signing key, with ZIP or manifest signing. */
|
|
14
14
|
publicKey?: string;
|
|
15
15
|
}
|
|
16
16
|
declare const _default: {
|
|
@@ -19,22 +19,34 @@ declare const _default: {
|
|
|
19
19
|
summary: string;
|
|
20
20
|
help: string;
|
|
21
21
|
options: {
|
|
22
|
-
|
|
22
|
+
'checksums-file': {
|
|
23
|
+
type: "string";
|
|
24
|
+
};
|
|
25
|
+
'checksums-format': {
|
|
26
|
+
type: "string";
|
|
27
|
+
};
|
|
28
|
+
'checksums-algorithms': {
|
|
23
29
|
type: "string";
|
|
30
|
+
multiple: true;
|
|
24
31
|
};
|
|
25
|
-
checksums: {
|
|
32
|
+
'embed-checksums': {
|
|
26
33
|
type: "boolean";
|
|
27
34
|
};
|
|
28
|
-
|
|
35
|
+
'no-embed-checksums': {
|
|
29
36
|
type: "boolean";
|
|
30
37
|
};
|
|
31
|
-
'sign-
|
|
32
|
-
type: "
|
|
33
|
-
|
|
38
|
+
'no-sign-checksums': {
|
|
39
|
+
type: "boolean";
|
|
40
|
+
};
|
|
41
|
+
sign: {
|
|
42
|
+
type: "boolean";
|
|
34
43
|
};
|
|
35
44
|
force: {
|
|
36
45
|
type: "boolean";
|
|
37
46
|
};
|
|
47
|
+
package: {
|
|
48
|
+
type: "string";
|
|
49
|
+
};
|
|
38
50
|
slug: {
|
|
39
51
|
type: "string";
|
|
40
52
|
};
|
package/dist/commands/zip.js
CHANGED
|
@@ -1,108 +1,15 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { checksumSigningArgs, getArtifactSigning } from "./artifact-signing.js";
|
|
2
2
|
import { ValidationError } from "../errors.js";
|
|
3
|
-
import {
|
|
3
|
+
import { basename, dirname, resolve, sep } from 'node:path';
|
|
4
|
+
import { loadChangesetConfig } from "../changeset-config.js";
|
|
5
|
+
import { resolveManifestCodec, checkChecksumsFile } from "../integrity.js";
|
|
6
|
+
import { getPackageArg, getPackageSource, getDirectorySource, } from "./package-source.js";
|
|
4
7
|
import { FILENAME_FORMAT, formatFilename } from "../filename.js";
|
|
5
|
-
import { getPackageFiles } from "../package-deploy.js";
|
|
6
|
-
import { readPackageMeta } from "../package-meta.js";
|
|
7
8
|
import { getChecksumsPath, zipPackage } from "../package-zip.js";
|
|
8
|
-
import { SIGNING_KEY_ENV,
|
|
9
|
+
import { SIGNING_KEY_ENV, signFiles, getSignaturePath } from "../signing.js";
|
|
9
10
|
import { getSignSummary, getZipSummary } from "../summary.js";
|
|
10
|
-
import {
|
|
11
|
+
import { fileSelectionArgs, parseSignatureFormats, resolveOutput, sourceDirArgs, } from "./args.js";
|
|
11
12
|
import { getBoolean, getString, getStrings } from "./command.js";
|
|
12
|
-
/** Options of a source directory and its files, which --package takes from the config. */
|
|
13
|
-
const SOURCE_FLAGS = [
|
|
14
|
-
'source-dir',
|
|
15
|
-
'distignore',
|
|
16
|
-
'no-distignore',
|
|
17
|
-
'exclude',
|
|
18
|
-
'exclude-gitattributes',
|
|
19
|
-
];
|
|
20
|
-
/**
|
|
21
|
-
* Get the configured package to zip, failing for the options that set the
|
|
22
|
-
* source directory and the files instead.
|
|
23
|
-
*
|
|
24
|
-
* @param values Parsed CLI values.
|
|
25
|
-
* @param positionals Positional arguments.
|
|
26
|
-
* @return The package identifier or path, or undefined without --package.
|
|
27
|
-
* @throws {ValidationError} For an empty --package, or --package with a source directory or file selection option.
|
|
28
|
-
*/
|
|
29
|
-
function getPackageArg(values, positionals) {
|
|
30
|
-
const name = getString(values, 'package')?.trim();
|
|
31
|
-
const conflicts = SOURCE_FLAGS.filter((flag) => undefined !== getString(values, flag) ||
|
|
32
|
-
getBoolean(values, flag) ||
|
|
33
|
-
getStrings(values, flag).length);
|
|
34
|
-
if ('' === name) {
|
|
35
|
-
throw new ValidationError('--package <name> must not be empty.');
|
|
36
|
-
}
|
|
37
|
-
if (name && (positionals.length || conflicts.length)) {
|
|
38
|
-
throw new ValidationError(`--package takes the directory and files of the package from the config, so it can't be combined with ${positionals.length
|
|
39
|
-
? 'a source directory'
|
|
40
|
-
: conflicts.map((flag) => `--${flag}`).join(', ')}.`);
|
|
41
|
-
}
|
|
42
|
-
return name;
|
|
43
|
-
}
|
|
44
|
-
/**
|
|
45
|
-
* Describe the files of a directory with the slug and the version from the
|
|
46
|
-
* options, or from the plugin or theme headers.
|
|
47
|
-
*
|
|
48
|
-
* @param values Parsed CLI values.
|
|
49
|
-
* @param sourceDir Directory to zip.
|
|
50
|
-
* @param selection File selection.
|
|
51
|
-
* @param slug Default slug.
|
|
52
|
-
* @return Source directory, file selection, slug, version and type.
|
|
53
|
-
*/
|
|
54
|
-
async function describeSource(values, sourceDir, selection, slug) {
|
|
55
|
-
const name = getString(values, 'slug') ?? slug;
|
|
56
|
-
const meta = await readPackageMeta({ dir: sourceDir, slug: name });
|
|
57
|
-
return {
|
|
58
|
-
sourceDir,
|
|
59
|
-
selection,
|
|
60
|
-
slug: name,
|
|
61
|
-
version: getString(values, 'version') ?? meta?.version ?? null,
|
|
62
|
-
...(meta ? { type: meta.type } : {}),
|
|
63
|
-
};
|
|
64
|
-
}
|
|
65
|
-
/**
|
|
66
|
-
* Get the files of a source directory from the CLI options.
|
|
67
|
-
*
|
|
68
|
-
* @param values Parsed CLI values.
|
|
69
|
-
* @param positionals Positional arguments.
|
|
70
|
-
* @param cwd Working directory.
|
|
71
|
-
* @return Source directory, file selection, slug and version.
|
|
72
|
-
*/
|
|
73
|
-
async function getDirectorySource(values, positionals, cwd) {
|
|
74
|
-
const sourceDir = resolve(cwd, getSourceDirArg(values, positionals));
|
|
75
|
-
await checkDirectory(sourceDir);
|
|
76
|
-
return describeSource(values, sourceDir, resolveFileSelection({
|
|
77
|
-
cwd,
|
|
78
|
-
sourceDir,
|
|
79
|
-
...getFileSelectionInput(values),
|
|
80
|
-
}), basename(cwd));
|
|
81
|
-
}
|
|
82
|
-
/**
|
|
83
|
-
* Get the directory of a configured package with the files config of its
|
|
84
|
-
* package group, which its deploys use too.
|
|
85
|
-
*
|
|
86
|
-
* @param values Parsed CLI values.
|
|
87
|
-
* @param cwd Repository root with the wp-release config.
|
|
88
|
-
* @param name Package identifier or path under the configured root.
|
|
89
|
-
* @param configFile Config file, instead of the conventional one.
|
|
90
|
-
* @return Source directory, file selection, slug, version and package.
|
|
91
|
-
* @throws {ValidationError} Without a config or for an unknown package.
|
|
92
|
-
*/
|
|
93
|
-
async function getPackageSource(values, cwd, name, configFile) {
|
|
94
|
-
const pkg = await getPackageFiles({
|
|
95
|
-
cwd,
|
|
96
|
-
package: name,
|
|
97
|
-
...(configFile ? { configFile } : {}),
|
|
98
|
-
});
|
|
99
|
-
const source = await describeSource(values, pkg.directory, resolveFileSelection({
|
|
100
|
-
cwd,
|
|
101
|
-
sourceDir: pkg.directory,
|
|
102
|
-
...pkg.files,
|
|
103
|
-
}), basename(pkg.directory));
|
|
104
|
-
return { ...source, package: pkg.name };
|
|
105
|
-
}
|
|
106
13
|
export default {
|
|
107
14
|
name: 'zip',
|
|
108
15
|
usage: 'zip [<source-dir>] [options] | zip --package <name> [options]',
|
|
@@ -135,18 +42,24 @@ Options:
|
|
|
135
42
|
--checksums Also write the checksums manifest of the archived
|
|
136
43
|
files next to the ZIP, with .checksums.json
|
|
137
44
|
instead of .zip
|
|
138
|
-
--
|
|
139
|
-
the
|
|
140
|
-
|
|
141
|
-
|
|
45
|
+
--checksums-file <file> Relative manifest filename; implies checksums.
|
|
46
|
+
Beside the ZIP, or inside its package root with
|
|
47
|
+
--embed-checksums. No absolute paths or dot segments.
|
|
48
|
+
--checksums-format <format> wordpress-json (default) or sha256sum
|
|
49
|
+
--checksums-algorithms <names> md5,sha256 (default), comma-separated or repeated
|
|
50
|
+
--embed-checksums Embed manifest instead of writing it beside ZIP
|
|
51
|
+
--no-embed-checksums Override embedding configured in integrity
|
|
52
|
+
--sign-checksums Sign the manifest only, before embedding it
|
|
53
|
+
--no-sign-checksums Override configured manifest signing
|
|
54
|
+
--sign Sign only the ZIP with the ${SIGNING_KEY_ENV} key
|
|
142
55
|
--sign-format <names> Signature formats of --sign, comma-separated or
|
|
143
56
|
repeated, like wordpress,minisign,ssh, as the
|
|
144
57
|
--format of "wp-release sign" (default:
|
|
145
|
-
wordpress);
|
|
58
|
+
wordpress); requires --sign or --sign-checksums
|
|
146
59
|
--force Overwrite existing output files
|
|
147
60
|
|
|
148
|
-
Entries are sorted and use a fixed timestamp and file mode
|
|
149
|
-
|
|
61
|
+
Entries are sorted and use a fixed timestamp and file mode. Embedded minisign
|
|
62
|
+
signatures include their signing time; other built-in metadata is deterministic.
|
|
150
63
|
|
|
151
64
|
Examples:
|
|
152
65
|
wp-release zip dist --slug my-plugin --checksums
|
|
@@ -163,9 +76,14 @@ Examples:
|
|
|
163
76
|
...sourceDirArgs,
|
|
164
77
|
...fileSelectionArgs,
|
|
165
78
|
package: { type: 'string' },
|
|
166
|
-
|
|
79
|
+
...checksumSigningArgs,
|
|
80
|
+
'checksums-file': { type: 'string' },
|
|
81
|
+
'checksums-format': { type: 'string' },
|
|
82
|
+
'checksums-algorithms': { type: 'string', multiple: true },
|
|
83
|
+
'embed-checksums': { type: 'boolean' },
|
|
84
|
+
'no-embed-checksums': { type: 'boolean' },
|
|
85
|
+
'no-sign-checksums': { type: 'boolean' },
|
|
167
86
|
sign: { type: 'boolean' },
|
|
168
|
-
'sign-format': { type: 'string', multiple: true },
|
|
169
87
|
force: { type: 'boolean' },
|
|
170
88
|
},
|
|
171
89
|
async run({ values, positionals, cwd, configFile, logger, }) {
|
|
@@ -179,21 +97,68 @@ Examples:
|
|
|
179
97
|
output: getString(values, 'output'),
|
|
180
98
|
filename: `${filename}.zip`,
|
|
181
99
|
});
|
|
100
|
+
const loaded = await loadChangesetConfig({ cwd, configFile });
|
|
101
|
+
const configured = loaded?.config.integrity;
|
|
102
|
+
const algorithms = getStrings(values, 'checksums-algorithms').flatMap((value) => value.split(','));
|
|
103
|
+
const integrity = {
|
|
104
|
+
...configured,
|
|
105
|
+
codec: getString(values, 'checksums-format') ?? configured?.codec,
|
|
106
|
+
...(algorithms.length
|
|
107
|
+
? { algorithms: algorithms }
|
|
108
|
+
: {}),
|
|
109
|
+
};
|
|
110
|
+
const codec = resolveManifestCodec(integrity.codec);
|
|
111
|
+
const embed = !getBoolean(values, 'no-embed-checksums') &&
|
|
112
|
+
(getBoolean(values, 'embed-checksums') ||
|
|
113
|
+
Boolean(configured?.embed));
|
|
114
|
+
const checksumsFile = checkChecksumsFile(getString(values, 'checksums-file') ??
|
|
115
|
+
configured?.file ??
|
|
116
|
+
(codec.id === 'wordpress-json'
|
|
117
|
+
? basename(getChecksumsPath(zipPath))
|
|
118
|
+
: codec.filename));
|
|
119
|
+
const manifestSign = !getBoolean(values, 'no-sign-checksums') &&
|
|
120
|
+
(getBoolean(values, 'sign-checksums') ||
|
|
121
|
+
Boolean(configured?.sign));
|
|
122
|
+
const checksums = getBoolean(values, 'checksums') ||
|
|
123
|
+
embed ||
|
|
124
|
+
manifestSign ||
|
|
125
|
+
Boolean(getString(values, 'checksums-file') ?? configured?.file);
|
|
182
126
|
const signFormats = getStrings(values, 'sign-format');
|
|
183
|
-
const
|
|
184
|
-
?
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
127
|
+
const configuredFormats = configured?.sign
|
|
128
|
+
? configured.sign.formats ?? []
|
|
129
|
+
: [];
|
|
130
|
+
const formats = parseSignatureFormats(signFormats.length ? signFormats : configuredFormats);
|
|
131
|
+
if (signFormats.length &&
|
|
132
|
+
!manifestSign &&
|
|
133
|
+
!getBoolean(values, 'sign')) {
|
|
134
|
+
throw new ValidationError('--sign-format requires --sign or --sign-checksums; it does not enable signing.');
|
|
135
|
+
}
|
|
136
|
+
const signing = getBoolean(values, 'sign')
|
|
137
|
+
? getArtifactSigning({
|
|
138
|
+
...values,
|
|
139
|
+
'sign-checksums': false,
|
|
140
|
+
'sign-format': formats,
|
|
141
|
+
}, {
|
|
142
|
+
artifact: zipPath,
|
|
143
|
+
checksums: resolve(dirname(zipPath), checksumsFile),
|
|
144
|
+
})
|
|
194
145
|
: undefined;
|
|
195
|
-
|
|
196
|
-
|
|
146
|
+
const externalManifest = checksums && !embed
|
|
147
|
+
? resolve(dirname(zipPath), checksumsFile)
|
|
148
|
+
: undefined;
|
|
149
|
+
const artifacts = [
|
|
150
|
+
zipPath,
|
|
151
|
+
...(externalManifest ? [externalManifest] : []),
|
|
152
|
+
...(signing
|
|
153
|
+
? formats.map((format) => getSignaturePath(zipPath, format))
|
|
154
|
+
: []),
|
|
155
|
+
...(externalManifest && manifestSign
|
|
156
|
+
? formats.map((format) => getSignaturePath(externalManifest, format))
|
|
157
|
+
: []),
|
|
158
|
+
];
|
|
159
|
+
if (new Set(artifacts).size !== artifacts.length ||
|
|
160
|
+
artifacts.some((path, index) => artifacts.some((other, otherIndex) => index !== otherIndex && other.startsWith(path + sep)))) {
|
|
161
|
+
throw new ValidationError('ZIP, manifest and signature destinations must be distinct.');
|
|
197
162
|
}
|
|
198
163
|
const zipped = await zipPackage({
|
|
199
164
|
sourceDir: source.sourceDir,
|
|
@@ -205,7 +170,11 @@ Examples:
|
|
|
205
170
|
slug: source.slug,
|
|
206
171
|
version: source.version,
|
|
207
172
|
...(source.type ? { type: source.type } : {}),
|
|
208
|
-
checksums
|
|
173
|
+
checksums,
|
|
174
|
+
checksumsFile: checksums ? checksumsFile : undefined,
|
|
175
|
+
embedChecksums: embed,
|
|
176
|
+
integrity,
|
|
177
|
+
signChecksums: manifestSign ? formats : undefined,
|
|
209
178
|
force: getBoolean(values, 'force'),
|
|
210
179
|
});
|
|
211
180
|
logger.debug(`Archived ${zipped.files} files from ${source.sourceDir}.`);
|
|
@@ -219,9 +188,18 @@ Examples:
|
|
|
219
188
|
signed?.signatures.forEach((path) => logger.info(path));
|
|
220
189
|
const result = {
|
|
221
190
|
...zipped,
|
|
191
|
+
...(zipped.checksumsSigning
|
|
192
|
+
? {
|
|
193
|
+
signatures: zipped.checksumsSignatures,
|
|
194
|
+
publicKey: zipped.checksumsSigning.publicKey,
|
|
195
|
+
}
|
|
196
|
+
: {}),
|
|
222
197
|
...(signed
|
|
223
198
|
? {
|
|
224
|
-
signatures:
|
|
199
|
+
signatures: [
|
|
200
|
+
...signed.signatures,
|
|
201
|
+
...(zipped.checksumsSignatures ?? []),
|
|
202
|
+
],
|
|
225
203
|
publicKey: signed.publicKey,
|
|
226
204
|
}
|
|
227
205
|
: {}),
|
|
@@ -230,6 +208,13 @@ Examples:
|
|
|
230
208
|
...(source.package ? { package: source.package } : {}),
|
|
231
209
|
};
|
|
232
210
|
logger.summary((context) => getZipSummary(context, result));
|
|
211
|
+
const checksumsSigning = zipped.checksumsSigning;
|
|
212
|
+
if (checksumsSigning) {
|
|
213
|
+
logger.summary((context) => getSignSummary(context, {
|
|
214
|
+
...checksumsSigning,
|
|
215
|
+
formats,
|
|
216
|
+
}));
|
|
217
|
+
}
|
|
233
218
|
if (signed) {
|
|
234
219
|
logger.summary((context) => getSignSummary(context, signed));
|
|
235
220
|
}
|
package/dist/index.d.ts
CHANGED
|
@@ -59,3 +59,9 @@ export type { SummaryContext } from './summary.ts';
|
|
|
59
59
|
export type { CheckoutPath, DeployResult } from './svn-deploy.ts';
|
|
60
60
|
export type { GitCommandInput, GitCommandOutput, GitRunner, RepositoryLocation, } from './git.ts';
|
|
61
61
|
export type { GitDeployInput, GitDeployOptions, GitDeployResult, } from './git-deploy.ts';
|
|
62
|
+
export type { SingleProjectOptions } from './single-project.ts';
|
|
63
|
+
export { buildPackage } from './package-build.ts';
|
|
64
|
+
export type { BuildPackageInput, BuildPackageResult } from './package-build.ts';
|
|
65
|
+
export type { BuildResult } from './commands/build.ts';
|
|
66
|
+
export { wordpressCodec, sha256sumCodec, resolveManifestCodec, readIntegrity, } from './integrity.ts';
|
|
67
|
+
export type { ManifestCodec, IntegrityConfig } from './integrity.ts';
|
package/dist/index.js
CHANGED
|
@@ -27,3 +27,5 @@ export { GitDeploy, getDefaultToken, getGitAuthEnv, isValidRefName, resolveGitDe
|
|
|
27
27
|
export { getGitDeployInputFromArgs } from "./commands/deploy-git.js";
|
|
28
28
|
export { getDeployPackages, getGitDeploys, getPackageFiles, getSvnDeploys, } from "./package-deploy.js";
|
|
29
29
|
export { zipPackage } from "./package-zip.js";
|
|
30
|
+
export { buildPackage } from "./package-build.js";
|
|
31
|
+
export { wordpressCodec, sha256sumCodec, resolveManifestCodec, readIntegrity, } from "./integrity.js";
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import type { ChecksumsManifest, ChecksumAlgorithm } from './checksums.ts';
|
|
2
|
+
import type { SignatureFormat } from './signing.ts';
|
|
3
|
+
/** Custom codecs receive the validated WordPress-compatible integrity model. */
|
|
4
|
+
export interface ManifestCodec {
|
|
5
|
+
id: string;
|
|
6
|
+
filename: string;
|
|
7
|
+
encode: (manifest: ChecksumsManifest) => Uint8Array | Promise<Uint8Array>;
|
|
8
|
+
decode: (bytes: Uint8Array) => ChecksumsManifest | Promise<ChecksumsManifest>;
|
|
9
|
+
}
|
|
10
|
+
export interface IntegrityConfig {
|
|
11
|
+
codec?: 'wordpress-json' | 'sha256sum' | ManifestCodec;
|
|
12
|
+
algorithms?: ChecksumAlgorithm[];
|
|
13
|
+
file?: string;
|
|
14
|
+
embed?: boolean;
|
|
15
|
+
sign?: false | {
|
|
16
|
+
formats?: SignatureFormat[];
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
export declare const wordpressCodec: ManifestCodec;
|
|
20
|
+
export declare const sha256sumCodec: ManifestCodec;
|
|
21
|
+
export declare function resolveManifestCodec(value?: IntegrityConfig['codec']): ManifestCodec;
|
|
22
|
+
/**
|
|
23
|
+
* Relative artifact paths cannot escape their destination or use Windows paths.
|
|
24
|
+
* @param file
|
|
25
|
+
*/
|
|
26
|
+
export declare function checkChecksumsFile(file: string): string;
|
|
27
|
+
export declare function readIntegrity(path: string, codec?: IntegrityConfig['codec']): Promise<ChecksumsManifest>;
|
|
28
|
+
/**
|
|
29
|
+
* Encode once and ensure the format preserves the selected payload hashes.
|
|
30
|
+
* @param codec
|
|
31
|
+
* @param manifest
|
|
32
|
+
*/
|
|
33
|
+
export declare function encodeIntegrity(codec: ManifestCodec, manifest: ChecksumsManifest): Promise<Buffer>;
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import { readFile } from 'node:fs/promises';
|
|
2
|
+
import { basename } from 'node:path';
|
|
3
|
+
import { parseChecksums, readChecksums } from "./checksums.js";
|
|
4
|
+
import { ValidationError } from "./errors.js";
|
|
5
|
+
export const wordpressCodec = {
|
|
6
|
+
id: 'wordpress-json',
|
|
7
|
+
filename: 'checksums.json',
|
|
8
|
+
encode: (manifest) => Buffer.from(JSON.stringify(manifest, null, '\t') + '\n'),
|
|
9
|
+
decode: (bytes) => parseChecksums(Buffer.from(bytes).toString('utf8')),
|
|
10
|
+
};
|
|
11
|
+
export const sha256sumCodec = {
|
|
12
|
+
id: 'sha256sum',
|
|
13
|
+
filename: 'SHA256SUMS',
|
|
14
|
+
encode(manifest) {
|
|
15
|
+
const lines = Object.entries(manifest.files)
|
|
16
|
+
.sort()
|
|
17
|
+
.map(([path, hashes]) => {
|
|
18
|
+
if ('string' !== typeof hashes.sha256) {
|
|
19
|
+
throw new ValidationError('sha256sum requires one SHA-256 digest per file.');
|
|
20
|
+
}
|
|
21
|
+
const escaped = /[\\\n\r]/.test(path);
|
|
22
|
+
const name = path
|
|
23
|
+
.replaceAll('\\', '\\\\')
|
|
24
|
+
.replaceAll('\n', '\\n')
|
|
25
|
+
.replaceAll('\r', '\\r');
|
|
26
|
+
return `${escaped ? '\\' : ''}${hashes.sha256} ${name}`;
|
|
27
|
+
});
|
|
28
|
+
return Buffer.from(lines.join('\n') + '\n');
|
|
29
|
+
},
|
|
30
|
+
decode(bytes) {
|
|
31
|
+
const files = Object.create(null);
|
|
32
|
+
for (const line of Buffer.from(bytes)
|
|
33
|
+
.toString('utf8')
|
|
34
|
+
.split('\n')
|
|
35
|
+
.filter(Boolean)) {
|
|
36
|
+
const match = /^(\\?)([a-fA-F0-9]{64}) [ *](.+)$/.exec(line);
|
|
37
|
+
if (!match) {
|
|
38
|
+
throw new ValidationError('Invalid SHA256SUMS entry.');
|
|
39
|
+
}
|
|
40
|
+
const path = match[1]
|
|
41
|
+
? (match[3] ?? '').replace(/\\([\\nr])/g, (_, char) => ({ n: '\n', r: '\r', '\\': '\\' })[char] ?? '')
|
|
42
|
+
: match[3] ?? '';
|
|
43
|
+
if (Object.hasOwn(files, path)) {
|
|
44
|
+
throw new ValidationError(`Duplicate checksum path: ${path}`);
|
|
45
|
+
}
|
|
46
|
+
files[path] = { sha256: (match[2] ?? '').toLowerCase() };
|
|
47
|
+
}
|
|
48
|
+
return parseChecksums(JSON.stringify({ files }));
|
|
49
|
+
},
|
|
50
|
+
};
|
|
51
|
+
export function resolveManifestCodec(value = 'wordpress-json') {
|
|
52
|
+
if (typeof value === 'string') {
|
|
53
|
+
if (!['wordpress-json', 'sha256sum'].includes(value)) {
|
|
54
|
+
throw new ValidationError(`Unknown checksums format: ${value}.`);
|
|
55
|
+
}
|
|
56
|
+
return value === 'sha256sum' ? sha256sumCodec : wordpressCodec;
|
|
57
|
+
}
|
|
58
|
+
if (!value ||
|
|
59
|
+
typeof value.encode !== 'function' ||
|
|
60
|
+
typeof value.decode !== 'function' ||
|
|
61
|
+
!value.id ||
|
|
62
|
+
!value.filename) {
|
|
63
|
+
throw new ValidationError('A checksums codec needs id, filename, encode and decode.');
|
|
64
|
+
}
|
|
65
|
+
return value;
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Relative artifact paths cannot escape their destination or use Windows paths.
|
|
69
|
+
* @param file
|
|
70
|
+
*/
|
|
71
|
+
export function checkChecksumsFile(file) {
|
|
72
|
+
if (!file ||
|
|
73
|
+
/[\\\x00-\x1f:]/.test(file) ||
|
|
74
|
+
file
|
|
75
|
+
.split('/')
|
|
76
|
+
.some((part) => !part || part === '.' || part === '..')) {
|
|
77
|
+
throw new ValidationError('--checksums-file must be a relative file path with forward slashes and no dot segments.');
|
|
78
|
+
}
|
|
79
|
+
return file;
|
|
80
|
+
}
|
|
81
|
+
export async function readIntegrity(path, codec) {
|
|
82
|
+
const selected = resolveManifestCodec(codec ??
|
|
83
|
+
(basename(path) === 'SHA256SUMS'
|
|
84
|
+
? 'sha256sum'
|
|
85
|
+
: 'wordpress-json'));
|
|
86
|
+
const manifest = selected === wordpressCodec
|
|
87
|
+
? await readChecksums(path)
|
|
88
|
+
: await selected.decode(await readFile(path));
|
|
89
|
+
return parseChecksums(JSON.stringify(manifest), path);
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* Encode once and ensure the format preserves the selected payload hashes.
|
|
93
|
+
* @param codec
|
|
94
|
+
* @param manifest
|
|
95
|
+
*/
|
|
96
|
+
export async function encodeIntegrity(codec, manifest) {
|
|
97
|
+
const bytes = Buffer.from(await codec.encode(manifest));
|
|
98
|
+
const decoded = parseChecksums(JSON.stringify(await codec.decode(bytes)));
|
|
99
|
+
const expected = Object.keys(manifest.files);
|
|
100
|
+
const actual = Object.keys(decoded.files);
|
|
101
|
+
const preserved = expected.length === actual.length &&
|
|
102
|
+
actual.every((path) => {
|
|
103
|
+
const hashes = decoded.files[path];
|
|
104
|
+
return (Boolean(manifest.files[path]) &&
|
|
105
|
+
Object.entries(hashes ?? {}).every(([algorithm, digest]) => digest ===
|
|
106
|
+
manifest.files[path]?.[algorithm]));
|
|
107
|
+
});
|
|
108
|
+
if (!preserved ||
|
|
109
|
+
['plugin', 'theme', 'version'].some((key) => decoded[key] !== undefined &&
|
|
110
|
+
decoded[key] !==
|
|
111
|
+
manifest[key])) {
|
|
112
|
+
throw new ValidationError('The checksums codec must preserve payload paths, digests and any represented package identity.');
|
|
113
|
+
}
|
|
114
|
+
return bytes;
|
|
115
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Validate generated file destinations before writing release artifacts.
|
|
3
|
+
*
|
|
4
|
+
* @param paths Output file paths.
|
|
5
|
+
* @param force Whether existing regular files can be overwritten.
|
|
6
|
+
*/
|
|
7
|
+
export declare function checkOutputFiles(paths: string[], force: boolean): void;
|