@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.
Files changed (61) hide show
  1. package/README.md +67 -20
  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.js +33 -9
  9. package/dist/commands/args.d.ts +0 -21
  10. package/dist/commands/args.js +1 -35
  11. package/dist/commands/changeset-add.d.ts +1 -1
  12. package/dist/commands/changeset-add.js +3 -2
  13. package/dist/commands/changeset-status.d.ts +1 -1
  14. package/dist/commands/changeset-status.js +2 -1
  15. package/dist/commands/changeset-tag.d.ts +1 -1
  16. package/dist/commands/changeset-tag.js +4 -2
  17. package/dist/commands/changeset-version.d.ts +1 -1
  18. package/dist/commands/changeset-version.js +2 -1
  19. package/dist/commands/checksums.js +2 -2
  20. package/dist/commands/command.d.ts +2 -0
  21. package/dist/commands/deploy-git.d.ts +1 -1
  22. package/dist/commands/deploy-git.js +2 -1
  23. package/dist/commands/deploy-svn.d.ts +1 -1
  24. package/dist/commands/deploy-svn.js +2 -1
  25. package/dist/commands/keygen.d.ts +3 -0
  26. package/dist/commands/keygen.js +6 -1
  27. package/dist/commands/package-deploys.d.ts +8 -0
  28. package/dist/commands/package-deploys.js +5 -0
  29. package/dist/commands/sign.d.ts +19 -1
  30. package/dist/commands/sign.js +47 -12
  31. package/dist/commands/verify.d.ts +3 -0
  32. package/dist/commands/verify.js +43 -24
  33. package/dist/commands/zip.d.ts +12 -8
  34. package/dist/commands/zip.js +135 -78
  35. package/dist/ed25519.d.ts +25 -0
  36. package/dist/ed25519.js +44 -0
  37. package/dist/file-selection.d.ts +21 -0
  38. package/dist/file-selection.js +35 -0
  39. package/dist/git.d.ts +17 -0
  40. package/dist/git.js +23 -0
  41. package/dist/index.d.ts +9 -4
  42. package/dist/index.js +6 -3
  43. package/dist/minisign.d.ts +41 -0
  44. package/dist/minisign.js +128 -0
  45. package/dist/package-deploy.d.ts +28 -0
  46. package/dist/package-deploy.js +41 -6
  47. package/dist/package-zip.d.ts +52 -0
  48. package/dist/package-zip.js +78 -0
  49. package/dist/release-repository.d.ts +4 -3
  50. package/dist/release-repository.js +5 -4
  51. package/dist/signing.d.ts +64 -9
  52. package/dist/signing.js +113 -40
  53. package/dist/ssh-signature.d.ts +43 -0
  54. package/dist/ssh-signature.js +156 -0
  55. package/dist/summary.d.ts +3 -3
  56. package/dist/summary.js +2 -2
  57. package/dist/tag-format.d.ts +21 -12
  58. package/dist/tag-format.js +25 -39
  59. package/dist/version-files.d.ts +6 -7
  60. package/dist/version-files.js +10 -11
  61. package/package.json +2 -2
@@ -1,7 +1,6 @@
1
1
  import { stat } from 'node:fs/promises';
2
- import { isAbsolute, relative, resolve } from 'node:path';
2
+ import { resolve } from 'node:path';
3
3
  import { ValidationError } from "../errors.js";
4
- import { SIGNATURE_EXTENSION } from "../signing.js";
5
4
  import { getBoolean, getString, getStrings } from "./command.js";
