@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
package/dist/cli.js CHANGED
@@ -1,3 +1,4 @@
1
+ import { randomUUID } from 'node:crypto';
1
2
  import { appendFileSync } from 'node:fs';
2
3
  import { parseArgs } from 'node:util';
3
4
  import zip from "./commands/zip.js";
@@ -43,6 +44,7 @@ const globalArgs = {
43
44
  json: { type: 'boolean' },
44
45
  'github-output': { type: 'boolean' },
45
46
  config: { type: 'string' },
47
+ 'tag-format': { type: 'string' },
46
48
  };
47
49
  const globalOptions = `Global options:
48
50
  -h, --help Show help for the command
@@ -55,7 +57,32 @@ const globalOptions = `Global options:
55
57
  --json Print the result as JSON
56
58
  --github-output Set the result as JSON to the "result" output of
57
59
  the GitHub Actions step
58
- --config <path> Path to a JavaScript release config file`;
60
+ --config <path> Path to a JavaScript release config file
61
+ --tag-format <format>
62
+ Tag format with {name}, {path} and {version} that
63
+ replaces the tagFormat of the config, like
64
+ {path}/{version}`;
65
+ /**
66
+ * Get the config file and the tag format from the global options.
67
+ *
68
+ * @param values Parsed CLI values.
69
+ * @return The --config and --tag-format values.
70
+ * @throws {ValidationError} For an empty --config or --tag-format.
71
+ */
72
+ function getConfigArgs(values) {
73
+ const configFile = getString(values, 'config');
74
+ const tagFormat = getString(values, 'tag-format');
75
+ if ('' === configFile?.trim()) {
76
+ throw new ValidationError('--config <path> must not be empty.');
77
+ }
78
+ if ('' === tagFormat?.trim()) {
79
+ throw new ValidationError('--tag-format <format> must not be empty.');
80
+ }
81
+ return {
82
+ ...(configFile ? { configFile } : {}),
83
+ ...(tagFormat ? { tagFormat } : {}),
84
+ };
85
+ }
59
86
  /**
60
87
  * Get the top-level help text listing all commands.
61
88
  *
@@ -151,6 +178,28 @@ function createLogWriter(values, streams) {
151
178
  }
152
179
  return create(streams);
153
180
  }
181
+ /**
182
+ * Format the result of a command as GitHub Actions step outputs: the whole
183
+ * result as JSON in "result", and each top-level string, number or boolean
184
+ * as its own output, like "zipPath", so that a workflow reads it without
185
+ * fromJSON(). Lists and objects are only in "result". Every value but the
186
+ * compact JSON, which has no line breaks, uses a random delimiter, so that a
187
+ * value with line breaks can't set other outputs.
188
+ *
189
+ * @param result Command result.
190
+ * @return Lines for the GITHUB_OUTPUT file.
191
+ */
192
+ export function formatGitHubOutputs(result) {
193
+ const values = 'object' === typeof result && null !== result
194
+ ? Object.entries(result).filter(([name, value]) => 'result' !== name &&
195
+ ['string', 'number', 'boolean'].includes(typeof value))
196
+ : [];
197
+ const delimiter = `ghadelimiter_${randomUUID()}`;
198
+ return [
199
+ `result=${JSON.stringify(result)}\n`,
200
+ ...values.map(([name, value]) => `${name}<<${delimiter}\n${String(value)}\n${delimiter}\n`),
201
+ ].join('');
202
+ }
154
203
  /**
155
204
  * Get the GitHub Actions step output file for --github-output, checked before
156
205
  * the command runs.
@@ -188,10 +237,7 @@ async function runCommand(command, args, io) {
188
237
  options: { ...globalArgs, ...command.options },
189
238
  allowPositionals: true,
190
239
  });
191
- const configFile = getString(values, 'config');
192
- if ('' === configFile?.trim()) {
193
- throw new ValidationError('--config <path> must not be empty.');
194
- }
240
+ const { configFile, tagFormat } = getConfigArgs(values);
195
241
  const githubOutputFile = getGitHubOutputFile(values);
196
242
  logger = new Logger({
197
243
  writer: createLogWriter(values, { stdout, stderr }),
@@ -208,6 +254,7 @@ async function runCommand(command, args, io) {
208
254
  positionals,
209
255
  cwd,
210
256
  ...(configFile ? { configFile } : {}),
257
+ ...(tagFormat ? { tagFormat } : {}),
211
258
  logger,
212
259
  ...(interactive
213
260
  ? { terminal: { input: stdin, output: stdout } }
@@ -217,8 +264,7 @@ async function runCommand(command, args, io) {
217
264
  stdout.write(JSON.stringify(result, null, '\t') + '\n');
218
265
  }
219
266
  if (githubOutputFile) {
220
- // Compact JSON has no line breaks, so it fits a single output line.
221
- appendFileSync(githubOutputFile, `result=${JSON.stringify(result)}\n`);
267
+ appendFileSync(githubOutputFile, formatGitHubOutputs(result));
222
268
  }
223
269
  return exitCode;
224
270
  }
@@ -238,6 +284,7 @@ async function runCommand(command, args, io) {
238
284
  * @param context.cwd Working directory that relative paths resolve against.
239
285
  * @param context.configFile Explicit config file path, when supplied by a wrapper.
240
286
  * @param context.logger Logger for the command output.
287
+ * @param context.svnRunner Optional SVN command runner supplied by a wrapper.
241
288
  * @return Exit code and result of the command.
242
289
  * @throws {Error} If the command is unknown, the arguments are invalid or the command fails.
243
290
  */
