@wpelevator/wp-release 0.13.0 → 0.15.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.
Files changed (62) hide show
  1. package/README.md +83 -25
  2. package/dist/changeset-config.d.ts +8 -6
  3. package/dist/changeset-config.js +7 -5
  4. package/dist/changeset-packages.d.ts +12 -2
  5. package/dist/changeset-packages.js +13 -1
  6. package/dist/changeset-release.d.ts +5 -2
  7. package/dist/changeset-release.js +12 -6
  8. package/dist/cli.d.ts +16 -7
  9. package/dist/cli.js +58 -11
  10. package/dist/commands/args.d.ts +11 -21
  11. package/dist/commands/args.js +24 -35
  12. package/dist/commands/changeset-add.d.ts +1 -1
  13. package/dist/commands/changeset-add.js +3 -2
  14. package/dist/commands/changeset-status.d.ts +1 -1
  15. package/dist/commands/changeset-status.js +2 -1
  16. package/dist/commands/changeset-tag.d.ts +1 -1
  17. package/dist/commands/changeset-tag.js +4 -2
  18. package/dist/commands/changeset-version.d.ts +1 -1
  19. package/dist/commands/changeset-version.js +2 -1
  20. package/dist/commands/checksums.js +2 -2
  21. package/dist/commands/command.d.ts +5 -0
  22. package/dist/commands/deploy-git.d.ts +1 -1
  23. package/dist/commands/deploy-git.js +2 -1
  24. package/dist/commands/deploy-svn.d.ts +11 -15
  25. package/dist/commands/deploy-svn.js +26 -5
  26. package/dist/commands/keygen.d.ts +3 -0
  27. package/dist/commands/keygen.js +6 -1
  28. package/dist/commands/package-deploys.d.ts +8 -0
  29. package/dist/commands/package-deploys.js +5 -0
  30. package/dist/commands/sign.d.ts +10 -1
  31. package/dist/commands/sign.js +29 -31
  32. package/dist/commands/verify.d.ts +3 -0
  33. package/dist/commands/verify.js +43 -24
  34. package/dist/commands/zip.d.ts +23 -8
  35. package/dist/commands/zip.js +171 -73
  36. package/dist/ed25519.d.ts +25 -0
  37. package/dist/ed25519.js +44 -0
  38. package/dist/file-selection.d.ts +21 -0
  39. package/dist/file-selection.js +35 -0
  40. package/dist/git.d.ts +17 -0
  41. package/dist/git.js +23 -0
  42. package/dist/index.d.ts +11 -5
  43. package/dist/index.js +6 -3
  44. package/dist/minisign.d.ts +41 -0
  45. package/dist/minisign.js +128 -0
  46. package/dist/package-deploy.d.ts +28 -0
  47. package/dist/package-deploy.js +41 -6
  48. package/dist/package-zip.d.ts +52 -0
  49. package/dist/package-zip.js +78 -0
  50. package/dist/release-repository.d.ts +4 -3
  51. package/dist/release-repository.js +5 -4
  52. package/dist/signing.d.ts +100 -9
  53. package/dist/signing.js +165 -41
  54. package/dist/ssh-signature.d.ts +43 -0
  55. package/dist/ssh-signature.js +156 -0
  56. package/dist/summary.d.ts +3 -3
  57. package/dist/summary.js +2 -2
  58. package/dist/tag-format.d.ts +21 -12
  59. package/dist/tag-format.js +25 -39
  60. package/dist/version-files.d.ts +6 -7
  61. package/dist/version-files.js +10 -11
  62. package/package.json +2 -2