6
5
  /**
7
6
  * Options shared by the commands that select files with .distignore and
@@ -63,39 +62,6 @@ export async function isDirectory(path) {
63
62
  const stats = await stat(path).catch(() => null);
64
63
  return Boolean(stats?.isDirectory());
65
64
  }
66
- /**
67
- * Ignore a file that the command writes or reads, such as the checksums
68
- * manifest, when it's inside the selected directory.
69
- *
70
- * @param rules Matcher from loadFileRules().
71
- * @param sourceDir Selected directory.
72
- * @param path Absolute file path.
73
- * @return The same matcher.
74
- */
75
- export function ignoreOwnFile(rules, sourceDir, path) {
76
- const relativePath = relative(sourceDir, path).split('\\').join('/');
77
- const isInside = relativePath &&
78
- '..' !== relativePath &&
79
- !relativePath.startsWith('../') &&
80
- !isAbsolute(relativePath);
81
- if (isInside) {
82
- // Escape the gitignore wildcards so that the pattern matches only this file.
83
- rules.add(`/${relativePath.replace(/[\\*?[\]!#]/g, '\\$&')}`);
84
- }
85
- return rules;
86
- }
87
- /**
88
- * Ignore the signature file that "wp-release sign" writes next to a file the
89
- * command writes, when it's inside the selected directory.
90
- *
91
- * @param rules Matcher from loadFileRules().
92
- * @param sourceDir Selected directory.
93
- * @param path Absolute path of the signed file.
94
- * @return The same matcher.
95
- */
96
- export function ignoreOwnSignature(rules, sourceDir, path) {
97
- return ignoreOwnFile(rules, sourceDir, path + SIGNATURE_EXTENSION);
98
- }
99
65
  /**
100
66
  * Option for the source directory, which can also be the first positional
101
67
  * argument.
@@ -29,7 +29,7 @@ declare const _default: {
29
29
  readonly type: "string";
30
30
  };
31
31
  };
32
- run({ values, cwd, configFile, logger, terminal }: import("./command.ts").CommandContext): Promise<{
32
+ run({ values, cwd, configFile, tagFormat, logger, terminal }: import("./command.ts").CommandContext): Promise<{
33
33
  exitCode: number;
34
34
  result: {
35
35
  file: string;
@@ -94,7 +94,7 @@ export default {
94
94
  --summary <text> Customer-facing release note
95
95
  --empty Record that this change does not need a release
96
96
  --since <ref> Compare changed files with this git ref (default:
97
- origin/<baseBranch>)
97
+ <remote>/<baseBranch>)
98
98
  --name <name> Changeset filename (default: random identifier)
99
99
 
100
100
  Use --package and --bump pairs for package releases, or --empty for changes
@@ -104,11 +104,12 @@ ref, including uncommitted files, and have no changeset yet. Without
104
104
  --package and --bump in a terminal, the command asks for the packages, with
105
105
  the changed ones preselected, their bumps and the release note.`,
106
106
  options: changesetAddArgs,
107
- async run({ values, cwd, configFile, logger, terminal }) {
107
+ async run({ values, cwd, configFile, tagFormat, logger, terminal }) {
108
108
  const input = parseChangesetAddArgs(values);
109
109
  const release = new ChangesetRelease({
110
110
  cwd,
111
111
  ...(configFile ? { configFile } : {}),
112
+ ...(tagFormat ? { tagFormat } : {}),
112
113
  });
113
114
  const changeset = await getChangeset(release, input, terminal);
114
115
  // Adding resolves the package paths and rejects a package passed twice.
@@ -26,7 +26,7 @@ declare const _default: {
26
26
  readonly type: "string";
27
27
  };
28
28
  };
29
- run({ values, cwd, configFile, logger }: import("./command.ts").CommandContext): Promise<{
29
+ run({ values, cwd, configFile, tagFormat, logger }: import("./command.ts").CommandContext): Promise<{
30
30
  exitCode: number;
31
31
  result: {
32
32
  releases: {
@@ -10,11 +10,12 @@ export default {
10
10
  Exit status is 2 when a changed package has no changeset added since the comparison base.
11
11
  An empty changeset added since the comparison base counts as an explicit no-release decision.`,
12
12
  options: changesetStatusArgs,
13
- async run({ values, cwd, configFile, logger }) {
13
+ async run({ values, cwd, configFile, tagFormat, logger }) {
14
14
  const { since } = parseChangesetStatusArgs(values);
15
15
  const result = await getChangesetStatus({
16
16
  cwd,
17
17
  ...(configFile ? { configFile } : {}),
18
+ ...(tagFormat ? { tagFormat } : {}),
18
19
  ...(since ? { since } : {}),
19
20
  });
20
21
  logger.summary(() => {
@@ -24,7 +24,7 @@ declare const _default: {
24
24
  readonly type: "string";
25
25
  };
26
26
  };
27
- run({ values, cwd, configFile, logger }: import("./command.ts").CommandContext): Promise<{
27
+ run({ values, cwd, configFile, tagFormat, logger }: import("./command.ts").CommandContext): Promise<{
28
28
  exitCode: number;
29
29
  result: import("../changeset-release.ts").ChangesetTagResult;
30
30
  }>;
@@ -14,7 +14,8 @@ export default {
14
14
  the configured root (repeatable)
15
15
  --version <version> Fail unless the single --package is at this version;
16
16
  the version to tag for packages versioned by git tags
17
- --push Push newly created tags to origin, one at a time
17
+ --push Push newly created tags to the configured remote
18
+ (default: origin), one at a time
18
19
 
19
20
  Tags are created only when absent, so rerunning after a partial failure is
20
21
  safe. Before a tag is created, every configured version target must match the
@@ -23,11 +24,12 @@ and pre-release flag of its release, for a workflow step that creates the
23
24
  GitHub Release. --untagged lists only the releases that still need a tag, for
24
25
  example to release each of them with a separate workflow.`,
25
26
  options: changesetTagArgs,
26
- async run({ values, cwd, configFile, logger }) {
27
+ async run({ values, cwd, configFile, tagFormat, logger }) {
27
28
  const args = parseChangesetTagArgs(values);
28
29
  const result = await tagChangesets({
29
30
  cwd,
30
31
  ...(configFile ? { configFile } : {}),
32
+ ...(tagFormat ? { tagFormat } : {}),
31
33
  ...args,
32
34
  });
33
35
  result.tags.forEach((item) => {
@@ -36,7 +36,7 @@ declare const _default: {
36
36
  readonly type: "string";
37
37
  };
38
38
  };
39
- run({ values, cwd, configFile, logger }: import("./command.ts").CommandContext): Promise<{
39
+ run({ values, cwd, configFile, tagFormat, logger }: import("./command.ts").CommandContext): Promise<{
40
40
  exitCode: number;
41
41
  result: {
42
42
  date: string;
@@ -28,11 +28,12 @@ The readme Stable tag keeps the last stable version.
28
28
  With --snapshot, build and publish the versioned files without committing
29
29
  them, for example to test a pull request.`,
30
30
  options: changesetVersionArgs,
31
- async run({ values, cwd, configFile, logger }) {
31
+ async run({ values, cwd, configFile, tagFormat, logger }) {
32
32
  const args = parseChangesetVersionArgs(values);
33
33
  const result = await versionChangesets({
34
34
  cwd,
35
35
  ...(configFile ? { configFile } : {}),
36
+ ...(tagFormat ? { tagFormat } : {}),
36
37
  ...args,
37
38
  });
38
39
  result.releases.forEach((release) => logger.info(`${release.package.name}: ${release.currentVersion} → ${release.nextVersion} (${release.bump})`));
@@ -1,10 +1,10 @@
1
1
  import { basename, resolve } from 'node:path';
2
2
  import { CHECKSUMS_EXTENSION, createChecksums, writeChecksums, } from "../checksums.js";
3
- import { loadFileRules, resolveFileSelection } from "../file-selection.js";
3
+ import { ignoreOwnFile, ignoreOwnSignature, loadFileRules, resolveFileSelection, } from "../file-selection.js";
4
4
  import { FILENAME_FORMAT, formatFilename } from "../filename.js";
5
5
  import { readPackageMeta } from "../package-meta.js";
6
6
  import { getChecksumsSummary } from "../summary.js";
7
- import { checkDirectory, fileSelectionArgs, getFileSelectionInput, getSourceDirArg, ignoreOwnFile, ignoreOwnSignature, resolveOutput, sourceDirArgs, } from "./args.js";
7
+ import { checkDirectory, fileSelectionArgs, getFileSelectionInput, getSourceDirArg, resolveOutput, sourceDirArgs, } from "./args.js";
8
8
  import { getBoolean, getString } from "./command.js";
9
9
  export default {
10
10
  name: 'checksums',
@@ -14,6 +14,8 @@ export interface CommandContext {
14
14
  cwd: string;
15
15
  /** Explicit JavaScript release config path from --config, if supplied. */