@@ -251,16 +298,16 @@ export async function execute(name, args, context) {
251
298
  options: { ...globalArgs, ...command.options },
252
299
  allowPositionals: true,
253
300
  });
254
- const configFile = getString(values, 'config') ?? context.configFile;
255
- if ('' === configFile?.trim()) {
256
- throw new ValidationError('--config <path> must not be empty.');
257
- }
301
+ const configArgs = getConfigArgs(values);
302
+ const configFile = configArgs.configFile ?? context.configFile;
303
+ const { tagFormat } = configArgs;
258
304
  // The command table is keyed by name, so the result matches CommandResults.
259
305
  return command.run({
260
306
  values,
261
307
  positionals,
262
308
  ...context,
263
309
  ...(configFile ? { configFile } : {}),
310
+ ...(tagFormat ? { tagFormat } : {}),
264
311
  });
265
312
  }
266
313
  /**
@@ -1,5 +1,5 @@
1
1
  import type { ParseArgsOptionsConfig } from 'node:util';
2
- import type { FileRules } from '../file-selection.ts';
2
+ import type { SignatureFormat } from '../signing.ts';
3
3
  import type { ParsedValues } from './command.ts';
4
4
  /**
5
5
  * Options shared by the commands that select files with .distignore and
@@ -40,26 +40,6 @@ export declare function checkDirectory(path: string): Promise<void>;
40
40
  * @return Whether the path is a directory.
41
41
  */
42
42
  export declare function isDirectory(path: string): Promise<boolean>;
43
- /**
44
- * Ignore a file that the command writes or reads, such as the checksums
45
- * manifest, when it's inside the selected directory.
46
- *
47
- * @param rules Matcher from loadFileRules().
48
- * @param sourceDir Selected directory.
49
- * @param path Absolute file path.
50
- * @return The same matcher.
51
- */
52
- export declare function ignoreOwnFile(rules: FileRules, sourceDir: string, path: string): FileRules;
53
- /**
54
- * Ignore the signature file that "wp-release sign" writes next to a file the
55
- * command writes, when it's inside the selected directory.
56
- *
57
- * @param rules Matcher from loadFileRules().
58
- * @param sourceDir Selected directory.
59
- * @param path Absolute path of the signed file.
60
- * @return The same matcher.
61
- */
62
- export declare function ignoreOwnSignature(rules: FileRules, sourceDir: string, path: string): FileRules;
63
43
  /**
64
44
  * Option for the source directory, which can also be the first positional
65
45
  * argument.
@@ -90,3 +70,13 @@ export interface ResolveOutputInput {
90
70
  * @return Absolute file path.
91
71
  */
92
72
  export declare function resolveOutput(input: ResolveOutputInput): string;
73
+ /**
74
+ * Get the signature formats from the --format values of sign or the
75
+ * --sign-format values of zip, which can be repeated
76
+ * or comma-separated.
77
+ *
78
+ * @param values Option values.
79
+ * @return Unique formats, the default when none are given.
80
+ * @throws {ValidationError} If a format is unknown.
81
+ */
82
+ export declare function parseSignatureFormats(values: string[]): SignatureFormat[];
@@ -1,7 +1,7 @@
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";
4
+ import { DEFAULT_SIGNATURE_FORMAT, SIGNATURE_FORMATS, isSignatureFormat, } from "../signing.js";
5
5
  import { getBoolean, getString, getStrings } from "./command.js";
