@wpelevator/wp-release 0.13.0 → 0.14.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 +67 -20
- package/dist/changeset-config.d.ts +8 -6
- package/dist/changeset-config.js +7 -5
- package/dist/changeset-packages.d.ts +12 -2
- package/dist/changeset-packages.js +13 -1
- package/dist/changeset-release.d.ts +5 -2
- package/dist/changeset-release.js +12 -6
- package/dist/cli.js +33 -9
- package/dist/commands/args.d.ts +0 -21
- package/dist/commands/args.js +1 -35
- package/dist/commands/changeset-add.d.ts +1 -1
- package/dist/commands/changeset-add.js +3 -2
- package/dist/commands/changeset-status.d.ts +1 -1
- package/dist/commands/changeset-status.js +2 -1
- package/dist/commands/changeset-tag.d.ts +1 -1
- package/dist/commands/changeset-tag.js +4 -2
- package/dist/commands/changeset-version.d.ts +1 -1
- package/dist/commands/changeset-version.js +2 -1
- package/dist/commands/checksums.js +2 -2
- package/dist/commands/command.d.ts +2 -0
- package/dist/commands/deploy-git.d.ts +1 -1
- package/dist/commands/deploy-git.js +2 -1
- package/dist/commands/deploy-svn.d.ts +1 -1
- package/dist/commands/deploy-svn.js +2 -1
- package/dist/commands/keygen.d.ts +3 -0
- package/dist/commands/keygen.js +6 -1
- package/dist/commands/package-deploys.d.ts +8 -0
- package/dist/commands/package-deploys.js +5 -0
- package/dist/commands/sign.d.ts +19 -1
- package/dist/commands/sign.js +47 -12
- package/dist/commands/verify.d.ts +3 -0
- package/dist/commands/verify.js +43 -24
- package/dist/commands/zip.d.ts +12 -8
- package/dist/commands/zip.js +135 -78
- package/dist/ed25519.d.ts +25 -0
- package/dist/ed25519.js +44 -0
- package/dist/file-selection.d.ts +21 -0
- package/dist/file-selection.js +35 -0
- package/dist/git.d.ts +17 -0
- package/dist/git.js +23 -0
- package/dist/index.d.ts +9 -4
- package/dist/index.js +6 -3
- package/dist/minisign.d.ts +41 -0
- package/dist/minisign.js +128 -0
- package/dist/package-deploy.d.ts +28 -0
- package/dist/package-deploy.js +41 -6
- package/dist/package-zip.d.ts +52 -0
- package/dist/package-zip.js +78 -0
- package/dist/release-repository.d.ts +4 -3
- package/dist/release-repository.js +5 -4
- package/dist/signing.d.ts +64 -9
- package/dist/signing.js +113 -40
- package/dist/ssh-signature.d.ts +43 -0
- package/dist/ssh-signature.js +156 -0
- package/dist/summary.d.ts +3 -3
- package/dist/summary.js +2 -2
- package/dist/tag-format.d.ts +21 -12
- package/dist/tag-format.js +25 -39
- package/dist/version-files.d.ts +6 -7
- package/dist/version-files.js +10 -11
- package/package.json +2 -2
package/dist/minisign.js
ADDED
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import { createHash, sign, verify } from 'node:crypto';
|
|
2
|
+
import { readFile } from 'node:fs/promises';
|
|
3
|
+
import { basename } from 'node:path';
|
|
4
|
+
import { PUBLIC_KEY_BYTES, digestFile, getRawPublicKey } from "./ed25519.js";
|
|
5
|
+
export const MINISIGN_EXTENSION = '.minisig';
|
|
6
|
+
// Algorithm of a public key, and of a signature of the BLAKE2b-512 digest.
|
|
7
|
+
const KEY_ALGORITHM = 'Ed';
|
|
8
|
+
const HASHED_ALGORITHM = 'ED';
|
|
9
|
+
const KEY_ID_BYTES = 8;
|
|
10
|
+
const SIGNATURE_BYTES = 64;
|
|
11
|
+
const UNTRUSTED_PREFIX = 'untrusted comment: ';
|
|
12
|
+
const TRUSTED_PREFIX = 'trusted comment: ';
|
|
13
|
+
/**
|
|
14
|
+
* Get the minisign key ID of a public key. minisign picks a random ID when
|
|
15
|
+
* it generates a key, so wp-release derives one from the public key instead,
|
|
16
|
+
* which keeps it the same for every signature of the key.
|
|
17
|
+
*
|
|
18
|
+
* @param publicKey Public key bytes.
|
|
19
|
+
* @return 8-byte key ID.
|
|
20
|
+
*/
|
|
21
|
+
function getKeyId(publicKey) {
|
|
22
|
+
return createHash('blake2b512')
|
|
23
|
+
.update(publicKey)
|
|
24
|
+
.digest()
|
|
25
|
+
.subarray(0, KEY_ID_BYTES);
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Format a key ID like minisign prints it: the hex of the little-endian
|
|
29
|
+
* 64-bit number.
|
|
30
|
+
*
|
|
31
|
+
* @param keyId Key ID bytes.
|
|
32
|
+
* @return Uppercase hex key ID.
|
|
33
|
+
*/
|
|
34
|
+
function formatKeyId(keyId) {
|
|
35
|
+
return Buffer.from(keyId).reverse().toString('hex').toUpperCase();
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Get the minisign public key of a key, the base64 line that `minisign -P`
|
|
39
|
+
* takes and that the second line of a minisign .pub file holds.
|
|
40
|
+
*
|
|
41
|
+
* @param key Private or public key.
|
|
42
|
+
* @return Base64 minisign public key.
|
|
43
|
+
*/
|
|
44
|
+
export function getMinisignPublicKey(key) {
|
|
45
|
+
const publicKey = getRawPublicKey(key);
|
|
46
|
+
return Buffer.concat([
|
|
47
|
+
Buffer.from(KEY_ALGORITHM),
|
|
48
|
+
getKeyId(publicKey),
|
|
49
|
+
publicKey,
|
|
50
|
+
]).toString('base64');
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Get the raw public key from a minisign public key, the base64 line or the
|
|
54
|
+
* whole two-line .pub file.
|
|
55
|
+
*
|
|
56
|
+
* @param encoded Minisign public key.
|
|
57
|
+
* @return Public key bytes, or null when it isn't a minisign public key.
|
|
58
|
+
*/
|
|
59
|
+
export function parseMinisignPublicKey(encoded) {
|
|
60
|
+
const line = encoded
|
|
61
|
+
.trim()
|
|
62
|
+
.split('\n')
|
|
63
|
+
.map((text) => text.trim())
|
|
64
|
+
.filter((text) => text && !text.startsWith(UNTRUSTED_PREFIX));
|
|
65
|
+
const bytes = Buffer.from(line.join(''), 'base64');
|
|
66
|
+
const isMinisign = 1 === line.length &&
|
|
67
|
+
2 + KEY_ID_BYTES + PUBLIC_KEY_BYTES === bytes.length &&
|
|
68
|
+
KEY_ALGORITHM === bytes.subarray(0, 2).toString();
|
|
69
|
+
return isMinisign ? bytes.subarray(2 + KEY_ID_BYTES) : null;
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Create a minisign signature of a file: an Ed25519 signature of its
|
|
73
|
+
* BLAKE2b-512 digest, and a global signature that also covers the trusted
|
|
74
|
+
* comment with the signing time and file name.
|
|
75
|
+
*
|
|
76
|
+
* @param path File path.
|
|
77
|
+
* @param privateKey Private key.
|
|
78
|
+
* @return Contents of the .minisig file.
|
|
79
|
+
*/
|
|
80
|
+
export async function createMinisignSignature(path, privateKey) {
|
|
81
|
+
const keyId = getKeyId(getRawPublicKey(privateKey));
|
|
82
|
+
const signature = sign(null, await digestFile(path, 'blake2b512'), privateKey);
|
|
83
|
+
const name = basename(path).replace(/[\r\n]/g, ' ');
|
|
84
|
+
const trustedComment = `timestamp:${Math.floor(Date.now() / 1000)}\tfile:${name}\thashed`;
|
|
85
|
+
const globalSignature = sign(null, Buffer.concat([signature, Buffer.from(trustedComment)]), privateKey);
|
|
86
|
+
return [
|
|
87
|
+
`${UNTRUSTED_PREFIX}signature from wp-release key ${formatKeyId(keyId)}`,
|
|
88
|
+
Buffer.concat([
|
|
89
|
+
Buffer.from(HASHED_ALGORITHM),
|
|
90
|
+
keyId,
|
|
91
|
+
signature,
|
|
92
|
+
]).toString('base64'),
|
|
93
|
+
`${TRUSTED_PREFIX}${trustedComment}`,
|
|
94
|
+
globalSignature.toString('base64'),
|
|
95
|
+
'',
|
|
96
|
+
].join('\n');
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* Check a file against a minisign signature file, including the global
|
|
100
|
+
* signature of the trusted comment. Both the BLAKE2b-512 prehashed (ED)
|
|
101
|
+
* and the legacy (Ed) signatures of minisign are accepted. The key ID isn't
|
|
102
|
+
* compared, since it only tells minisign which key to use, so signatures of
|
|
103
|
+
* keys that minisign generated with a random ID are accepted too.
|
|
104
|
+
*
|
|
105
|
+
* @param path File path.
|
|
106
|
+
* @param signaturePath Signature file path.
|
|
107
|
+
* @param publicKey Public key.
|
|
108
|
+
* @return Whether the signature matches.
|
|
109
|
+
*/
|
|
110
|
+
export async function verifyMinisignSignature(path, signaturePath, publicKey) {
|
|
111
|
+
const [, encoded = '', trusted = '', encodedGlobal = ''] = (await readFile(signaturePath, 'utf8'))
|
|
112
|
+
.split('\n')
|
|
113
|
+
.map((line) => line.trim());
|
|
114
|
+
const bytes = Buffer.from(encoded, 'base64');
|
|
115
|
+
const algorithm = bytes.subarray(0, 2).toString();
|
|
116
|
+
const signature = bytes.subarray(2 + KEY_ID_BYTES);
|
|
117
|
+
const isValidShape = 2 + KEY_ID_BYTES + SIGNATURE_BYTES === bytes.length &&
|
|
118
|
+
[HASHED_ALGORITHM, KEY_ALGORITHM].includes(algorithm) &&
|
|
119
|
+
trusted.startsWith(TRUSTED_PREFIX);
|
|
120
|
+
return (isValidShape &&
|
|
121
|
+
verify(null, HASHED_ALGORITHM === algorithm
|
|
122
|
+
? await digestFile(path, 'blake2b512')
|
|
123
|
+
: await readFile(path), publicKey, signature) &&
|
|
124
|
+
verify(null, Buffer.concat([
|
|
125
|
+
signature,
|
|
126
|
+
Buffer.from(trusted.slice(TRUSTED_PREFIX.length)),
|
|
127
|
+
]), publicKey, Buffer.from(encodedGlobal, 'base64')));
|
|
128
|
+
}
|
package/dist/package-deploy.d.ts
CHANGED
|
@@ -7,6 +7,8 @@ export interface PackageDeployOptions {
|
|
|
7
7
|
cwd: string;
|
|
8
8
|
/** Config file, instead of the conventional one at the root. */
|
|
9
9
|
configFile?: string;
|
|
10
|
+
/** Tag format that replaces the tagFormat of the config. */
|
|
11
|
+
tagFormat?: string;
|
|
10
12
|
/**
|
|
11
13
|
* Ref to deploy: a package release tag, like refs/tags/plugins/alpha/1.2.0,
|
|
12
14
|
* or a branch, like refs/heads/main. A name without refs/ is a release tag
|
|
@@ -35,6 +37,10 @@ export interface PackageDeploy<Input> {
|
|
|
35
37
|
target: string;
|
|
36
38
|
/** Tag or branch that the deploy writes in the target. */
|
|
37
39
|
targetRef: string;
|
|
40
|
+
/** Host of a remote target repository, like github.com. */
|
|
41
|
+
targetHost?: string;
|
|
42
|
+
/** Path of a remote target repository on its host, like owner/repo. */
|
|
43
|
+
targetPath?: string;
|
|
38
44
|
/** Package, target and ref, with credentials redacted, for logs. */
|
|
39
45
|
label: string;
|
|
40
46
|
/** Options of the deploy. */
|
|
@@ -79,6 +85,24 @@ export declare function getDeployPackages(options: {
|
|
|
79
85
|
cwd: string;
|
|
80
86
|
configFile?: string;
|
|
81
87
|
}): Promise<PackageDeployFiles[]>;
|
|
88
|
+
/**
|
|
89
|
+
* Get a configured package with the files that its releases ship, from the
|
|
90
|
+
* files config of its package group, which its deploys use too, for example
|
|
91
|
+
* to build its ZIP with the same files. Any package of the config works, with
|
|
92
|
+
* or without a version or deploy targets.
|
|
93
|
+
*
|
|
94
|
+
* @param options Repository root, config file and package.
|
|
95
|
+
* @param options.cwd Repository root with the wp-release config.
|
|
96
|
+
* @param options.configFile Config file, instead of the conventional one at the root.
|
|
97
|
+
* @param options.package Package identifier or path under the configured root.
|
|
98
|
+
* @return The package and its file selection.
|
|
99
|
+
* @throws {ValidationError} Without a config, for an invalid one or an unknown package.
|
|
100
|
+
*/
|
|
101
|
+
export declare function getPackageFiles(options: {
|
|
102
|
+
cwd: string;
|
|
103
|
+
configFile?: string;
|
|
104
|
+
package: string;
|
|
105
|
+
}): Promise<PackageDeployFiles>;
|
|
82
106
|
/** Ignore file and patterns that select the files of a package. */
|
|
83
107
|
export interface PackageFileSelection {
|
|
84
108
|
/** Absolute ignore file path, or false to not read one. */
|
|
@@ -93,6 +117,10 @@ export interface PackageDeployPlanItem extends PackageDeployRelease {
|
|
|
93
117
|
targets: Array<{
|
|
94
118
|
target: string;
|
|
95
119
|
ref: string;
|
|
120
|
+
/** Host of a remote target repository. */
|
|
121
|
+
host?: string;
|
|
122
|
+
/** Path of a remote target repository on its host. */
|
|
123
|
+
path?: string;
|
|
96
124
|
}>;
|
|
97
125
|
}
|
|
98
126
|
/**
|
package/dist/package-deploy.js
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import { realpath } from 'node:fs/promises';
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
import { loadChangesetConfig, toTargetList } from "./changeset-config.js";
|
|
4
|
-
import { discoverDeployPackages, findPackage } from "./changeset-packages.js";
|
|
4
|
+
import { discoverDeployPackages, discoverConfiguredPackages, findPackage, } from "./changeset-packages.js";
|
|
5
5
|
import { ValidationError } from "./errors.js";
|
|
6
|
-
import { redactUrls } from "./git.js";
|
|
6
|
+
import { parseRepositoryUrl, redactUrls } from "./git.js";
|
|
7
7
|
import { ReleaseRepository } from "./release-repository.js";
|
|
8
|
-
import { formatTag } from "./tag-format.js";
|
|
8
|
+
import { DEFAULT_TAG_FORMAT, formatTag, formatTemplate } from "./tag-format.js";
|
|
9
9
|
import { isPrerelease, isVersion } from "./versions.js";
|
|
10
10
|
/**
|
|
11
11
|
* Get the Git deploys of the configured packages that a ref releases: every
|
|
@@ -65,10 +65,35 @@ export async function getDeployPackages(options) {
|
|
|
65
65
|
files: getFileSelection(pkg, config),
|
|
66
66
|
}));
|
|
67
67
|
}
|
|
68
|
+
/**
|
|
69
|
+
* Get a configured package with the files that its releases ship, from the
|
|
70
|
+
* files config of its package group, which its deploys use too, for example
|
|
71
|
+
* to build its ZIP with the same files. Any package of the config works, with
|
|
72
|
+
* or without a version or deploy targets.
|
|
73
|
+
*
|
|
74
|
+
* @param options Repository root, config file and package.
|
|
75
|
+
* @param options.cwd Repository root with the wp-release config.
|
|
76
|
+
* @param options.configFile Config file, instead of the conventional one at the root.
|
|
77
|
+
* @param options.package Package identifier or path under the configured root.
|
|
78
|
+
* @return The package and its file selection.
|
|
79
|
+
* @throws {ValidationError} Without a config, for an invalid one or an unknown package.
|
|
80
|
+
*/
|
|
81
|
+
export async function getPackageFiles(options) {
|
|
82
|
+
const config = await requireConfig(options);
|
|
83
|
+
const pkg = findPackage(await discoverConfiguredPackages(new ReleaseRepository({ cwd: options.cwd }), config), options.package);
|
|
84
|
+
if (!pkg) {
|
|
85
|
+
throw new ValidationError(`Unknown package "${options.package}".`);
|
|
86
|
+
}
|
|
87
|
+
return {
|
|
88
|
+
...toDeployPackage(pkg),
|
|
89
|
+
files: getFileSelection(pkg, config),
|
|
90
|
+
};
|
|
91
|
+
}
|
|
68
92
|
async function requireConfig(options) {
|
|
69
93
|
const loaded = await loadChangesetConfig({
|
|
70
94
|
cwd: options.cwd,
|
|
71
95
|
...(options.configFile ? { configFile: options.configFile } : {}),
|
|
96
|
+
...(options.tagFormat ? { tagFormat: options.tagFormat } : {}),
|
|
72
97
|
});
|
|
73
98
|
if (!loaded) {
|
|
74
99
|
throw new ValidationError('No wp-release config found. Add wp-release.config.js or pass --config <path>.');
|
|
@@ -112,7 +137,7 @@ function selectPackages(packages, identifiers) {
|
|
|
112
137
|
* @return Releases.
|
|
113
138
|
*/
|
|
114
139
|
function getReleases(packages, config, ref) {
|
|
115
|
-
const tagFormat = config.tagFormat ??
|
|
140
|
+
const tagFormat = config.tagFormat ?? DEFAULT_TAG_FORMAT;
|
|
116
141
|
const tag = ref.startsWith('refs/tags/')
|
|
117
142
|
? ref.slice('refs/tags/'.length)
|
|
118
143
|
: ref;
|
|
@@ -156,12 +181,16 @@ async function getGitDeploy(target, release, config, root, options) {
|
|
|
156
181
|
.relative(root, await realpath(pkg.directory))
|
|
157
182
|
.split(path.sep)
|
|
158
183
|
.join('/');
|
|
184
|
+
const location = repository ? parseRepositoryUrl(repository) : undefined;
|
|
159
185
|
return repository
|
|
160
186
|
? {
|
|
161
187
|
package: toDeployPackage(pkg),
|
|
162
188
|
release: toRelease(release),
|
|
163
189
|
target: redactUrls(repository),
|
|
164
190
|
targetRef: ref,
|
|
191
|
+
...(location
|
|
192
|
+
? { targetHost: location.host, targetPath: location.path }
|
|
193
|
+
: {}),
|
|
165
194
|
label: `${pkg.name} to ${redactUrls(repository)} ${ref}`,
|
|
166
195
|
input: {
|
|
167
196
|
cwd: options.cwd,
|
|
@@ -181,7 +210,10 @@ async function getSvnDeploy(target, release, config, options) {
|
|
|
181
210
|
const pkg = release.package;
|
|
182
211
|
const version = release.version ?? '';
|
|
183
212
|
const stable = !isPrerelease(version);
|
|
184
|
-
|
|
213
|
+
// The WordPress.org slug is the name of the package directory by default.
|
|
214
|
+
const slug = target.slug
|
|
215
|
+
? await resolveTemplate(target.slug, release, version)
|
|
216
|
+
: path.basename(pkg.directory);
|
|
185
217
|
const message = target.message
|
|
186
218
|
? await resolveTemplate(target.message, release, version)
|
|
187
219
|
: '';
|
|
@@ -230,7 +262,7 @@ async function resolveTemplate(template, release, ref) {
|
|
|
230
262
|
ref,
|
|
231
263
|
revision: release.revision,
|
|
232
264
|
})
|
|
233
|
-
:
|
|
265
|
+
: formatTemplate(template, release.package, release.version ?? '')
|
|
234
266
|
.replaceAll('{branch}', release.branch ?? '')
|
|
235
267
|
.replaceAll('{ref}', ref)
|
|
236
268
|
.replaceAll('{revision}', release.revision);
|
|
@@ -282,6 +314,9 @@ export function getDeployPlan(deploys) {
|
|
|
282
314
|
targets: items.map((item) => ({
|
|
283
315
|
target: item.target,
|
|
284
316
|
ref: item.targetRef,
|
|
317
|
+
...(item.targetHost
|
|
318
|
+
? { host: item.targetHost, path: item.targetPath }
|
|
319
|
+
: {}),
|
|
285
320
|
})),
|
|
286
321
|
};
|
|
287
322
|
});
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import type { PackageType } from './checksums.ts';
|
|
2
|
+
import type { FileSelection } from './file-selection.ts';
|
|
3
|
+
export interface ZipPackageInput {
|
|
4
|
+
/** Directory to archive. */
|
|
5
|
+
sourceDir: string;
|
|
6
|
+
/** File selection from resolveFileSelection(). */
|
|
7
|
+
selection: FileSelection;
|
|
8
|
+
/** Archive path. */
|
|
9
|
+
zipPath: string;
|
|
10
|
+
/** Folder inside the archive, or null for the root. */
|
|
11
|
+
rootFolder: string | null;
|
|
12
|
+
/** Plugin or theme slug, for the checksums manifest. */
|
|
13
|
+
slug: string;
|
|
14
|
+
/** Version for the checksums manifest. */
|
|
15
|
+
version?: string | null;
|
|
16
|
+
/** Package type for the checksums manifest. */
|
|
17
|
+
type?: PackageType;
|
|
18
|
+
/** Also write the checksums manifest next to the archive. */
|
|
19
|
+
checksums?: boolean;
|
|
20
|
+
/** Overwrite existing output files. */
|
|
21
|
+
force?: boolean;
|
|
22
|
+
}
|
|
23
|
+
/** Result of zipPackage(). */
|
|
24
|
+
export interface ZipPackageResult {
|
|
25
|
+
/** Absolute path of the ZIP. */
|
|
26
|
+
zipPath: string;
|
|
27
|
+
/** Absolute path of the checksums manifest, set with checksums. */
|
|
28
|
+
checksumsPath?: string;
|
|
29
|
+
/** Number of archived files. */
|
|
30
|
+
files: number;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Get the checksums manifest path next to an archive, with .checksums.json
|
|
34
|
+
* instead of .zip.
|
|
35
|
+
*
|
|
36
|
+
* @param zipPath Archive path.
|
|
37
|
+
* @return Manifest path.
|
|
38
|
+
*/
|
|
39
|
+
export declare function getChecksumsPath(zipPath: string): string;
|
|
40
|
+
/**
|
|
41
|
+
* Write a ZIP of the selected files of a plugin, theme or package, and
|
|
42
|
+
* optionally the checksums manifest of the same files next to it. The zip
|
|
43
|
+
* command, with a directory or a configured package release, and the
|
|
44
|
+
* snapshot builds of a monorepo all use it, so that every ZIP of a package
|
|
45
|
+
* selects its files the same way. Output files inside the source directory,
|
|
46
|
+
* and their signatures, are never archived.
|
|
47
|
+
*
|
|
48
|
+
* @param input Files, archive path and manifest options.
|
|
49
|
+
* @return Paths of the written files and the number of archived files.
|
|
50
|
+
* @throws {ValidationError} Without selected files, or when an output file exists without force.
|
|
51
|
+
*/
|
|
52
|
+
export declare function zipPackage(input: ZipPackageInput): Promise<ZipPackageResult>;
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { existsSync } from 'node:fs';
|
|
2
|
+
import { CHECKSUMS_EXTENSION, createChecksums, writeChecksums, } from "./checksums.js";
|
|
3
|
+
import { ValidationError } from "./errors.js";
|
|
4
|
+
import { ignoreOwnFile, ignoreOwnSignature, loadFileRules, selectFiles, } from "./file-selection.js";
|
|
5
|
+
import { createZip } from "./zip.js";
|
|
6
|
+
const ZIP_EXTENSION = '.zip';
|
|
7
|
+
/**
|
|
8
|
+
* Get the checksums manifest path next to an archive, with .checksums.json
|
|
9
|
+
* instead of .zip.
|
|
10
|
+
*
|
|
11
|
+
* @param zipPath Archive path.
|
|
12
|
+
* @return Manifest path.
|
|
13
|
+
*/
|
|
14
|
+
export function getChecksumsPath(zipPath) {
|
|
15
|
+
const name = zipPath.endsWith(ZIP_EXTENSION)
|
|
16
|
+
? zipPath.slice(0, -ZIP_EXTENSION.length)
|
|
17
|
+
: zipPath;
|
|
18
|
+
return name + CHECKSUMS_EXTENSION;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Fail before writing anything when an output file exists.
|
|
22
|
+
*
|
|
23
|
+
* @param paths Output paths.
|
|
24
|
+
* @param force Whether existing files are overwritten.
|
|
25
|
+
* @throws {ValidationError} If a file exists and force isn't set.
|
|
26
|
+
*/
|
|
27
|
+
function checkOverwrite(paths, force) {
|
|
28
|
+
const existing = paths.filter((path) => !force && existsSync(path));
|
|
29
|
+
if (existing.length) {
|
|
30
|
+
throw new ValidationError(`${existing.join(' and ')} already exists, pass --force to overwrite it.`);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Write a ZIP of the selected files of a plugin, theme or package, and
|
|
35
|
+
* optionally the checksums manifest of the same files next to it. The zip
|
|
36
|
+
* command, with a directory or a configured package release, and the
|
|
37
|
+
* snapshot builds of a monorepo all use it, so that every ZIP of a package
|
|
38
|
+
* selects its files the same way. Output files inside the source directory,
|
|
39
|
+
* and their signatures, are never archived.
|
|
40
|
+
*
|
|
41
|
+
* @param input Files, archive path and manifest options.
|
|
42
|
+
* @return Paths of the written files and the number of archived files.
|
|
43
|
+
* @throws {ValidationError} Without selected files, or when an output file exists without force.
|
|
44
|
+
*/
|
|
45
|
+
export async function zipPackage(input) {
|
|
46
|
+
const { sourceDir, zipPath, force = false } = input;
|
|
47
|
+
const checksumsPath = input.checksums ? getChecksumsPath(zipPath) : null;
|
|
48
|
+
const outputs = [zipPath, checksumsPath].filter((path) => null !== path);
|
|
49
|
+
const rules = await loadFileRules(input.selection);
|
|
50
|
+
outputs.forEach((path) => ignoreOwnSignature(ignoreOwnFile(rules, sourceDir, path), sourceDir, path));
|
|
51
|
+
const files = await selectFiles(sourceDir, rules);
|
|
52
|
+
if (!files.length) {
|
|
53
|
+
throw new ValidationError(`No files to archive in ${sourceDir}.`);
|
|
54
|
+
}
|
|
55
|
+
checkOverwrite(outputs, force);
|
|
56
|
+
await createZip({
|
|
57
|
+
sourceDir,
|
|
58
|
+
files,
|
|
59
|
+
zipPath,
|
|
60
|
+
rootFolder: input.rootFolder,
|
|
61
|
+
force,
|
|
62
|
+
});
|
|
63
|
+
if (checksumsPath) {
|
|
64
|
+
const manifest = await createChecksums({
|
|
65
|
+
sourceDir,
|
|
66
|
+
rules,
|
|
67
|
+
slug: input.slug,
|
|
68
|
+
version: input.version ?? null,
|
|
69
|
+
type: input.type,
|
|
70
|
+
});
|
|
71
|
+
await writeChecksums(checksumsPath, manifest, force);
|
|
72
|
+
}
|
|
73
|
+
return {
|
|
74
|
+
zipPath,
|
|
75
|
+
...(checksumsPath ? { checksumsPath } : {}),
|
|
76
|
+
files: files.length,
|
|
77
|
+
};
|
|
78
|
+
}
|
|
@@ -66,11 +66,12 @@ export declare class ReleaseRepository {
|
|
|
66
66
|
*/
|
|
67
67
|
createTag(tag: string): Promise<void>;
|
|
68
68
|
/**
|
|
69
|
-
* Push a tag to
|
|
69
|
+
* Push a tag to a remote.
|
|
70
70
|
*
|
|
71
|
-
* @param tag
|
|
71
|
+
* @param tag Tag name.
|
|
72
|
+
* @param remote Remote name.
|
|
72
73
|
*/
|
|
73
|
-
pushTag(tag: string): Promise<void>;
|
|
74
|
+
pushTag(tag: string, remote: string): Promise<void>;
|
|
74
75
|
/**
|
|
75
76
|
* Find the commit that added a file.
|
|
76
77
|
*
|
|
@@ -78,12 +78,13 @@ export class ReleaseRepository {
|
|
|
78
78
|
await this.run(['tag', tag]);
|
|
79
79
|
}
|
|
80
80
|
/**
|
|
81
|
-
* Push a tag to
|
|
81
|
+
* Push a tag to a remote.
|
|
82
82
|
*
|
|
83
|
-
* @param tag
|
|
83
|
+
* @param tag Tag name.
|
|
84
|
+
* @param remote Remote name.
|
|
84
85
|
*/
|
|
85
|
-
async pushTag(tag) {
|
|
86
|
-
await this.run(['push',
|
|
86
|
+
async pushTag(tag, remote) {
|
|
87
|
+
await this.run(['push', remote, tag]);
|
|
87
88
|
}
|
|
88
89
|
/**
|
|
89
90
|
* Find the commit that added a file.
|
package/dist/signing.d.ts
CHANGED
|
@@ -1,4 +1,8 @@
|
|
|
1
1
|
import type { KeyObject } from 'node:crypto';
|
|
2
|
+
/**
|
|
3
|
+
* Extension of the signature files in the format that WordPress core's
|
|
4
|
+
* verify_file_signature() checks.
|
|
5
|
+
*/
|
|
2
6
|
export declare const SIGNATURE_EXTENSION = ".sig";
|
|
3
7
|
/**
|
|
4
8
|
* Environment variable with the base64 Ed25519 signing key.
|
|
@@ -7,11 +11,12 @@ export declare const SIGNING_KEY_ENV = "WP_RELEASE_SIGNING_KEY";
|
|
|
7
11
|
/**
|
|
8
12
|
* Generate an Ed25519 key pair in the base64 formats used by wp-release.
|
|
9
13
|
*
|
|
10
|
-
* @return Private seed for WP_RELEASE_SIGNING_KEY and public
|
|
14
|
+
* @return Private seed for WP_RELEASE_SIGNING_KEY, the base64 public key and the public key of each signature format.
|
|
11
15
|
*/
|
|
12
16
|
export declare function generateSigningKeyPair(): {
|
|
13
17
|
privateKey: string;
|
|
14
18
|
publicKey: string;
|
|
19
|
+
publicKeys: Record<SignatureFormat, string>;
|
|
15
20
|
};
|
|
16
21
|
/**
|
|
17
22
|
* Get an Ed25519 private key from the base64 format that sodium and the
|
|
@@ -24,11 +29,13 @@ export declare function generateSigningKeyPair(): {
|
|
|
24
29
|
*/
|
|
25
30
|
export declare function parseSigningKey(encoded: string): KeyObject;
|
|
26
31
|
/**
|
|
27
|
-
* Get an Ed25519 public key from
|
|
32
|
+
* Get an Ed25519 public key from the base64 32-byte key, a minisign public
|
|
33
|
+
* key or an OpenSSH ssh-ed25519 public key. They're forms of the same key,
|
|
34
|
+
* so any of them checks the signatures of every format.
|
|
28
35
|
*
|
|
29
|
-
* @param encoded
|
|
36
|
+
* @param encoded Public key.
|
|
30
37
|
* @return Public key.
|
|
31
|
-
* @throws {ValidationError} If the key
|
|
38
|
+
* @throws {ValidationError} If the key isn't in one of the forms.
|
|
32
39
|
*/
|
|
33
40
|
export declare function parsePublicKey(encoded: string): KeyObject;
|
|
34
41
|
/**
|
|
@@ -55,24 +62,72 @@ export declare function getFileDigest(path: string): Promise<Buffer>;
|
|
|
55
62
|
* @return Base64 signature.
|
|
56
63
|
*/
|
|
57
64
|
export declare function signFile(path: string, privateKey: KeyObject): Promise<string>;
|
|
65
|
+
/** Names of the signature formats. */
|
|
66
|
+
export type SignatureFormat = 'wordpress' | 'minisign' | 'ssh';
|
|
67
|
+
/** How a signature format writes and checks signature files. */
|
|
68
|
+
export interface SignatureFormatHandler {
|
|
69
|
+
/** Extension of the signature file, appended to the signed file name. */
|
|
70
|
+
extension: string;
|
|
71
|
+
/** Get the contents of the signature file of a file. */
|
|
72
|
+
create: (path: string, privateKey: KeyObject) => Promise<string>;
|
|
73
|
+
/** Check a file against its signature file. */
|
|
74
|
+
verify: (path: string, signaturePath: string, publicKey: KeyObject) => Promise<boolean>;
|
|
75
|
+
/** Get the public key in the form that the verifiers of the format take. */
|
|
76
|
+
getPublicKey: (key: KeyObject) => string;
|
|
77
|
+
}
|
|
78
|
+
export declare const DEFAULT_SIGNATURE_FORMAT: SignatureFormat;
|
|
79
|
+
/**
|
|
80
|
+
* Signature formats, all Ed25519 signatures by the same key:
|
|
81
|
+
*
|
|
82
|
+
* - wordpress: base64 signature of the SHA-384 digest in <file>.sig, which
|
|
83
|
+
* WordPress core's verify_file_signature() checks.
|
|
84
|
+
* - minisign: <file>.minisig, which `minisign -V` checks.
|
|
85
|
+
* - ssh: armored SSH signature in <file>.sshsig, which `ssh-keygen -Y verify`
|
|
86
|
+
* checks with the "file" namespace.
|
|
87
|
+
*/
|
|
88
|
+
export declare const SIGNATURE_FORMATS: Record<SignatureFormat, SignatureFormatHandler>;
|
|
89
|
+
/**
|
|
90
|
+
* Check that a value is the name of a signature format.
|
|
91
|
+
*
|
|
92
|
+
* @param value Value to check.
|
|
93
|
+
* @return Whether it's a signature format.
|
|
94
|
+
*/
|
|
95
|
+
export declare function isSignatureFormat(value: string): value is SignatureFormat;
|
|
96
|
+
/**
|
|
97
|
+
* Get the signature file path of a file.
|
|
98
|
+
*
|
|
99
|
+
* @param path File path.
|
|
100
|
+
* @param format Signature format.
|
|
101
|
+
* @return Signature file path.
|
|
102
|
+
*/
|
|
103
|
+
export declare function getSignaturePath(path: string, format?: SignatureFormat): string;
|
|
104
|
+
/**
|
|
105
|
+
* Get the public key of a key in the form of each signature format.
|
|
106
|
+
*
|
|
107
|
+
* @param key Private or public key.
|
|
108
|
+
* @return Public keys by format.
|
|
109
|
+
*/
|
|
110
|
+
export declare function getPublicKeys(key: KeyObject): Record<SignatureFormat, string>;
|
|
58
111
|
/**
|
|
59
|
-
* Write the signature of a file to a
|
|
112
|
+
* Write the signature of a file to a signature file next to it.
|
|
60
113
|
*
|
|
61
114
|
* @param path File path.
|
|
62
115
|
* @param privateKey Private key from parseSigningKey().
|
|
63
116
|
* @param force Whether to overwrite an existing signature file.
|
|
117
|
+
* @param format Signature format.
|
|
64
118
|
* @return Signature file path.
|
|
65
119
|
* @throws {ValidationError} If the signature file exists and force isn't set.
|
|
66
120
|
*/
|
|
67
|
-
export declare function writeSignature(path: string, privateKey: KeyObject, force?: boolean): Promise<string>;
|
|
121
|
+
export declare function writeSignature(path: string, privateKey: KeyObject, force?: boolean, format?: SignatureFormat): Promise<string>;
|
|
68
122
|
/**
|
|
69
|
-
* Check a file against a .sig file
|
|
70
|
-
* line
|
|
123
|
+
* Check a file against a signature file. A wordpress .sig file can hold one
|
|
124
|
+
* signature per line, and is valid when any of them match, so that a file
|
|
71
125
|
* can carry the signatures of a key rotation window.
|
|
72
126
|
*
|
|
73
127
|
* @param path File path.
|
|
74
128
|
* @param signaturePath Signature file path.
|
|
75
129
|
* @param publicKey Public key from parsePublicKey().
|
|
130
|
+
* @param format Signature format.
|
|
76
131
|
* @return Whether a signature matches.
|
|
77
132
|
*/
|
|
78
|
-
export declare function verifyFileSignature(path: string, signaturePath: string, publicKey: KeyObject): Promise<boolean>;
|
|
133
|
+
export declare function verifyFileSignature(path: string, signaturePath: string, publicKey: KeyObject, format?: SignatureFormat): Promise<boolean>;
|