@@ -0,0 +1,44 @@
1
+ import { createHash, createPublicKey } from 'node:crypto';
2
+ import { createReadStream } from 'node:fs';
3
+ export const PUBLIC_KEY_BYTES = 32;
4
+ // DER prefix of a SPKI Ed25519 public key, followed by the 32-byte key.
5
+ const SPKI_PREFIX = Buffer.from('302a300506032b6570032100', 'hex');
6
+ /**
7
+ * Get the raw 32 bytes of an Ed25519 public key.
8
+ *
9
+ * @param key Private or public key.
10
+ * @return Public key bytes.
11
+ */
12
+ export function getRawPublicKey(key) {
13
+ return ('private' === key.type ? createPublicKey(key) : key)
14
+ .export({ format: 'der', type: 'spki' })
15
+ .subarray(SPKI_PREFIX.length);
16
+ }
17
+ /**
18
+ * Get an Ed25519 public key from its raw 32 bytes.
19
+ *
20
+ * @param bytes Public key bytes.
21
+ * @return Public key.
22
+ */
23
+ export function createRawPublicKey(bytes) {
24
+ return createPublicKey({
25
+ key: Buffer.concat([SPKI_PREFIX, bytes]),
26
+ format: 'der',
27
+ type: 'spki',
28
+ });
29
+ }
30
+ /**
31
+ * Get the raw digest of a file, which the signature formats sign instead of
32
+ * the whole file.
33
+ *
34
+ * @param path File path.
35
+ * @param algorithm Node.js hash algorithm, like sha384 or blake2b512.
36
+ * @return Digest bytes.
37
+ */
38
+ export async function digestFile(path, algorithm) {
39
+ const hash = createHash(algorithm);
40
+ for await (const chunk of createReadStream(path)) {
41
+ hash.update(chunk);
42
+ }
43
+ return hash.digest();
44
+ }
@@ -82,3 +82,24 @@ export declare function isErrorCode(error: unknown, code: string): boolean;
82
82
  * @throws {ValidationError} For selected symbolic links, which ZIP archives and WordPress.org SVN don't support.
83
83
  */
84
84
  export declare function selectFiles(sourceDir: string, rules: FileRules): Promise<string[]>;
85
+ /**
86
+ * Ignore a file that a command writes or reads, such as the checksums
87
+ * manifest, when it's inside the selected directory.
88
+ *
89
+ * @param rules Matcher from loadFileRules().
90
+ * @param sourceDir Selected directory.
91
+ * @param path Absolute file path.
92
+ * @return The same matcher.
93
+ */
94
+ export declare function ignoreOwnFile(rules: FileRules, sourceDir: string, path: string): FileRules;
95
+ /**
96
+ * Ignore the signature files of every format that "wp-release sign" writes
97
+ * next to a file a command writes, when they're inside the selected
98
+ * directory.
99
+ *
100
+ * @param rules Matcher from loadFileRules().
101
+ * @param sourceDir Selected directory.
102
+ * @param path Absolute path of the signed file.
103
+ * @return The same matcher.
104
+ */
105
+ export declare function ignoreOwnSignature(rules: FileRules, sourceDir: string, path: string): FileRules;
@@ -3,6 +3,7 @@ import { isAbsolute, join, posix, relative, resolve } from 'node:path';
3
3
  import ignore from 'ignore';
4
4
  import { ValidationError } from "./errors.js";
5
5
  import { createGitRunner } from "./git.js";
6
+ import { SIGNATURE_FORMATS } from "./signing.js";
6
7
  export const DISTIGNORE_FILENAME = '.distignore';