6
6
  /**
7
7
  * Options shared by the commands that select files with .distignore and
@@ -63,39 +63,6 @@ export async function isDirectory(path) {
63
63
  const stats = await stat(path).catch(() => null);
64
64
  return Boolean(stats?.isDirectory());
65
65
  }
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
66
  /**
100
67
  * Option for the source directory, which can also be the first positional
101
68
  * argument.
@@ -129,3 +96,25 @@ export function getSourceDirArg(values, positionals) {
129
96
  export function resolveOutput(input) {
130
97
  return resolve(input.cwd, input.output ?? input.filename);
131
98
  }
99
+ /**
100
+ * Get the signature formats from the --format values of sign or the
101
+ * --sign-format values of zip, which can be repeated
102
+ * or comma-separated.
103
+ *
104
+ * @param values Option values.
105
+ * @return Unique formats, the default when none are given.
106
+ * @throws {ValidationError} If a format is unknown.
107
+ */
108
+ export function parseSignatureFormats(values) {
109
+ const names = values
110
+ .flatMap((value) => value.split(','))
111
+ .map((name) => name.trim())
112
+ .filter(Boolean);
113
+ const unknown = names.filter((name) => !isSignatureFormat(name));
114
+ if (unknown.length) {
115
+ throw new ValidationError(`Unknown signature format ${unknown.join(', ')}, use ${Object.keys(SIGNATURE_FORMATS).join(', ')}.`);
116
+ }
117
+ return names.length
118
+ ? [...new Set(names.filter(isSignatureFormat))]
119
+ : [DEFAULT_SIGNATURE_FORMAT];
120
+ }
@@ -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',
@@ -1,6 +1,7 @@
1
1
  import type { ParseArgsOptionsConfig } from 'node:util';
2
2
  import type { PromptTerminal } from '../changeset-prompt.ts';
3
3
  import type { Logger } from '../logger.ts';
4
+ import type { SvnRunner } from '../svn.ts';
4
5
  /**
5
6
  * Parsed option values from node:util parseArgs().
6
7
  */
@@ -14,8 +15,12 @@ export interface CommandContext {
14
15
  cwd: string;
15
16
  /** Explicit JavaScript release config path from --config, if supplied. */
16
17
  configFile?: string;
18
+ /** Tag format from --tag-format, which replaces the tagFormat of the config. */
19
+ tagFormat?: string;
17
20
  /** Logger for the command output. */
18
21
  logger: Logger;
22
+ /** SVN command runner supplied by a wrapper, instead of spawning svn directly. */
23
+ svnRunner?: SvnRunner;
19
24
  /** Terminal for interactive prompts, only when the CLI runs in one and prints no JSON. */
20
25
  terminal?: PromptTerminal;
21
26
  }
@@ -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)
@@ -1,5 +1,14 @@
1
1
  import type { DeployInput } from '../deploy-options.ts';
2
- import type { ParsedValues } from './command.ts';
2
+ import type { DeployResult } from '../svn-deploy.ts';
3
+ import type { PackageDeployPlanItem } from '../package-deploy.ts';
4
+ import type { CommandOutcome, ParsedValues } from './command.ts';
5
+ import type { PackageDeployOutcome } from './package-deploys.ts';
6
+ /** Results of a single deploy, a configured plan or configured deploys. */
7
+ export type DeploySvnResult = DeployResult | {
8
+ packages: PackageDeployPlanItem[];
9
+ } | {
10
+ deploys: PackageDeployOutcome<DeployResult>[];
11
+ };
3
12
  export interface DeployArgsInput {
4
13
  /** Parsed CLI values. */
5
14
  values: ParsedValues;
@@ -76,19 +85,6 @@ declare const _default: {
76
85
  type: "string";
77
86
  };
78
87
  };
