@wpelevator/wp-release 0.4.1 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,10 +1,10 @@
1
1
  # WP Release
2
2
 
3
- Package, checksum, verify and version-check WordPress plugins and themes, and deploy plugins to the WordPress.org plugin SVN repository. Usable as a CLI, as a library, and through the [`wp-release-deploy-svn`](../../actions/wp-release-deploy-svn) GitHub Action.
3
+ Package, checksum, verify and version-check WordPress plugins and themes, and deploy plugins to the WordPress.org plugin SVN repository. Usable as a CLI, as a library, and through the [`wp-release-zip`](https://github.com/wpelevator/wp-release-zip), [`wp-release-checksums`](https://github.com/wpelevator/wp-release-checksums), [`wp-release-sign`](https://github.com/wpelevator/wp-release-sign) and [`wp-release-deploy-svn`](https://github.com/wpelevator/wp-release-deploy-svn) GitHub Actions.
4
4
 
5
5
  Themes are released by uploading a ZIP on WordPress.org, so `deploy-svn` is for plugins only; use `zip` to build the theme upload.
6
6
 
7
- **Status:** first release (0.x). All commands are implemented and tested against local SVN repositories, but `deploy-svn` hasn't been used for a WordPress.org deploy yet, so try it with `--dry-run` first. Signatures are tested against libsodium, but haven't been used for a real release yet.
7
+ **Status:** first release (0.x). Signatures are tested against libsodium, but haven't been used for a real release yet.
8
8
 
9
9
  Option names follow the existing community tools (10up WordPress.org actions, `wp dist-archive`, `wp-scripts plugin-zip`, `grunt-wp-deploy`) so that migrating needs few or no renames. See [Comparison with existing tools](#comparison-with-existing-tools) for the differences.
10
10
 
@@ -35,18 +35,47 @@ npx wp-release --help
35
35
 
36
36
  Run `npx wp-release <command> --help` for the options of each command. Every command reads the plugin or theme directory from the first argument or `--source-dir`, defaulting to the current directory, and prints its result as JSON with `--json`.
37
37
 
38
+ ### GitHub Actions annotations and job summaries
39
+
40
+ With the `github` log format, the CLI uses current GitHub Actions features to report problems where you look for them, instead of only in the log. `--log-format <name>` picks how the CLI logs, separately from the result that `--json` prints:
41
+
42
+ | Log format | Output |
43
+ |---|---|
44
+ | `text` | Plain text. The default outside GitHub Actions. |
45
+ | `github` | GitHub Actions annotations and job summaries. The default when `GITHUB_ACTIONS=true`. `--ci` is a shorthand for it. |
46
+
47
+ Pass `--log-format text` for plain text in GitHub Actions. The GitHub Actions always use the `github` log format. Only the CLI checks `GITHUB_ACTIONS`: the library classes don't read the environment, and choose the format by the [writer](#logging) they get.
48
+
49
+ Errors, warnings and notices are printed as [workflow commands](https://docs.github.com/en/actions/reference/workflows-and-actions/workflow-commands), so they show up as annotations on the run and on the files of a pull request:
50
+
51
+ - `version-check` and `deploy-svn` annotate each version mismatch on the file and line of the version: the `Version` header, the readme `Stable tag` or the PHP constant.
52
+ - `verify` annotates each changed file and each file that isn't in the checksums.
53
+
54
+ Each command also adds a section to the [job summary](https://docs.github.com/en/actions/reference/workflows-and-actions/workflow-commands#adding-a-job-summary) of the run, when `GITHUB_STEP_SUMMARY` is set:
55
+
56
+ | Command | Summary |
57
+ |---|---|
58
+ | `deploy-svn` | Plugin, linked to its WordPress.org page, version, written SVN paths, the commit linked to its WordPress.org changeset (or the dry run or no-op result), and the SVN changes in a collapsed table. |
59
+ | `zip` | ZIP, number of files and checksums manifest. |
60
+ | `checksums` | Manifest and number of files. |
61
+ | `sign` | Signature files and the public key. |
62
+ | `verify` | That all files match, or the counts and a table of the changed, missing and extra files. |
63
+ | `version-check` | The matching version, or a table of the mismatches with their files and lines. |
64
+
65
+ A failed command adds its error to the summary, or a table of its problems with their files and lines, listing up to 100 and counting the rest. The Markdown of the summaries isn't part of the library API. File paths are relative to `GITHUB_WORKSPACE`, or the current directory without it. Files outside it, like those of an extracted ZIP, get an annotation without a file. Multi-line messages stay one annotation.
66
+
38
67
  ### Naming convention
39
68
 
40
69
  `@wpelevator/wp-release` is the suite of release tools and `wp-release` is its CLI. "Release" is the whole workflow; each command is one operation in it. Commands are flat: a single word or dashed words like `version-check` and `deploy-svn`, without command groups or aliases. Each GitHub Action is named `wp-release-` followed by the command name, so the CLI command and the `uses:` name always match:
41
70
 
42
71
  | Capability | CLI command | GitHub Action |
43
72
  |---|---|---|
44
- | Build a ZIP | `wp-release zip` | [`wpelevator/wp-release-zip`](../../actions/wp-release-zip) |
45
- | Create a checksums manifest | `wp-release checksums` | [`wpelevator/wp-release-checksums`](../../actions/wp-release-checksums) |
46
- | Sign files | `wp-release sign` | [`wpelevator/wp-release-sign`](../../actions/wp-release-sign) |
73
+ | Build a ZIP | `wp-release zip` | [`wpelevator/wp-release-zip`](https://github.com/wpelevator/wp-release-zip) |
74
+ | Create a checksums manifest | `wp-release checksums` | [`wpelevator/wp-release-checksums`](https://github.com/wpelevator/wp-release-checksums) |
75
+ | Sign files | `wp-release sign` | [`wpelevator/wp-release-sign`](https://github.com/wpelevator/wp-release-sign) |
47
76
  | Verify files against a manifest | `wp-release verify` | |
48
77
  | Check that the versions agree | `wp-release version-check` | |
49
- | Deploy to WordPress.org SVN | `wp-release deploy-svn` | [`wpelevator/wp-release-deploy-svn`](../../actions/wp-release-deploy-svn) |
78
+ | Deploy to WordPress.org SVN | `wp-release deploy-svn` | [`wpelevator/wp-release-deploy-svn`](https://github.com/wpelevator/wp-release-deploy-svn) |
50
79
 
51
80
  Each concept has one name, written in the syntax of each interface:
52
81
 
@@ -59,6 +88,7 @@ Each concept has one name, written in the syntax of each interface:
59
88
  | SVN tag to create | `--svn-tag <version>` | `svn-tag` | `svnTag` | Tag written by `deploy-svn`, distinct from a git tag. |
60
89
  | SVN repository | `--svn-url <url>` | `svn-url` | `svnUrl` | Deploy destination. |
61
90
  | SVN credentials | `--svn-username <name>`, `SVN_USERNAME` and `SVN_PASSWORD` env vars | `svn-username`, `svn-password` | `svnUsername`, `svnPassword` | Needed only to commit. There's no password flag, since it would show in the process list; the action inputs and library options fall back to the env vars. |
91
+ | Log format | `--log-format <text\|github>`, `--ci` for `github` | always `github` | `writer: new TextWriter()` or `new GitHubActionsWriter()` | How the log is printed: plain text, or GitHub Actions annotations and job summaries. Each name matches the start of its writer class. |
62
92
  | Assets sync | `--no-assets` to skip | `assets: false` to skip | `assets` | Booleans are positive in the library and the action; `--no-assets` is only the CLI negation, never `noAssets` or a `no-assets` input. |
63
93
 
64
94
  CLI JSON results use camelCase keys like `zipPath`, and GitHub Action outputs use kebab-case like `zip-path`. Library classes are named after the capability (`SvnDeploy`, `Svn`) without repeating "release".
@@ -83,6 +113,8 @@ The commands read the same headers as WordPress. The main plugin file is the top
83
113
 
84
114
  ### ZIP
85
115
 
116
+ The [`wp-release-zip`](https://github.com/wpelevator/wp-release-zip) GitHub Action runs this command.
117
+
86
118
  `zip` writes `{slug}.{version}.zip` to the current directory, or to `--output <file>`, with the selected files in a `{slug}/` root folder:
87
119
 
88
120
  ```bash
@@ -97,6 +129,8 @@ wp-release zip dist --slug my-plugin --checksums
97
129
 
98
130
  ### Checksums
99
131
 
132
+ The [`wp-release-checksums`](https://github.com/wpelevator/wp-release-checksums) GitHub Action runs this command.
133
+
100
134
  `checksums` writes a manifest with the MD5 and SHA-256 hash of every selected file, in the format of the WordPress.org plugin checksums API (`https://downloads.wordpress.org/plugin-checksums/{slug}/{version}.json`). Paths are relative to the plugin or theme folder, sorted, and use forward slashes:
101
135
 
102
136
  ```json
@@ -164,6 +198,8 @@ The public key has to come from somewhere other than the plugin that it checks,
164
198
 
165
199
  ### Sign
166
200
 
201
+ The [`wp-release-sign`](https://github.com/wpelevator/wp-release-sign) GitHub Action runs this command.
202
+
167
203
  `sign` writes a detached signature next to each file, `<file>.sig`, so that downloads and installed copies can be trusted. Sign both the ZIP and the checksums manifest: the ZIP signature covers installs and updates, and the manifest signature covers the contents after unpacking and copies that weren't built by you.
168
204
 
169
205
  ```bash
@@ -192,6 +228,8 @@ wp-release version-check --constant MY_PLUGIN_VERSION --git-tag v1.2.0
192
228
 
193
229
  ### Deploy to SVN
194
230
 
231
+ The [`wp-release-deploy-svn`](https://github.com/wpelevator/wp-release-deploy-svn) GitHub Action runs this command.
232
+
195
233
  `deploy-svn` syncs a plugin to the WordPress.org plugin SVN repository. Each SVN write is a separate flag and nothing is written unless requested. All writes go into one commit.
196
234
 
197
235
  | Flag | Default | Writes |
@@ -240,6 +278,7 @@ Validation:
240
278
  - `--version` defaults to `--svn-tag`, then the `Version` header of the deployed files; when `--version` and `--svn-tag` are both set they must match.
241
279
  - Pre-release versions like `1.0.0-rc.1` can be tagged, but syncing one to `trunk/` warns, since trunk is for the development version.
242
280
  - `--trunk` without `--svn-tag` works but warns, because trunk then differs from the stable tag that WordPress.org serves downloads from.
281
+ - A version mismatch throws a `VersionMismatchError`, a `ValidationError` with the `mismatches` and a problem for each, with its file and line. See [Logging](#logging).
243
282
 
244
283
  ### Exit codes
245
284
 
@@ -271,6 +310,30 @@ const { committed, revision, version, changes } = await new SvnDeploy(
271
310
 
272
311
  `resolveDeployOptions()` applies the defaults and throws a `ValidationError` (with `exitCode` `2`) for invalid combinations. `SvnDeploy` exposes each step (`prepare()`, `checkVersions()`, `checkout()`, `sync()`, `commit()` and `cleanup()`) separately so CI wrappers can run and report them one by one; `run()` runs all of them, or nothing when there's nothing to write. `getPlan()` lists the planned SVN changes. The `Svn` class wraps the svn command line, and takes the command runner as a dependency so that tests can replace it.
273
312
 
313
+ ### Logging
314
+
315
+ `Logger` filters messages by level and passes them to a writer, like the transports of other Node.js loggers. The writer decides how messages look:
316
+
317
+ - `TextWriter` prints plain text to `stdout` and `stderr`. It's the default.
318
+ - `GitHubActionsWriter` prints [workflow commands](#github-actions-annotations-and-job-summaries), so that notices, warnings and errors with a file are annotations, and appends summaries to `GITHUB_STEP_SUMMARY`. It's the CLI's `github` log format, and the GitHub Actions pass it to their logger. Its `workspace` and `summaryPath` options default to `GITHUB_WORKSPACE` and `GITHUB_STEP_SUMMARY`.
319
+
320
+ ```js
321
+ import { GitHubActionsWriter, Logger, logError } from '@wpelevator/wp-release';
322
+
323
+ const logger = new Logger( { writer: new GitHubActionsWriter(), verbose: true } );
324
+
325
+ logger.error( 'Version header is 1.2.0, expected 1.3.0.', {
326
+ file: 'my-plugin.php',
327
+ line: 4,
328
+ title: 'Version mismatch',
329
+ } );
330
+ logger.summary( ( { workspace } ) => `### Checked ${ workspace }` );
331
+ ```
332
+
333
+ A writer implements `LogWriter` with `write()`, `startGroup()`, `endGroup()` and `summary()`, so other CI systems can have their own format. `summary()` takes a function that builds the Markdown, which only writers with a summary call.
334
+
335
+ A `ValidationError` can list `problems`, each with a `message` and an optional `file`, `line` and `title`. `logError( logger, error, title )` logs each problem as an error on its file and line and lists them in the summary under the title, or logs the message when there are none. `VersionMismatchError` and `version-check` report the version mismatches this way.
336
+
274
337
  ZIP files and metadata:
275
338
 
276
339
  ```js
package/dist/cli.js CHANGED
@@ -5,8 +5,11 @@ import sign from "./commands/sign.js";
5
5
  import verify from "./commands/verify.js";
6
6
  import versionCheck from "./commands/version-check.js";
7
7
  import deploySvn from "./commands/deploy-svn.js";
8
- import { Logger } from "./logger.js";
9
- import { getBoolean } from "./commands/command.js";
8
+ import { Logger, TextWriter } from "./logger.js";
9
+ import { getBoolean, getString } from "./commands/command.js";
10
+ import { ValidationError } from "./errors.js";
11
+ import { GitHubActionsWriter } from "./github-actions-writer.js";
12
+ import { logError } from "./report.js";
10
13
  export const EXIT_SUCCESS = 0;
11
14
  export const EXIT_FAILURE = 1;
12
15
  export const EXIT_INVALID = 2;
@@ -22,15 +25,19 @@ const globalArgs = {
22
25
  help: { type: 'boolean', short: 'h' },
23
26
  verbose: { type: 'boolean', short: 'v' },
24
27
  quiet: { type: 'boolean', short: 'q' },
28
+ 'log-format': { type: 'string' },
25
29
  ci: { type: 'boolean' },
26
30
  json: { type: 'boolean' },
27
31
  };
28
32
  const globalOptions = `Global options:
29
- -h, --help Show help for the command
30
- -v, --verbose Print more output
31
- -q, --quiet Print errors only
32
- --ci Use GitHub Actions annotations (default when GITHUB_ACTIONS=true)
33
- --json Print the result as JSON`;
33
+ -h, --help Show help for the command
34
+ -v, --verbose Print more output
35
+ -q, --quiet Print errors only
36
+ --log-format <name> Log format: text, or github for GitHub Actions
37
+ annotations and job summaries (default: github
38
+ when GITHUB_ACTIONS=true, otherwise text)
39
+ --ci Same as --log-format github
40
+ --json Print the result as JSON`;
34
41
  /**
35
42
  * Get the top-level help text listing all commands.
36
43
  *
@@ -92,6 +99,40 @@ function getErrorExitCode(error) {
92
99
  }
93
100
  return EXIT_FAILURE;
94
101
  }
102
+ /**
103
+ * Log writers by log format name, each created for the CLI streams. The
104
+ * names match the writer classes: text is TextWriter and github is
105
+ * GitHubActionsWriter.
106
+ */
107
+ const logFormats = {
108
+ text: (streams) => new TextWriter(streams),
109
+ github: (streams) => new GitHubActionsWriter(streams),
110
+ };
111
+ /**
112
+ * Create the log writer of the log format: --log-format, github with --ci, or
113
+ * github in GitHub Actions (GITHUB_ACTIONS=true) and text elsewhere. Only the
114
+ * CLI reads the environment, so the library itself stays CI-agnostic.
115
+ *
116
+ * @param values Parsed option values.
117
+ * @param streams Output streams.
118
+ * @return Log writer.
119
+ * @throws {ValidationError} For an unknown log format, or --ci with another log format.
120
+ */
121
+ function createLogWriter(values, streams) {
122
+ const ci = getBoolean(values, 'ci');
123
+ const detected = 'true' === process.env.GITHUB_ACTIONS || ci ? 'github' : 'text';
124
+ const logFormat = getString(values, 'log-format') ?? detected;
125
+ const create = Object.hasOwn(logFormats, logFormat)
126
+ ? logFormats[logFormat]
127
+ : undefined;
128
+ if (!create) {
129
+ throw new ValidationError(`Unknown log format "${logFormat}". Use ${Object.keys(logFormats).join(' or ')}.`);
130
+ }
131
+ if (ci && 'github' !== logFormat) {
132
+ throw new ValidationError(`--ci is the same as --log-format github and can't be combined with --log-format ${logFormat}.`);
133
+ }
134
+ return create(streams);
135
+ }
95
136
  /**
96
137
  * Parse the arguments of a command and run it.
97
138
  *
@@ -107,7 +148,7 @@ function getErrorExitCode(error) {
107
148
  */
108
149
  async function runCommand(command, args, io) {
109
150
  const { stdout, stderr, cwd } = io;
110
- let logger = new Logger({ stdout, stderr });
151
+ let logger = new Logger({ writer: new TextWriter({ stdout, stderr }) });
111
152
  try {
112
153
  const { values, positionals } = parseArgs({
113
154
  args,
@@ -115,11 +156,9 @@ async function runCommand(command, args, io) {
115
156
  allowPositionals: true,
116
157
  });
117
158
  logger = new Logger({
118
- ci: getBoolean(values, 'ci'),
159
+ writer: createLogWriter(values, { stdout, stderr }),
119
160
  quiet: getBoolean(values, 'quiet') || getBoolean(values, 'json'),
120
161
  verbose: getBoolean(values, 'verbose'),
121
- stdout,
122
- stderr,
123
162
  });
124
163
  const { exitCode, result } = await command.run({
125
164
  values,
@@ -133,7 +172,7 @@ async function runCommand(command, args, io) {
133
172
  return exitCode;
134
173
  }
135
174
  catch (error) {
136
- logger.error(error instanceof Error ? error.message : String(error));
175
+ logError(logger, error, `wp-release ${command.name}`);
137
176
  return getErrorExitCode(error);
138
177
  }
139
178
  }
@@ -3,6 +3,7 @@ import { CHECKSUMS_EXTENSION, createChecksums, writeChecksums, } from "../checks
3
3
  import { loadFileRules, resolveFileSelection } from "../file-selection.js";
4
4
  import { FILENAME_FORMAT, formatFilename } from "../filename.js";
5
5
  import { readPackageMeta } from "../package-meta.js";
6
+ import { getChecksumsSummary } from "../summary.js";
6
7
  import { checkDirectory, fileSelectionArgs, getFileSelectionInput, getSourceDirArg, ignoreOwnFile, ignoreOwnSignature, resolveOutput, sourceDirArgs, } from "./args.js";
7
8
  import { getBoolean, getString } from "./command.js";
8
9
  export default {
@@ -71,6 +72,7 @@ matches the ZIP contents.`,
71
72
  await writeChecksums(checksumsPath, manifest, getBoolean(values, 'force'));
72
73
  logger.debug(`Hashed ${files} files in ${sourceDir}.`);
73
74
  logger.info(checksumsPath);
75
+ logger.summary((context) => getChecksumsSummary(context, { checksumsPath, files }));
74
76
  return { exitCode: 0, result: { checksumsPath, files } };
75
77
  },
76
78
  };
@@ -1,6 +1,7 @@
1
1
  import { resolve } from 'node:path';
2
2
  import { stat } from 'node:fs/promises';
3
3
  import { ValidationError } from "../errors.js";
4
+ import { getSignSummary } from "../summary.js";
4
5
  import { SIGNING_KEY_ENV, getPublicKey, parseSigningKey, writeSignature, } from "../signing.js";
5
6
  import { getBoolean } from "./command.js";
6
7
  export default {
@@ -50,6 +51,7 @@ that belongs to the signing key.`,
50
51
  logger.info(signaturePath);
51
52
  signatures.push(signaturePath);
52
53
  }
54
+ logger.summary((context) => getSignSummary(context, { signatures, publicKey }));
53
55
  return { exitCode: 0, result: { signatures, publicKey } };
54
56
  },
55
57
  };
@@ -7,6 +7,8 @@ import { downloadFile, isUrl } from "../download.js";
7
7
  import { loadFileRules, parseExclude } from "../file-selection.js";
8
8
  import { ValidationError } from "../errors.js";
9
9
  import { readPackageMeta } from "../package-meta.js";
10
+ import { logProblems } from "../report.js";
11
+ import { getVerifySummary } from "../summary.js";
10
12
  import { extractZip, readZipEntries } from "../unzip.js";
11
13
  import { SIGNATURE_EXTENSION, parsePublicKey, verifyFileSignature, } from "../signing.js";
12
14
  import { checkDirectory, getSourceDirArg, ignoreOwnFile, ignoreOwnSignature, sourceDirArgs, } from "./args.js";
@@ -199,6 +201,46 @@ async function checkEmbeddedCopies(input) {
199
201
  : [],
200
202
  };
201
203
  }
204
+ /**
205
+ * Get the problems of a verify run. Changed files and files that aren't in
206
+ * the checksums are reported on the file. Missing files have no file to
207
+ * report on.
208
+ *
209
+ * @param dir Checked directory.
210
+ * @param result Differences between the files and the manifest.
211
+ * @param result.changed Changed files.
212
+ * @param result.missing Missing files.
213
+ * @param result.extra Files that aren't in the checksums.
214
+ * @param result.versionMismatch Manifest and file versions when they differ.
215
+ * @return Problems.
216
+ */
217
+ function getVerifyProblems(dir, result) {
218
+ const { changed, missing, extra, versionMismatch } = result;
219
+ return [
220
+ ...changed.map((file) => ({
221
+ message: `Changed: ${file}`,
222
+ file: join(dir, file),
223
+ title: 'Changed file',
224
+ })),
225
+ ...missing.map((file) => ({
226
+ message: `Missing: ${file}`,
227
+ title: 'Missing file',
228
+ })),
229
+ ...extra.map((file) => ({
230
+ message: `Not in the checksums: ${file}`,
231
+ file: join(dir, file),
232
+ title: 'File not in the checksums',
233
+ })),
234
+ ...(versionMismatch
235
+ ? [
236
+ {
237
+ message: `The checksums are for version ${versionMismatch.manifest}, but the files are version ${versionMismatch.files}.`,
238
+ title: 'Version mismatch',
239
+ },
240
+ ]
241
+ : []),
242
+ ];
243
+ }
202
244
  /**
203
245
  * Compare the version in the manifest with the Version header of the
204
246
  * checked files, so that an older signed release can't pass as a newer one.
@@ -253,25 +295,32 @@ async function verifyTarget(input) {
253
295
  !copies.differing.length &&
254
296
  !copies.missing.length &&
255
297
  !versionMismatch;
256
- changed.forEach((file) => logger.error(`Changed: ${file}`));
257
- missing.forEach((file) => logger.error(`Missing: ${file}`));
258
- extra.forEach((file) => logger.error(`Not in the checksums: ${file}`));
259
- if (versionMismatch) {
260
- logger.error(`The checksums are for version ${versionMismatch.manifest}, but the files are version ${versionMismatch.files}.`);
261
- }
262
- logger.info(`${matched.length} files match, ${changed.length} changed, ${missing.length} missing and ${extra.length} extra.`);
263
- return {
264
- exitCode: valid ? 0 : 2,
265
- result: {
266
- valid,
298
+ const counts = `${matched.length} files match, ${changed.length} changed, ${missing.length} missing and ${extra.length} extra.`;
299
+ if (valid) {
300
+ logger.summary((context) => getVerifySummary(context, {
267
301
  checksumsPath,
268
302
  matched: matched.length,
303
+ }));
304
+ }
305
+ else {
306
+ logProblems(logger, 'wp-release verify', getVerifyProblems(dir, {
269
307
  changed,
270
308
  missing,
271
309
  extra,
272
310
  versionMismatch,
273
- },
311
+ }), counts);
312
+ }
313
+ logger.info(counts);
314
+ const result = {
315
+ valid,
316
+ checksumsPath,
317
+ matched: matched.length,
318
+ changed,
319
+ missing,
320
+ extra,
321
+ versionMismatch,
274
322
  };
323
+ return { exitCode: valid ? 0 : 2, result };
275
324
  }
276
325
  export default {
277
326
  name: 'verify',
@@ -2,7 +2,9 @@ import { basename, resolve } from 'node:path';
2
2
  import { isTagRef } from "../deploy-options.js";
3
3
  import { ValidationError } from "../errors.js";
4
4
  import { readPackageMeta } from "../package-meta.js";
5
- import { GIT_TAG_FORMAT, findVersionMismatches } from "../version-check.js";
5
+ import { GIT_TAG_FORMAT, findVersionMismatches, getVersionMismatchProblems, } from "../version-check.js";
6
+ import { logProblems } from "../report.js";
7
+ import { getVersionCheckSummary } from "../summary.js";
6
8
  import { checkDirectory, getSourceDirArg, sourceDirArgs } from "./args.js";
7
9
  import { getBoolean, getString, getStrings } from "./command.js";
8
10
  /**
@@ -68,9 +70,10 @@ no Stable tag, so it's only checked for plugins.`,
68
70
  constants: getStrings(values, 'constant'),
69
71
  stableTag: !getBoolean(values, 'no-stable-tag'),
70
72
  });
71
- mismatches.forEach((mismatch) => logger.error(mismatch.message));
73
+ logProblems(logger, 'wp-release version-check', getVersionMismatchProblems(sourceDir, mismatches));
72
74
  if (!mismatches.length) {
73
75
  logger.info(`All versions match ${version}.`);
76
+ logger.summary(() => getVersionCheckSummary(version));
74
77
  }
75
78
  return {
76
79
  exitCode: mismatches.length ? 2 : 0,
@@ -5,6 +5,7 @@ import { ValidationError } from "../errors.js";
5
5
  import { loadFileRules, resolveFileSelection, selectFiles, } from "../file-selection.js";
6
6
  import { FILENAME_FORMAT, formatFilename } from "../filename.js";
7
7
  import { readPackageMeta } from "../package-meta.js";
8
+ import { getZipSummary } from "../summary.js";
8
9
  import { createZip } from "../zip.js";
9
10
  import { checkDirectory, fileSelectionArgs, getFileSelectionInput, getSourceDirArg, ignoreOwnFile, ignoreOwnSignature, resolveOutput, sourceDirArgs, } from "./args.js";
10
11
  import { getBoolean, getString } from "./command.js";
@@ -126,13 +127,12 @@ always give the same ZIP.`,
126
127
  await writeChecksums(checksumsPath, manifest, force);
127
128
  logger.info(checksumsPath);
128
129
  }
129
- return {
130
- exitCode: 0,
131
- result: {
132
- zipPath,
133
- ...(checksumsPath ? { checksumsPath } : {}),
134
- files: files.length,
135
- },
130
+ const result = {
131
+ zipPath,
132
+ ...(checksumsPath ? { checksumsPath } : {}),
133
+ files: files.length,
136
134
  };
135
+ logger.summary((context) => getZipSummary(context, result));
136
+ return { exitCode: 0, result };
137
137
  },
138
138
  };
package/dist/errors.d.ts CHANGED
@@ -1,9 +1,27 @@
1
+ /**
2
+ * One failed check, with the file and line it's about when there is one.
3
+ */
4
+ export interface Problem {
5
+ /** Description of the problem. */
6
+ message: string;
7
+ /** File the problem is in, absolute or relative to the working directory. */
8
+ file?: string | null;
9
+ /** Line in the file, starting at 1. */
10
+ line?: number | null;
11
+ /** Short title of the kind of problem, like "Version mismatch". */
12
+ title?: string;
13
+ }
1
14
  /**
2
15
  * Invalid options or failed release checks, reported with exit code 2.
16
+ * Failed checks can list their problems, so that each one is reported on its
17
+ * own file and line.
3
18
  */
4
19
  export declare class ValidationError extends Error {
5
20
  name: string;
6
21
  exitCode: number;
22
+ /** Each failed check, empty when the message is all there is. */
23
+ readonly problems: Problem[];
24
+ constructor(message: string, problems?: Problem[]);
7
25
  }
8
26
  /**
9
27
  * Missing SVN credentials or a failed login or permission check, reported
package/dist/errors.js CHANGED
@@ -1,9 +1,17 @@
1
1
  /**
2
2
  * Invalid options or failed release checks, reported with exit code 2.
3
+ * Failed checks can list their problems, so that each one is reported on its
4
+ * own file and line.
3
5
  */
4
6
  export class ValidationError extends Error {
5
7
  name = 'ValidationError';
6
8
  exitCode = 2;
9
+ /** Each failed check, empty when the message is all there is. */
10
+ problems;
11
+ constructor(message, problems = []) {
12
+ super(message);
13
+ this.problems = problems;
14
+ }
7
15
  }
8
16
  /**
9
17
  * Missing SVN credentials or a failed login or permission check, reported
@@ -0,0 +1,52 @@
1
+ import type { Annotation, LogEntry, LogWriter, OutputStream, SummaryBuilder } from './logger.ts';
2
+ export interface GitHubActionsWriterOptions {
3
+ /** Stream for regular output. */
4
+ stdout?: OutputStream;
5
+ /** Stream for warnings and errors. */
6
+ stderr?: OutputStream;
7
+ /**
8
+ * Directory that annotation and summary paths are relative to, defaults
9
+ * to GITHUB_WORKSPACE or the working directory.
10
+ */
11
+ workspace?: string;
12
+ /**
13
+ * File that summaries are appended to, defaults to GITHUB_STEP_SUMMARY.
14
+ * Null turns summaries off.
15
+ */
16
+ summaryPath?: string | null;
17
+ }
18
+ /**
19
+ * Writer that prints GitHub Actions workflow commands, so that notices,
20
+ * warnings and errors are annotations on the run and on the files they're
21
+ * about, and appends summaries to the job summary.
22
+ */
23
+ export declare class GitHubActionsWriter implements LogWriter {
24
+ readonly stdout: OutputStream;
25
+ readonly stderr: OutputStream;
26
+ readonly workspace: string;
27
+ readonly summaryPath: string | null;
28
+ constructor({ stdout, stderr, workspace, summaryPath, }?: GitHubActionsWriterOptions);
29
+ write({ level, message, annotation }: LogEntry): void;
30
+ startGroup(title: string): void;
31
+ endGroup(): void;
32
+ summary(build: SummaryBuilder): void;
33
+ /**
34
+ * Format a workflow command like
35
+ * "::error file=my-plugin.php,line=3,title=Version mismatch::message".
36
+ *
37
+ * @param name Command name.
38
+ * @param message Message.
39
+ * @param annotation File, line and title of the annotation.
40
+ * @return Workflow command with a line break.
41
+ */
42
+ formatCommand(name: string, message: string, annotation?: Annotation): string;
43
+ /**
44
+ * Get the path of an annotation file relative to the workspace. GitHub
45
+ * only links files of the repository, so files outside the workspace,
46
+ * like an extracted ZIP in a temporary directory, get no file.
47
+ *
48
+ * @param file File path, absolute or relative to the working directory.
49
+ * @return Path with forward slashes, or null when it's outside the workspace.
50
+ */
51
+ getAnnotationFile(file?: string | null): string | null;
52
+ }
@@ -0,0 +1,94 @@
1
+ import { appendFileSync } from 'node:fs';
2
+ import { getWorkspacePath } from "./summary.js";
3
+ /**
4
+ * Escape the message of a workflow command, so that a multi-line message
5
+ * stays one annotation.
6
+ *
7
+ * @param text Message.
8
+ * @return Escaped message.
9
+ */
10
+ function escapeData(text) {
11
+ return text
12
+ .replaceAll('%', '%25')
13
+ .replaceAll('\r', '%0D')
14
+ .replaceAll('\n', '%0A');
15
+ }
16
+ /**
17
+ * Escape a workflow command property like a file path or title.
18
+ *
19
+ * @param text Property value.
20
+ * @return Escaped value.
21
+ */
22
+ function escapeProperty(text) {
23
+ return escapeData(text).replaceAll(':', '%3A').replaceAll(',', '%2C');
24
+ }
25
+ /**
26
+ * Writer that prints GitHub Actions workflow commands, so that notices,
27
+ * warnings and errors are annotations on the run and on the files they're
28
+ * about, and appends summaries to the job summary.
29
+ */
30
+ export class GitHubActionsWriter {
31
+ stdout;
32
+ stderr;
33
+ workspace;
34
+ summaryPath;
35
+ constructor({ stdout = process.stdout, stderr = process.stderr, workspace = process.env.GITHUB_WORKSPACE || process.cwd(), summaryPath = process.env.GITHUB_STEP_SUMMARY || null, } = {}) {
36
+ this.stdout = stdout;
37
+ this.stderr = stderr;
38
+ this.workspace = workspace;
39
+ this.summaryPath = summaryPath;
40
+ }
41
+ write({ level, message, annotation }) {
42
+ const stream = 'warning' === level || 'error' === level
43
+ ? this.stderr
44
+ : this.stdout;
45
+ stream.write('info' === level
46
+ ? `${message}\n`
47
+ : this.formatCommand(level, message, annotation));
48
+ }
49
+ startGroup(title) {
50
+ this.stdout.write(`::group::${escapeData(title)}\n`);
51
+ }
52
+ endGroup() {
53
+ this.stdout.write('::endgroup::\n');
54
+ }
55
+ summary(build) {
56
+ if (this.summaryPath) {
57
+ // A blank line keeps the Markdown of separate summaries apart.
58
+ appendFileSync(this.summaryPath, `${build({ workspace: this.workspace }).trimEnd()}\n\n`);
59
+ }
60
+ }
61
+ /**
62
+ * Format a workflow command like
63
+ * "::error file=my-plugin.php,line=3,title=Version mismatch::message".
64
+ *
65
+ * @param name Command name.
66
+ * @param message Message.
67
+ * @param annotation File, line and title of the annotation.
68
+ * @return Workflow command with a line break.
69
+ */
70
+ formatCommand(name, message, annotation = {}) {
71
+ const file = this.getAnnotationFile(annotation.file);
72
+ const properties = [
73
+ file ? `file=${escapeProperty(file)}` : '',
74
+ file && annotation.line ? `line=${annotation.line}` : '',
75
+ annotation.title
76
+ ? `title=${escapeProperty(annotation.title)}`
77
+ : '',
78
+ ]
79
+ .filter(Boolean)
80
+ .join(',');
81
+ return `::${name}${properties ? ` ${properties}` : ''}::${escapeData(message)}\n`;
82
+ }
83
+ /**
84
+ * Get the path of an annotation file relative to the workspace. GitHub
85
+ * only links files of the repository, so files outside the workspace,
86
+ * like an extracted ZIP in a temporary directory, get no file.
87
+ *
88
+ * @param file File path, absolute or relative to the working directory.
89
+ * @return Path with forward slashes, or null when it's outside the workspace.
90
+ */
91
+ getAnnotationFile(file) {
92
+ return file ? getWorkspacePath(this.workspace, file) : null;
93
+ }
94
+ }