7
8
  /**
8
9
  * Version control metadata that is never released, at any depth. Without
@@ -227,3 +228,37 @@ async function findEntryFiles(sourceDir, path, entry, rules) {
227
228
  }
228
229
  return [];
229
230
  }
231
+ /**
232
+ * Ignore a file that a command writes or reads, such as the checksums
233
+ * manifest, when it's inside the selected directory.
234
+ *
235
+ * @param rules Matcher from loadFileRules().
236
+ * @param sourceDir Selected directory.
237
+ * @param path Absolute file path.
238
+ * @return The same matcher.
239
+ */
240
+ export function ignoreOwnFile(rules, sourceDir, path) {
241
+ const relativePath = relative(sourceDir, path).split('\\').join('/');
242
+ const isInside = relativePath &&
243
+ '..' !== relativePath &&
244
+ !relativePath.startsWith('../') &&
245
+ !isAbsolute(relativePath);
246
+ if (isInside) {
247
+ // Escape the gitignore wildcards so that the pattern matches only this file.
248
+ rules.add(`/${relativePath.replace(/[\\*?[\]!#]/g, '\\$&')}`);
249
+ }
250
+ return rules;
251
+ }
252
+ /**
253
+ * Ignore the signature files of every format that "wp-release sign" writes
254
+ * next to a file a command writes, when they're inside the selected
255
+ * directory.
256
+ *
257
+ * @param rules Matcher from loadFileRules().
258
+ * @param sourceDir Selected directory.
259
+ * @param path Absolute path of the signed file.
260
+ * @return The same matcher.
261
+ */
262
+ export function ignoreOwnSignature(rules, sourceDir, path) {
263
+ return Object.values(SIGNATURE_FORMATS).reduce((matcher, { extension }) => ignoreOwnFile(matcher, sourceDir, path + extension), rules);
264
+ }
package/dist/git.d.ts CHANGED
@@ -26,6 +26,23 @@ export type GitRunner = (input: GitCommandInput) => Promise<GitCommandOutput>;
26
26
  * @return Text with the user info of URLs replaced by ***.
27
27
  */
28
28
  export declare function redactUrls(text: string): string;
29
+ /** Host and path of a repository URL. */
30
+ export interface RepositoryLocation {
31
+ /** Host with the port, like github.com or git.example.test:8443. */
32
+ host: string;
33
+ /** Path without the leading slash and the .git suffix, like owner/repo or group/subgroup/project. */
34
+ path: string;
35
+ }
36
+ /**
37
+ * Get the host and path of a remote repository URL, like github.com and
38
+ * owner/repo for https://github.com/owner/repo.git or
39
+ * git@github.com:owner/repo.git, for steps that call the API of the forge
40
+ * that hosts it. Credentials in the URL aren't part of either.
41
+ *
42
+ * @param url Repository URL: HTTP(S), SSH or git, or the scp-like SSH form.
43
+ * @return Host and path, or undefined for a local repository or another URL.
44
+ */
45
+ export declare function parseRepositoryUrl(url: string): RepositoryLocation | undefined;
29
46
  /**
30
47
  * Failed git command.
31
48
  */
package/dist/git.js CHANGED
@@ -9,6 +9,29 @@ import { spawn } from 'node:child_process';
9
9
  export function redactUrls(text) {
10
10
  return text.replace(/(\w+:\/\/)[^/@\s]+@/g, '$1***@');
11
11
  }
12
+ /**
13
+ * Get the host and path of a remote repository URL, like github.com and
14
+ * owner/repo for https://github.com/owner/repo.git or
15
+ * git@github.com:owner/repo.git, for steps that call the API of the forge
16
+ * that hosts it. Credentials in the URL aren't part of either.
17
+ *
18
+ * @param url Repository URL: HTTP(S), SSH or git, or the scp-like SSH form.
19
+ * @return Host and path, or undefined for a local repository or another URL.
20
+ */
21
+ export function parseRepositoryUrl(url) {
22
+ const scpLike = url.match(/^(?:[^@/:]+@)?([^/:\\]{2,}):(?!\/\/)(.+)$/);
23
+ const parsed = !scpLike && URL.canParse(url) ? new URL(url) : undefined;
24
+ const isRemote = ['http:', 'https:', 'ssh:', 'git:'].includes(parsed?.protocol ?? '');
25
+ const location = scpLike
26
+ ? { host: scpLike[1] ?? '', path: scpLike[2] ?? '' }
27
+ : { host: parsed?.host ?? '', path: parsed?.pathname ?? '' };
28
+ const path = location.path
29
+ .replace(/^\/+|\/+$/g, '')
30
+ .replace(/\.git$/, '');
31
+ return (scpLike || isRemote) && location.host && path
32
+ ? { host: location.host, path }
33
+ : undefined;
34
+ }
12
35
  /**
13
36
  * Failed git command.
14
37
  */