16
16
  configFile?: string;
17
+ /** Tag format from --tag-format, which replaces the tagFormat of the config. */
18
+ tagFormat?: string;
17
19
  /** Logger for the command output. */
18
20
  logger: Logger;
19
21
  /** Terminal for interactive prompts, only when the CLI runs in one and prints no JSON. */
@@ -47,7 +47,7 @@ declare const _default: {
47
47
  type: "string";
48
48
  };
49
49
  };
50
- run({ values, positionals, cwd, configFile, logger }: import("./command.ts").CommandContext): Promise<{
50
+ run({ values, positionals, cwd, configFile, tagFormat, logger }: import("./command.ts").CommandContext): Promise<{
51
51
  exitCode: number;
52
52
  result: {
53
53
  packages: import("../package-deploy.ts").PackageDeployPlanItem[];
@@ -114,7 +114,7 @@ Examples:
114
114
  ...packageDeployArgs,
115
115
  'dry-run': { type: 'boolean' },
116
116
  },
117
- async run({ values, positionals, cwd, configFile, logger }) {
117
+ async run({ values, positionals, cwd, configFile, tagFormat, logger }) {
118
118
  const { ref, packages, list } = getPackageDeployArgs(values, positionals, TARGET_FLAGS);
119
119
  if (ref) {
120
120
  const deploys = await getGitDeploys({
@@ -123,6 +123,7 @@ Examples:
123
123
  packages,
124
124
  dryRun: getBoolean(values, 'dry-run'),
125
125
  ...(configFile ? { configFile } : {}),
126
+ ...(tagFormat ? { tagFormat } : {}),
126
127
  });
127
128
  return list
128
129
  ? listPackageDeploys(deploys, logger, ref)
@@ -76,7 +76,7 @@ declare const _default: {
76
76
  type: "string";
77
77
  };
78
78
  };
79
- run({ values, positionals, cwd, configFile, logger }: import("./command.ts").CommandContext): Promise<{
79
+ run({ values, positionals, cwd, configFile, tagFormat, logger }: import("./command.ts").CommandContext): Promise<{
80
80
  exitCode: number;
81
81
  result: {
82
82
  packages: import("../package-deploy.ts").PackageDeployPlanItem[];
@@ -135,7 +135,7 @@ Examples:
135
135
  ...packageDeployArgs,
136
136
  'dry-run': { type: 'boolean' },
137
137
  },
138
- async run({ values, positionals, cwd, configFile, logger }) {
138
+ async run({ values, positionals, cwd, configFile, tagFormat, logger }) {
139
139
  const { ref, packages, list } = getPackageDeployArgs(values, positionals, TARGET_FLAGS);
140
140
  if (ref) {
141
141
  const deploys = await getSvnDeploys({
@@ -144,6 +144,7 @@ Examples:
144
144
  packages,
145
145
  dryRun: getBoolean(values, 'dry-run'),
146
146
  ...(configFile ? { configFile } : {}),
147
+ ...(tagFormat ? { tagFormat } : {}),
147
148
  });
148
149
  return list
149
150
  ? listPackageDeploys(deploys, logger, ref)
@@ -1,3 +1,4 @@
1
+ import type { SignatureFormat } from '../signing.ts';
1
2
  import type { CommandOutcome } from './command.ts';
2
3
  /** Generated Ed25519 signing keys. */
3
4
  export interface KeygenResult {
@@ -5,6 +6,8 @@ export interface KeygenResult {
5
6
  privateKey: string;
6
7
  /** Base64 Ed25519 public key for verification. */
7
8
  publicKey: string;
9
+ /** Public key in the form of each signature format. */
10
+ publicKeys: Record<SignatureFormat, string>;
8
11
  }
9
12
  declare const _default: {
10
13
  name: string;
@@ -8,7 +8,10 @@ export default {
8
8
  help: `Generate a signing key pair for "wp-release sign" and "wp-release verify".
9
9
 
10
10
  The private seed is printed once as WP_RELEASE_SIGNING_KEY. Store it securely
11
- and never commit it or share it. The public key can be distributed to verifiers.`,
11
+ and never commit it or share it. The public key can be distributed to verifiers.
12
+ It's also printed as a minisign public key for "minisign -V -P" and as an
13
+ ssh-ed25519 public key for the allowed_signers file of "ssh-keygen -Y verify",
14
+ since the same key signs every format of "wp-release sign".`,
12
15
  options: {},
13
16
  async run({ values, logger }) {
14
17
  if (getBoolean(values, 'github-output')) {
@@ -17,6 +20,8 @@ and never commit it or share it. The public key can be distributed to verifiers.
17
20
  const result = generateSigningKeyPair();
18
21
  logger.info(`WP_RELEASE_SIGNING_KEY=${result.privateKey}`);
19
22
  logger.info(`Public key: ${result.publicKey}`);
23
+ logger.info(`Minisign public key: ${result.publicKeys.minisign}`);
24
+ logger.info(`SSH public key: ${result.publicKeys.ssh}`);
20
25
  logger.info('Store the private seed securely; it cannot be recovered from the public key.');
21
26
  return { exitCode: 0, result };
22
27
  },
@@ -22,6 +22,14 @@ export interface PackageDeployOutcome<Result> {
22
22
  package: string;
23
23
  /** Package, target and ref, with credentials redacted. */
24
24
  label: string;
25
+ /** Repository or WordPress.org plugin of the target, with credentials redacted. */
26
+ target: string;
27
+ /** Tag or branch that the deploy writes in the target. */
28
+ targetRef: string;
29
+ /** Host of a remote target repository, like github.com. */
30
+ targetHost?: string;
31
+ /** Path of a remote target repository on its host, like owner/repo. */
32
+ targetPath?: string;
25
33
  version?: string;
26
34
  branch?: string;
27
35
  /** Deploy result, unless it failed. */
@@ -78,6 +78,11 @@ async function runDeploy(item, deploy, logger) {
78
78
  const outcome = {
79
79
  package: item.package.name,
80
80
  label: item.label,
81
+ target: item.target,
82
+ targetRef: item.targetRef,
83
+ ...(item.targetHost
84
+ ? { targetHost: item.targetHost, targetPath: item.targetPath }
85
+ : {}),
81
86
  ...item.release,
82
87
  };
83
88
  try {
@@ -1,17 +1,35 @@
1
+ import type { SignatureFormat } from '../signing.ts';
1
2
  import type { CommandOutcome } from './command.ts';
2
3
  /** Result of the sign command. */
3
4
  export interface SignResult {
4
- /** Absolute paths of the written .sig files. */
5
+ /** Absolute paths of the written signature files. */
5
6
  signatures: string[];
7
+ /** Signature formats that were written. */
8
+ formats: SignatureFormat[];
6
9
  /** Base64 public key of the signing key. */
7
10
  publicKey: string;
11
+ /** Public key of the signing key in the form of each signature format. */
12
+ publicKeys: Record<SignatureFormat, string>;
8
13
  }
14
+ /**
15
+ * Get the signature formats from the --format values, which can be repeated
16
+ * or comma-separated.
17
+ *
18
+ * @param values Option values.
19
+ * @return Unique formats, the default when none are given.
20
+ * @throws {ValidationError} If a format is unknown.
21
+ */
22
+ export declare function parseSignatureFormats(values: string[]): SignatureFormat[];
9
23
  declare const _default: {
10
24
  name: string;
11
25
  usage: string;
12
26
  summary: string;
13
27
  help: string;
14
28
  options: {
29
+ format: {
30
+ type: "string";
31
+ multiple: true;
32
+ };
15
33
  force: {
16
34
  type: "boolean";
17
35
  };
@@ -2,8 +2,29 @@ import { resolve } from 'node:path';
2
2
  import { stat } from 'node:fs/promises';
3
3
  import { ValidationError } from "../errors.js";
4
4
  import { getSignSummary } from "../summary.js";
5
- import { SIGNING_KEY_ENV, getPublicKey, parseSigningKey, writeSignature, } from "../signing.js";
6
- import { getBoolean } from "./command.js";
5
+ import { DEFAULT_SIGNATURE_FORMAT, SIGNATURE_FORMATS, SIGNING_KEY_ENV, getPublicKey, getPublicKeys, isSignatureFormat, parseSigningKey, writeSignature, } from "../signing.js";
6
+ import { getBoolean, getStrings } from "./command.js";
7
+ /**
8
+ * Get the signature formats from the --format values, which can be repeated
9
+ * or comma-separated.
10
+ *
11
+ * @param values Option values.
12
+ * @return Unique formats, the default when none are given.
13
+ * @throws {ValidationError} If a format is unknown.
14
+ */
15
+ export function parseSignatureFormats(values) {
16
+ const names = values
17
+ .flatMap((value) => value.split(','))
18
+ .map((name) => name.trim())
19
+ .filter(Boolean);
20
+ const unknown = names.filter((name) => !isSignatureFormat(name));
21
+ if (unknown.length) {
22
+ throw new ValidationError(`Unknown signature format ${unknown.join(', ')}, use ${Object.keys(SIGNATURE_FORMATS).join(', ')}.`);
23
+ }
24
+ return names.length
25
+ ? [...new Set(names.filter(isSignatureFormat))]
26
+ : [DEFAULT_SIGNATURE_FORMAT];
27
+ }
7
28
  export default {
8
29
  name: 'sign',
9
30
  usage: 'sign <file>... [options]',
@@ -13,21 +34,32 @@ export default {
13
34
  manifest
14
35
 
15
36
  Options:
16
- --force Overwrite existing .sig files
37
+ --format <names> Signature formats to write, comma-separated or
38
+ repeated, like wordpress,minisign,ssh (default:
39
+ wordpress):
40
+ wordpress <file>.sig for WordPress core's
41
+ verify_file_signature()
42
+ minisign <file>.minisig for "minisign -V"
43
+ ssh <file>.sshsig for "ssh-keygen -Y verify"
44
+ with the "file" namespace
45
+ --force Overwrite existing signature files
17
46
 
18
47
  The signing key is read from the ${SIGNING_KEY_ENV} environment variable, as a
19
48
  base64 32-byte Ed25519 seed or 64-byte libsodium secret key. There's no flag
20
49
  for it, since it would show in the process list.
21
50
 
22
- The signature is the one WordPress core's verify_file_signature() checks: a
23
- base64 Ed25519 signature of the raw SHA-384 digest of the file. Sign both
24
- the ZIP and the checksums manifest. Use --verbose to print the public key
25
- that belongs to the signing key.`,
51
+ The wordpress signature is the one WordPress core's verify_file_signature()
52
+ checks: a base64 Ed25519 signature of the raw SHA-384 digest of the file.
53
+ All formats are signed by the same key. Sign both the ZIP and the checksums
54
+ manifest. Use --verbose to print the public key of the signing key in the
55
+ form of each format.`,
26
56
  options: {
57
+ format: { type: 'string', multiple: true },
27
58
  force: { type: 'boolean' },
28
59
  },
29
60
  async run({ values, positionals, cwd, logger, }) {
30
61
  const encoded = process.env[SIGNING_KEY_ENV];
62
+ const formats = parseSignatureFormats(getStrings(values, 'format'));
31
63
  if (!positionals.length) {
32
64
  throw new ValidationError('Pass the files to sign.');
33
65
  }
@@ -36,22 +68,25 @@ that belongs to the signing key.`,
36
68
  }
37
69
  const privateKey = parseSigningKey(encoded);
38
70
  const publicKey = getPublicKey(privateKey);
71
+ const publicKeys = getPublicKeys(privateKey);
39
72
  const paths = positionals.map((file) => resolve(cwd, file));
40
73
  await Promise.all(paths.map(async (path) => {
41
74
  if (!(await stat(path).catch(() => null))?.isFile()) {
42
75
  throw new ValidationError(`${path} is not a file.`);
43
76
  }
44
77
  }));
45
- logger.debug(`Signing with the public key ${publicKey}.`);
78
+ formats.forEach((format) => logger.debug(`Signing ${format} with the public key ${publicKeys[format]}.`));
46
79
  const signatures = [];
47
80
  const force = getBoolean(values, 'force');
81
+ const jobs = paths.flatMap((path) => formats.map((format) => ({ path, format })));
48
82
  // Sign in order, so that a failure leaves a predictable set of files.
49
- for (const path of paths) {
50
- const signaturePath = await writeSignature(path, privateKey, force);
83
+ for (const { path, format } of jobs) {
84
+ const signaturePath = await writeSignature(path, privateKey, force, format);
51
85
  logger.info(signaturePath);
52
86
  signatures.push(signaturePath);
53
87
  }
54
- logger.summary((context) => getSignSummary(context, { signatures, publicKey }));
55
- return { exitCode: 0, result: { signatures, publicKey } };
88
+ const result = { signatures, formats, publicKey, publicKeys };
89
+ logger.summary((context) => getSignSummary(context, result));
90
+ return { exitCode: 0, result };
56
91
  },
57
92
  };
@@ -24,6 +24,9 @@ declare const _default: {
24
24
  'public-key': {
25
25
  type: "string";
26
26
  };
27
+ 'signature-format': {
28
+ type: "string";
29
+ };
27
30
  };
28
31
  run({ values, positionals, cwd, logger }: import("./command.ts").CommandContext): Promise<CommandOutcome<unknown>>;
29
32
  };
@@ -4,28 +4,29 @@ import { tmpdir } from 'node:os';
4
4
  import { basename, isAbsolute, join, posix, resolve } from 'node:path';
5
5
  import { readChecksums, verifyChecksums } from "../checksums.js";
6
6
  import { downloadFile, isUrl } from "../download.js";
7
- import { loadFileRules, parseExclude } from "../file-selection.js";
7
+ import { ignoreOwnFile, ignoreOwnSignature, loadFileRules, parseExclude, } from "../file-selection.js";
8
8
  import { ValidationError } from "../errors.js";
9
9
  import { readPackageMeta } from "../package-meta.js";
10
10
  import { logProblems } from "../report.js";
11
11
  import { getVerifySummary } from "../summary.js";
12
12
  import { extractZip, readZipEntries } from "../unzip.js";
13
- import { SIGNATURE_EXTENSION, parsePublicKey, verifyFileSignature, } from "../signing.js";
14
- import { checkDirectory, getSourceDirArg, ignoreOwnFile, ignoreOwnSignature, sourceDirArgs, } from "./args.js";
13
+ import { DEFAULT_SIGNATURE_FORMAT, SIGNATURE_FORMATS, getSignaturePath, isSignatureFormat, parsePublicKey, verifyFileSignature, } from "../signing.js";
14
+ import { checkDirectory, getSourceDirArg, sourceDirArgs } from "./args.js";
15
15
  import { getString, getStrings } from "./command.js";
16
16
  /**
17
17
  * Fail unless the manifest has a valid signature file.
18
18
  *
19
19
  * @param checksumsPath Manifest path.
20
- * @param publicKey Base64 public key.
20
+ * @param publicKey Public key in any form that parsePublicKey() takes.
21
+ * @param format Signature format.
21
22
  * @throws {ValidationError} If the signature is missing or doesn't match.
22
23
  */
23
- async function checkSignature(checksumsPath, publicKey) {
24
- const signaturePath = checksumsPath + SIGNATURE_EXTENSION;
24
+ async function checkSignature(checksumsPath, publicKey, format) {
25
+ const signaturePath = getSignaturePath(checksumsPath, format);
25
26
  if (!existsSync(signaturePath)) {
26
27
  throw new ValidationError(`${signaturePath} doesn't exist.`);
27
28
  }
28
- if (!(await verifyFileSignature(checksumsPath, signaturePath, parsePublicKey(publicKey)))) {
29
+ if (!(await verifyFileSignature(checksumsPath, signaturePath, parsePublicKey(publicKey), format))) {
29
30
  throw new ValidationError(`${signaturePath} isn't a valid signature of ${checksumsPath} for the public key.`);
30
31
  }
31
32
  }
@@ -111,15 +112,16 @@ function getEmbeddedPath(checksumsPath, embedded) {
111
112
  *
112
113
  * @param checksumsPath Manifest path.
113
114
  * @param path Relative path of the embedded manifest, if any.
115
+ * @param format Signature format.
114
116
  * @return Pairs of the originals and their copies.
115
117
  */
116
- function getCopyPairs(checksumsPath, path) {
118
+ function getCopyPairs(checksumsPath, path, format) {
117
119
  return path
118
120
  ? [
119
121
  { original: checksumsPath, copy: path },
120
122
  {
121
- original: checksumsPath + SIGNATURE_EXTENSION,
122
- copy: path + SIGNATURE_EXTENSION,
123
+ original: getSignaturePath(checksumsPath, format),
124
+ copy: getSignaturePath(path, format),
123
125
  },
124
126
  ]
125
127
  : [];
@@ -172,12 +174,13 @@ async function hasSameContents(original, file) {
172
174
  * @param input.manifest Checksums manifest.
173
175
  * @param input.checksumsPath Manifest path.
174
176
  * @param input.embedded Explicit path of the embedded manifest.
177
+ * @param input.format Signature format of the manifest signature.
175
178
  * @return The copies that were found to differ or be missing, and the paths of the existing copies.
176
179
  * @throws {ValidationError} If an explicit path can't be verified.
177
180
  */
178
181
  async function checkEmbeddedCopies(input) {
179
- const { dir, manifest, checksumsPath, embedded } = input;
180
- const pairs = getCopyPairs(checksumsPath, getEmbeddedPath(checksumsPath, embedded));
182
+ const { dir, manifest, checksumsPath, embedded, format } = input;
183
+ const pairs = getCopyPairs(checksumsPath, getEmbeddedPath(checksumsPath, embedded), format);
181
184
  if (embedded) {
182
185
  checkExplicitPairs(pairs, manifest);
183
186
  }
@@ -268,17 +271,19 @@ async function findVersionMismatch(dir, manifest) {
268
271
  * @param input.checksumsPath
269
272
  * @param input.exclude
270
273
  * @param input.embedded
274
+ * @param input.format
271
275
  * @param input.logger
272
276
  * @return Exit code and result.
273
277
  */
274
278
  async function verifyTarget(input) {
275
- const { target, manifest, checksumsPath, exclude, embedded, logger } = input;
279
+ const { target, manifest, checksumsPath, exclude, embedded, format, logger, } = input;
276
280
  const { dir } = target;
277
281
  const copies = await checkEmbeddedCopies({
278
282
  dir,
279
283
  manifest,
280
284
  checksumsPath,
281
285
  embedded,
286
+ format,
282
287
  });
283
288
  const rules = ignoreOwnSignature(ignoreOwnFile(await loadFileRules({ exclude: parseExclude(exclude) }), dir, checksumsPath), dir, checksumsPath);
284
289
  copies.paths.forEach((copy) => ignoreOwnFile(rules, dir, join(dir, copy)));
@@ -338,17 +343,25 @@ Options:
338
343
  builds for a release, instead of <source-dir>
339
344
  --root-folder <name> Folder in the ZIP (default: the one folder that
340
345
  every entry is in)
341
- --embedded <path> Where the manifest and its .sig are shipped inside the
342
- files, like includes/checksums.json. Both the
343
- --checksums file and its .sig must exist, both
346
+ --embedded <path> Where the manifest and its signature are shipped in
347
+ the files, like includes/checksums.json. Both the
348
+ --checksums file and its signature must exist, both
344
349
  copies must be shipped, and they must be identical
345
350
  to them. Without this option, a file with the name
346
- of the --checksums file at the root (and its .sig)
347
- is a copy only when it exists, the matching original
348
- exists and the manifest doesn't list it
349
- --public-key <key> Base64 Ed25519 public key. Checks <checksums>.sig, as
350
- written by "wp-release sign", before comparing any
351
- file, and fails when it's missing or invalid
351
+ of the --checksums file at the root (and its
352
+ signature) is a copy only when it exists, the
353
+ matching original exists and the manifest doesn't
354
+ list it
355
+ --public-key <key> Ed25519 public key, as base64, a minisign public key
356
+ or an ssh-ed25519 public key. Checks the signature
357
+ of the manifest, as written by "wp-release sign",
358
+ before comparing any file, and fails when it's
359
+ missing or invalid
360
+ --signature-format <name>
361
+ Format of the manifest signature: wordpress
362
+ (<checksums>.sig), minisign (<checksums>.minisig)
363
+ or ssh (<checksums>.sshsig) (default: wordpress).
364
+ Also selects the signature copy for --embedded
352
365
  --exclude <patterns> Gitignore-style patterns to skip in both the directory
353
366
  and the manifest, comma-separated or repeated
354
367
 
@@ -361,7 +374,7 @@ Unlike "checksums" and "zip", verify doesn't read .distignore, so that an
361
374
  ignore file inside the checked directory can't hide added files. Use
362
375
  --exclude to skip files that aren't part of the release.
363
376
 
364
- A manifest can't list itself. A copy of the manifest and of its .sig inside
377
+ A manifest can't list itself. A copy of the manifest and of its signature in
365
378
  the checked files is therefore skipped, but it must be identical to the
366
379
  --checksums file. A path that the manifest lists is never skipped, and a
367
380
  file that isn't a matching copy is reported as extra. See --embedded for
@@ -372,6 +385,7 @@ where the copies are looked for.`,
372
385
  'root-folder': { type: 'string' },
373
386
  embedded: { type: 'string' },
374
387
  'public-key': { type: 'string' },
388
+ 'signature-format': { type: 'string' },
375
389
  ...sourceDirArgs,
376
390
  exclude: { type: 'string', multiple: true },
377
391
  },
@@ -389,8 +403,12 @@ where the copies are looked for.`,
389
403
  }
390
404
  const checksumsPath = resolve(cwd, checksums);
391
405
  const publicKey = getString(values, 'public-key');
406
+ const format = getString(values, 'signature-format') ?? DEFAULT_SIGNATURE_FORMAT;
407
+ if (!isSignatureFormat(format)) {
408
+ throw new ValidationError(`Unknown signature format ${format}, use ${Object.keys(SIGNATURE_FORMATS).join(', ')}.`);
409
+ }
392
410
  if (publicKey) {
393
- await checkSignature(checksumsPath, publicKey);
411
+ await checkSignature(checksumsPath, publicKey, format);
394
412
  logger.info(`Signature of ${checksumsPath} is valid.`);
395
413
  }
396
414
  const manifest = await readChecksums(checksumsPath);
@@ -407,6 +425,7 @@ where the copies are looked for.`,
407
425
  checksumsPath,
408
426
  exclude: getStrings(values, 'exclude'),
409
427
  embedded: getString(values, 'embedded'),
428
+ format,
410
429
  logger,
411
430
  });
412
431
  }