79
- run({ values, positionals, cwd, configFile, logger }: import("./command.ts").CommandContext): Promise<{
80
- exitCode: number;
81
- result: {
82
- packages: import("../package-deploy.ts").PackageDeployPlanItem[];
83
- };
84
- } | {
85
- exitCode: number;
86
- result: {
87
- deploys: import("./package-deploys.ts").PackageDeployOutcome<import("../svn-deploy.ts").DeployResult>[];
88
- };
89
- } | {
90
- exitCode: number;
91
- result: import("../svn-deploy.ts").DeployResult;
92
- }>;
88
+ run({ values, positionals, cwd, configFile, tagFormat, logger, svnRunner, }: import("./command.ts").CommandContext): Promise<CommandOutcome<DeploySvnResult>>;
93
89
  };
94
90
  export default _default;
@@ -1,6 +1,7 @@
1
1
  import { resolveDeployOptions } from "../deploy-options.js";
2
2
  import { getSvnDeploys } from "../package-deploy.js";
3
3
  import { SvnDeploy } from "../svn-deploy.js";
4
+ import { Svn } from "../svn.js";
4
5
  import { fileSelectionArgs, getFileSelectionInput, getSourceDirArg, sourceDirArgs, } from "./args.js";
5
6
  import { getBoolean, getString } from "./command.js";
6
7
  import { getPackageDeployArgs, listPackageDeploys, packageDeployArgs, runPackageDeploys, } from "./package-deploys.js";
@@ -23,6 +24,26 @@ const TARGET_FLAGS = [
23
24
  'svn-url',
24
25
  'message',
25
26
  ];
27
+ /**
28
+ * Run an SVN deploy with the optional command runner supplied by a wrapper.
29
+ *
30
+ * @param input Deploy input.
31
+ * @param logger Logger for the deploy.
32
+ * @param runner Optional SVN command runner.
33
+ * @return Deploy result.
34
+ */
35
+ async function runDeploy(input, logger, runner) {
36
+ const options = resolveDeployOptions(input);
37
+ const svn = runner
38
+ ? new Svn({
39
+ runner,
40
+ username: options.svnUsername,
41
+ password: options.svnPassword,
42
+ logger,
43
+ })
44
+ : undefined;
45
+ return new SvnDeploy(options, logger, svn).run();
46
+ }
26
47
  /**
27
48
  * Map the parsed "deploy-svn" arguments to resolveDeployOptions() input.
28
49
  * Flags that aren't passed stay undefined, so that the same defaults apply
@@ -135,7 +156,7 @@ Examples:
135
156
  ...packageDeployArgs,
136
157
  'dry-run': { type: 'boolean' },
137
158
  },
138
- async run({ values, positionals, cwd, configFile, logger }) {
159
+ async run({ values, positionals, cwd, configFile, tagFormat, logger, svnRunner, }) {
139
160
  const { ref, packages, list } = getPackageDeployArgs(values, positionals, TARGET_FLAGS);
140
161
  if (ref) {
141
162
  const deploys = await getSvnDeploys({
@@ -144,17 +165,17 @@ Examples:
144
165
  packages,
145
166
  dryRun: getBoolean(values, 'dry-run'),
146
167
  ...(configFile ? { configFile } : {}),
168
+ ...(tagFormat ? { tagFormat } : {}),
147
169
  });
148
170
  return list
149
171
  ? listPackageDeploys(deploys, logger, ref)
150
- : runPackageDeploys(deploys, (input) => new SvnDeploy(resolveDeployOptions({
172
+ : runPackageDeploys(deploys, (input) => runDeploy({
151
173
  ...input,
152
174
  svnUsername: getString(values, 'svn-username'),
153
175
  force: getBoolean(values, 'force'),
154
- }), logger).run(), logger, ref);
176
+ }, logger, svnRunner), logger, ref);
155
177
  }
156
- const options = resolveDeployOptions(getDeployInputFromArgs({ values, positionals, cwd }));
157
- const result = await new SvnDeploy(options, logger).run();
178
+ const result = await runDeploy(getDeployInputFromArgs({ values, positionals, cwd }), logger, svnRunner);
158
179
  return { exitCode: 0, result };
159
180
  },
160
181
  };
@@ -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,10 +1,15 @@
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
  }
9
14
  declare const _default: {
10
15
  name: string;
@@ -12,6 +17,10 @@ declare const _default: {
12
17
  summary: string;
13
18
  help: string;
14
19
  options: {
20
+ format: {
21
+ type: "string";
22
+ multiple: true;
23
+ };
15
24
  force: {
16
25
  type: "boolean";
17
26
  };