package/dist/index.d.ts CHANGED
@@ -9,7 +9,10 @@ export { DISTIGNORE_FILENAME, parseExclude, resolveFileSelection, loadFileRules,
9
9
  export { FILENAME_FORMAT, formatFilename } from './filename.ts';
10
10
  export { CHECKSUMS_EXTENSION, CHECKSUM_ALGORITHMS, hashFile, hashFiles, createChecksums, writeChecksums, readChecksums, parseChecksums, compareChecksums, verifyChecksums, } from './checksums.ts';
11
11
  export { HEADER_BYTES, PLUGIN_HEADERS, THEME_HEADERS, README_HEADERS, README_FILENAMES, parseFileHeaders, parseReadmeHeaders, readFileStart, findFileHeaderLine, findReadmeHeaderLine, findMainPluginFile, readPackageMeta, } from './package-meta.ts';
12
- export { SIGNATURE_EXTENSION, SIGNING_KEY_ENV, parseSigningKey, parsePublicKey, generateSigningKeyPair, getPublicKey, getFileDigest, signFile, writeSignature, verifyFileSignature, } from './signing.ts';
12
+ export { DEFAULT_SIGNATURE_FORMAT, SIGNATURE_EXTENSION, SIGNATURE_FORMATS, SIGNING_KEY_ENV, parseSigningKey, parsePublicKey, generateSigningKeyPair, getPublicKey, getPublicKeys, getFileDigest, getSignaturePath, isSignatureFormat, signFile, writeSignature, signFiles, verifyFileSignature, } from './signing.ts';
13
+ export type { SignatureFormat, SignatureFormatHandler } from './signing.ts';
14
+ export { MINISIGN_EXTENSION, createMinisignSignature, getMinisignPublicKey, parseMinisignPublicKey, verifyMinisignSignature, } from './minisign.ts';
15
+ export { SSH_SIGNATURE_EXTENSION, SSH_SIGNATURE_NAMESPACE, createSshSignature, getSshPublicKey, parseSshPublicKey, verifySshSignature, } from './ssh-signature.ts';
13
16
  export { ZIP_MTIME, ZIP_FILE_MODE, createZip } from './zip.ts';
14
17
  export { isUrl, downloadFile } from './download.ts';
15
18
  export { readZipEntries, getZipEntryErrors, extractZip } from './unzip.ts';
@@ -20,19 +23,22 @@ export { defineConfig, packageJsonVersion, composerJsonVersion, wordpressPluginV
20
23
  export { Svn, SvnError, createSvnRunner, parseStatus, toSvnPath, } from './svn.ts';
21
24
  export { syncDirectory, copyFileIfChanged } from './sync.ts';
22
25
  export { ASSET_MIME_TYPES, SvnDeploy } from './svn-deploy.ts';
23
- export { GitError, createGitRunner, redactUrls } from './git.ts';
26
+ export { GitError, createGitRunner, parseRepositoryUrl, redactUrls, } from './git.ts';
24
27
  export { GitDeploy, getDefaultToken, getGitAuthEnv, isValidRefName, resolveGitDeployOptions, } from './git-deploy.ts';
25
28
  export { getGitDeployInputFromArgs } from './commands/deploy-git.ts';
26
- export { getDeployPackages, getGitDeploys, getSvnDeploys, } from './package-deploy.ts';
29
+ export { getDeployPackages, getGitDeploys, getPackageFiles, getSvnDeploys, } from './package-deploy.ts';
30
+ export { zipPackage } from './package-zip.ts';
31
+ export type { ZipPackageInput, ZipPackageResult } from './package-zip.ts';
27
32
  export type { PackageDeploy, PackageDeployFiles, PackageDeployOptions, PackageFileSelection, PackageDeployPlanItem, PackageDeployRelease, } from './package-deploy.ts';
28
33
  export type { PackageDeployOutcome } from './commands/package-deploys.ts';
29
34
  export type { CliIo, CommandResults } from './cli.ts';
30
35
  export type { ChecksumsResult } from './commands/checksums.ts';
31
36
  export type { SignResult } from './commands/sign.ts';
37
+ export type { SignFilesInput, SignFilesResult } from './signing.ts';
32
38
  export type { KeygenResult } from './commands/keygen.ts';
33
39
  export type { ZipResult } from './commands/zip.ts';
34
40
  export type { Command, CommandContext, CommandOutcome, ParsedValues, } from './commands/command.ts';
35
- export type { DeployArgsInput } from './commands/deploy-svn.ts';
41
+ export type { DeployArgsInput, DeploySvnResult, } from './commands/deploy-svn.ts';
36
42
  export type { Annotation, LogEntry, LogLevel, LogWriter, LoggerOptions, OutputStream, SummaryBuilder, TextWriterOptions, } from './logger.ts';
37
43
  export type { GitHubActionsWriterOptions } from './github-actions-writer.ts';
38
44
  export type { Problem } from './errors.ts';
@@ -51,5 +57,5 @@ export type { SvnCheckoutInput, SvnCommandInput, SvnCommandOutput, SvnCommitInpu
51
57
  export type { SyncDirectoryInput, SyncResult } from './sync.ts';
52
58
  export type { SummaryContext } from './summary.ts';
53
59
  export type { CheckoutPath, DeployResult } from './svn-deploy.ts';
54
- export type { GitCommandInput, GitCommandOutput, GitRunner } from './git.ts';
60
+ export type { GitCommandInput, GitCommandOutput, GitRunner, RepositoryLocation, } from './git.ts';
55
61
  export type { GitDeployInput, GitDeployOptions, GitDeployResult, } from './git-deploy.ts';
package/dist/index.js CHANGED
@@ -9,7 +9,9 @@ export { DISTIGNORE_FILENAME, parseExclude, resolveFileSelection, loadFileRules,
9
9
  export { FILENAME_FORMAT, formatFilename } from "./filename.js";
10
10
  export { CHECKSUMS_EXTENSION, CHECKSUM_ALGORITHMS, hashFile, hashFiles, createChecksums, writeChecksums, readChecksums, parseChecksums, compareChecksums, verifyChecksums, } from "./checksums.js";
11
11
  export { HEADER_BYTES, PLUGIN_HEADERS, THEME_HEADERS, README_HEADERS, README_FILENAMES, parseFileHeaders, parseReadmeHeaders, readFileStart, findFileHeaderLine, findReadmeHeaderLine, findMainPluginFile, readPackageMeta, } from "./package-meta.js";
12
- export { SIGNATURE_EXTENSION, SIGNING_KEY_ENV, parseSigningKey, parsePublicKey, generateSigningKeyPair, getPublicKey, getFileDigest, signFile, writeSignature, verifyFileSignature, } from "./signing.js";
12
+ export { DEFAULT_SIGNATURE_FORMAT, SIGNATURE_EXTENSION, SIGNATURE_FORMATS, SIGNING_KEY_ENV, parseSigningKey, parsePublicKey, generateSigningKeyPair, getPublicKey, getPublicKeys, getFileDigest, getSignaturePath, isSignatureFormat, signFile, writeSignature, signFiles, verifyFileSignature, } from "./signing.js";
13
+ export { MINISIGN_EXTENSION, createMinisignSignature, getMinisignPublicKey, parseMinisignPublicKey, verifyMinisignSignature, } from "./minisign.js";
14
+ export { SSH_SIGNATURE_EXTENSION, SSH_SIGNATURE_NAMESPACE, createSshSignature, getSshPublicKey, parseSshPublicKey, verifySshSignature, } from "./ssh-signature.js";
13
15
  export { ZIP_MTIME, ZIP_FILE_MODE, createZip } from "./zip.js";
14
16
  export { isUrl, downloadFile } from "./download.js";
15
17
  export { readZipEntries, getZipEntryErrors, extractZip } from "./unzip.js";
@@ -20,7 +22,8 @@ export { defineConfig, packageJsonVersion, composerJsonVersion, wordpressPluginV
20
22
  export { Svn, SvnError, createSvnRunner, parseStatus, toSvnPath, } from "./svn.js";
21
23
  export { syncDirectory, copyFileIfChanged } from "./sync.js";
22
24
  export { ASSET_MIME_TYPES, SvnDeploy } from "./svn-deploy.js";
23
- export { GitError, createGitRunner, redactUrls } from "./git.js";
25
+ export { GitError, createGitRunner, parseRepositoryUrl, redactUrls, } from "./git.js";
24
26
  export { GitDeploy, getDefaultToken, getGitAuthEnv, isValidRefName, resolveGitDeployOptions, } from "./git-deploy.js";
25
27
  export { getGitDeployInputFromArgs } from "./commands/deploy-git.js";
26
- export { getDeployPackages, getGitDeploys, getSvnDeploys, } from "./package-deploy.js";
28
+ export { getDeployPackages, getGitDeploys, getPackageFiles, getSvnDeploys, } from "./package-deploy.js";
29
+ export { zipPackage } from "./package-zip.js";
@@ -0,0 +1,41 @@
1
+ import type { KeyObject } from 'node:crypto';
2
+ export declare const MINISIGN_EXTENSION = ".minisig";
3
+ /**
4
+ * Get the minisign public key of a key, the base64 line that `minisign -P`
5
+ * takes and that the second line of a minisign .pub file holds.
6
+ *
7
+ * @param key Private or public key.
8
+ * @return Base64 minisign public key.
9
+ */
10
+ export declare function getMinisignPublicKey(key: KeyObject): string;
11
+ /**
12
+ * Get the raw public key from a minisign public key, the base64 line or the
13
+ * whole two-line .pub file.
14
+ *
15
+ * @param encoded Minisign public key.
16
+ * @return Public key bytes, or null when it isn't a minisign public key.
17
+ */
18
+ export declare function parseMinisignPublicKey(encoded: string): Buffer | null;
19
+ /**
20
+ * Create a minisign signature of a file: an Ed25519 signature of its
21
+ * BLAKE2b-512 digest, and a global signature that also covers the trusted
22
+ * comment with the signing time and file name.
23
+ *
24
+ * @param path File path.
25
+ * @param privateKey Private key.
26
+ * @return Contents of the .minisig file.
27
+ */
28
+ export declare function createMinisignSignature(path: string, privateKey: KeyObject): Promise<string>;
29
+ /**
30
+ * Check a file against a minisign signature file, including the global
31
+ * signature of the trusted comment. Both the BLAKE2b-512 prehashed (ED)
32
+ * and the legacy (Ed) signatures of minisign are accepted. The key ID isn't
33
+ * compared, since it only tells minisign which key to use, so signatures of
34
+ * keys that minisign generated with a random ID are accepted too.
35
+ *
36
+ * @param path File path.
37
+ * @param signaturePath Signature file path.
38
+ * @param publicKey Public key.
39
+ * @return Whether the signature matches.
40
+ */
41
+ export declare function verifyMinisignSignature(path: string, signaturePath: string, publicKey: KeyObject): Promise<boolean>;
@@ -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
+ }
@@ -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
  /**
@@ -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 ?? '{path}/{version}';
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
- const slug = await resolveTemplate(target.slug ?? '{slug}', release, version);
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
- : formatTag(template, release.package, release.version ?? '')
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>;