@wpelevator/wp-release 0.4.0 → 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 +71 -8
- package/dist/cli.js +51 -12
- package/dist/commands/checksums.js +2 -0
- package/dist/commands/sign.js +2 -0
- package/dist/commands/verify.js +153 -58
- package/dist/commands/version-check.js +5 -2
- package/dist/commands/zip.js +7 -7
- package/dist/errors.d.ts +18 -0
- package/dist/errors.js +8 -0
- package/dist/github-actions-writer.d.ts +52 -0
- package/dist/github-actions-writer.js +94 -0
- package/dist/index.d.ts +9 -4
- package/dist/index.js +5 -3
- package/dist/logger.d.ts +88 -16
- package/dist/logger.js +73 -25
- package/dist/package-meta.d.ts +26 -0
- package/dist/package-meta.js +65 -10
- package/dist/report.d.ts +22 -0
- package/dist/report.js +35 -0
- package/dist/summary.d.ts +141 -0
- package/dist/summary.js +287 -0
- package/dist/svn-deploy.d.ts +2 -1
- package/dist/svn-deploy.js +8 -6
- package/dist/version-check.d.ts +39 -1
- package/dist/version-check.js +59 -5
- package/package.json +1 -1
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`](
|
|
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).
|
|
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`](
|
|
45
|
-
| Create a checksums manifest | `wp-release checksums` | [`wpelevator/wp-release-checksums`](
|
|
46
|
-
| Sign files | `wp-release sign` | [`wpelevator/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`](
|
|
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
|
|
@@ -134,7 +168,7 @@ wp-release verify wp-content/plugins/my-plugin --checksums my-plugin.1.2.0.check
|
|
|
134
168
|
- Manifest paths outside the package folder (absolute or with `..`) are rejected.
|
|
135
169
|
- `--zip <file|url>` checks the files of a ZIP instead of a directory. The ZIP can be a URL, such as the one that WordPress.org builds for a release, and its single root folder is found automatically (or set `--root-folder`). The ZIP is extracted to a temporary directory after the same safety checks as `deploy-svn --from-zip`.
|
|
136
170
|
- The version in the manifest must match the `Version` header of the checked files, so that an older release with its own valid manifest can't pass as a newer one.
|
|
137
|
-
- A manifest can't list itself. A copy of the manifest and of its `.sig` inside the checked files, at the root with the same names as the `--checksums` file or at the path of `--embedded`, is skipped when comparing files, but it must be identical to the `--checksums` file. See [Manifest inside the plugin](#manifest-inside-the-plugin).
|
|
171
|
+
- A manifest can't list itself. A copy of the manifest and of its `.sig` inside the checked files, at the root with the same names as the `--checksums` file or at the path of `--embedded`, is skipped when comparing files, but it must be identical to the `--checksums` file and its `.sig`. A path that the manifest lists is never skipped, and a file that isn't a matching copy is reported as not in the checksums. See [Manifest inside the plugin](#manifest-inside-the-plugin).
|
|
138
172
|
|
|
139
173
|
A checksums manifest proves only that the files match the manifest. With `--public-key <key>`, `verify` first checks `<checksums>.sig` against the base64 Ed25519 public key and fails with exit code `2` before comparing any file when the signature is missing or invalid, so that the manifest itself can be trusted. See [Sign](#sign).
|
|
140
174
|
|
|
@@ -158,12 +192,14 @@ wp-release deploy-svn build --trunk --svn-tag 1.2.0
|
|
|
158
192
|
--checksums build/checksums.json --public-key "$PUBLIC_KEY"
|
|
159
193
|
```
|
|
160
194
|
|
|
161
|
-
- The manifest can have any name and location, for example `--output build/includes/signatures/checksums.json`, and missing folders are created. Tell `verify` where the shipped copy is with `--embedded <path>`, like `--embedded includes/signatures/checksums.json`.
|
|
195
|
+
- The manifest can have any name and location, for example `--output build/includes/signatures/checksums.json`, and missing folders are created. Tell `verify` where the shipped copy is with `--embedded <path>`, like `--embedded includes/signatures/checksums.json`. Both the `--checksums` file and its `.sig` must then exist, both copies must be shipped, and they're skipped when comparing files and must be identical to the originals. The manifest can't list the embedded path. Without `--embedded`, `verify` looks for the manifest name at the root, and skips a file only when it exists, the matching original exists and the manifest doesn't list it. Otherwise it's checked like any other file, so a listed file that has the name of the manifest can't be deleted unnoticed, and a shipped `.sig` without an original is reported as not in the checksums.
|
|
162
196
|
|
|
163
197
|
The public key has to come from somewhere other than the plugin that it checks, because someone who can overwrite the plugin files can also replace a key and a signature inside them. A copy of the manifest also doesn't prevent a downgrade to an older release that was signed too, so the installing side has to enforce a minimum version.
|
|
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
|
|
30
|
-
-v, --verbose
|
|
31
|
-
-q, --quiet
|
|
32
|
-
--
|
|
33
|
-
|
|
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
|
-
|
|
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
|
|
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
|
};
|
package/dist/commands/sign.js
CHANGED
|
@@ -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
|
};
|
package/dist/commands/verify.js
CHANGED
|
@@ -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";
|
|
@@ -99,33 +101,20 @@ function getEmbeddedPath(checksumsPath, embedded) {
|
|
|
99
101
|
(isAbsolute(embedded) ||
|
|
100
102
|
embedded.includes('\\') ||
|
|
101
103
|
path.startsWith('../') ||
|
|
102
|
-
'.'
|
|
104
|
+
['.', '..'].includes(path))) {
|
|
103
105
|
throw new ValidationError(`--embedded must be a path inside the plugin with forward slashes, got "${embedded}".`);
|
|
104
106
|
}
|
|
105
107
|
return embedded || path.endsWith('.json') ? path : null;
|
|
106
108
|
}
|
|
107
109
|
/**
|
|
108
|
-
*
|
|
109
|
-
* directory with the given ones. A release that ships the manifest and its
|
|
110
|
-
* signature with the plugin files must ship the same bytes that were
|
|
111
|
-
* signed, otherwise a verifier on the site would check something else. The
|
|
112
|
-
* copies are not part of the manifest, since a manifest can't list itself,
|
|
113
|
-
* so they're skipped when comparing the files.
|
|
110
|
+
* Get the manifest and signature pairs to look for inside the checked files.
|
|
114
111
|
*
|
|
115
|
-
*
|
|
116
|
-
*
|
|
117
|
-
*
|
|
118
|
-
*
|
|
119
|
-
* @param input Directory, manifest and embedded path.
|
|
120
|
-
* @param input.dir Directory to check.
|
|
121
|
-
* @param input.checksumsPath Manifest path.
|
|
122
|
-
* @param input.embedded Explicit path of the embedded manifest.
|
|
123
|
-
* @return The copies that were found to differ or be missing, and their paths.
|
|
112
|
+
* @param checksumsPath Manifest path.
|
|
113
|
+
* @param path Relative path of the embedded manifest, if any.
|
|
114
|
+
* @return Pairs of the originals and their copies.
|
|
124
115
|
*/
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
const path = getEmbeddedPath(checksumsPath, embedded);
|
|
128
|
-
const pairs = path
|
|
116
|
+
function getCopyPairs(checksumsPath, path) {
|
|
117
|
+
return path
|
|
129
118
|
? [
|
|
130
119
|
{ original: checksumsPath, copy: path },
|
|
131
120
|
{
|
|
@@ -134,30 +123,124 @@ async function checkEmbeddedCopies(input) {
|
|
|
134
123
|
},
|
|
135
124
|
]
|
|
136
125
|
: [];
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
126
|
+
}
|
|
127
|
+
/**
|
|
128
|
+
* Check that an explicit --embedded path can be verified: the manifest can't
|
|
129
|
+
* list its own copies, and the originals to compare with must exist.
|
|
130
|
+
*
|
|
131
|
+
* @param pairs Pairs of the originals and their copies.
|
|
132
|
+
* @param manifest Checksums manifest.
|
|
133
|
+
* @throws {ValidationError} If a copy is listed or an original is missing.
|
|
134
|
+
*/
|
|
135
|
+
function checkExplicitPairs(pairs, manifest) {
|
|
136
|
+
pairs.forEach(({ original, copy }) => {
|
|
137
|
+
if (Object.hasOwn(manifest.files, copy)) {
|
|
138
|
+
throw new ValidationError(`The checksums list ${copy}, but a manifest can't list its own embedded copy.`);
|
|
139
|
+
}
|
|
140
|
+
if (!existsSync(original)) {
|
|
141
|
+
throw new ValidationError(`--embedded needs ${original} to compare ${copy} with, but it doesn't exist.`);
|
|
142
|
+
}
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
/**
|
|
146
|
+
* Check that two paths are the same file or have the same bytes.
|
|
147
|
+
*
|
|
148
|
+
* @param original Path of the original.
|
|
149
|
+
* @param file Path of the copy.
|
|
150
|
+
* @return Whether the contents are identical.
|
|
151
|
+
*/
|
|
152
|
+
async function hasSameContents(original, file) {
|
|
153
|
+
return (resolve(original) === resolve(file) ||
|
|
154
|
+
(await readFile(original)).equals(await readFile(file)));
|
|
155
|
+
}
|
|
156
|
+
/**
|
|
157
|
+
* Compare the copies of the manifest and its signature inside the checked
|
|
158
|
+
* directory with the given ones. A release that ships the manifest and its
|
|
159
|
+
* signature with the plugin files must ship the same bytes that were
|
|
160
|
+
* signed, otherwise a verifier on the site would check something else. The
|
|
161
|
+
* copies are not part of the manifest, since a manifest can't list itself,
|
|
162
|
+
* so they're skipped when comparing the files.
|
|
163
|
+
*
|
|
164
|
+
* With an explicit path both originals and both copies must exist. Without
|
|
165
|
+
* one, the copies are expected at the root with the name of the manifest,
|
|
166
|
+
* and a file is treated as a copy only when it exists, the matching
|
|
167
|
+
* original exists and the manifest doesn't list it. Any other file is
|
|
168
|
+
* checked like the rest of the files.
|
|
169
|
+
*
|
|
170
|
+
* @param input Directory, manifest and embedded path.
|
|
171
|
+
* @param input.dir Directory to check.
|
|
172
|
+
* @param input.manifest Checksums manifest.
|
|
173
|
+
* @param input.checksumsPath Manifest path.
|
|
174
|
+
* @param input.embedded Explicit path of the embedded manifest.
|
|
175
|
+
* @return The copies that were found to differ or be missing, and the paths of the existing copies.
|
|
176
|
+
* @throws {ValidationError} If an explicit path can't be verified.
|
|
177
|
+
*/
|
|
178
|
+
async function checkEmbeddedCopies(input) {
|
|
179
|
+
const { dir, manifest, checksumsPath, embedded } = input;
|
|
180
|
+
const pairs = getCopyPairs(checksumsPath, getEmbeddedPath(checksumsPath, embedded));
|
|
181
|
+
if (embedded) {
|
|
182
|
+
checkExplicitPairs(pairs, manifest);
|
|
183
|
+
}
|
|
184
|
+
const found = pairs.filter(({ original, copy }) => existsSync(join(dir, copy)) &&
|
|
185
|
+
(embedded ||
|
|
186
|
+
(existsSync(original) &&
|
|
187
|
+
!Object.hasOwn(manifest.files, copy))));
|
|
188
|
+
const results = await Promise.all(found.map(async ({ original, copy }) => ({
|
|
189
|
+
copy,
|
|
190
|
+
isSame: await hasSameContents(original, join(dir, copy)),
|
|
191
|
+
})));
|
|
151
192
|
return {
|
|
152
|
-
paths:
|
|
193
|
+
paths: found.map(({ copy }) => copy),
|
|
153
194
|
differing: results
|
|
154
|
-
.filter((
|
|
155
|
-
.map((
|
|
156
|
-
missing:
|
|
157
|
-
|
|
158
|
-
|
|
195
|
+
.filter(({ isSame }) => !isSame)
|
|
196
|
+
.map(({ copy }) => copy),
|
|
197
|
+
missing: embedded
|
|
198
|
+
? pairs
|
|
199
|
+
.filter(({ copy }) => !existsSync(join(dir, copy)))
|
|
200
|
+
.map(({ copy }) => copy)
|
|
201
|
+
: [],
|
|
159
202
|
};
|
|
160
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
|
+
}
|
|
161
244
|
/**
|
|
162
245
|
* Compare the version in the manifest with the Version header of the
|
|
163
246
|
* checked files, so that an older signed release can't pass as a newer one.
|
|
@@ -193,6 +276,7 @@ async function verifyTarget(input) {
|
|
|
193
276
|
const { dir } = target;
|
|
194
277
|
const copies = await checkEmbeddedCopies({
|
|
195
278
|
dir,
|
|
279
|
+
manifest,
|
|
196
280
|
checksumsPath,
|
|
197
281
|
embedded,
|
|
198
282
|
});
|
|
@@ -211,25 +295,32 @@ async function verifyTarget(input) {
|
|
|
211
295
|
!copies.differing.length &&
|
|
212
296
|
!copies.missing.length &&
|
|
213
297
|
!versionMismatch;
|
|
214
|
-
changed.
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
if (versionMismatch) {
|
|
218
|
-
logger.error(`The checksums are for version ${versionMismatch.manifest}, but the files are version ${versionMismatch.files}.`);
|
|
219
|
-
}
|
|
220
|
-
logger.info(`${matched.length} files match, ${changed.length} changed, ${missing.length} missing and ${extra.length} extra.`);
|
|
221
|
-
return {
|
|
222
|
-
exitCode: valid ? 0 : 2,
|
|
223
|
-
result: {
|
|
224
|
-
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, {
|
|
225
301
|
checksumsPath,
|
|
226
302
|
matched: matched.length,
|
|
303
|
+
}));
|
|
304
|
+
}
|
|
305
|
+
else {
|
|
306
|
+
logProblems(logger, 'wp-release verify', getVerifyProblems(dir, {
|
|
227
307
|
changed,
|
|
228
308
|
missing,
|
|
229
309
|
extra,
|
|
230
310
|
versionMismatch,
|
|
231
|
-
},
|
|
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,
|
|
232
322
|
};
|
|
323
|
+
return { exitCode: valid ? 0 : 2, result };
|
|
233
324
|
}
|
|
234
325
|
export default {
|
|
235
326
|
name: 'verify',
|
|
@@ -248,11 +339,13 @@ Options:
|
|
|
248
339
|
--root-folder <name> Folder in the ZIP (default: the one folder that
|
|
249
340
|
every entry is in)
|
|
250
341
|
--embedded <path> Where the manifest and its .sig are shipped inside the
|
|
251
|
-
files, like includes/checksums.json
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
342
|
+
files, like includes/checksums.json. Both the
|
|
343
|
+
--checksums file and its .sig must exist, both
|
|
344
|
+
copies must be shipped, and they must be identical
|
|
345
|
+
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
|
|
256
349
|
--public-key <key> Base64 Ed25519 public key. Checks <checksums>.sig, as
|
|
257
350
|
written by "wp-release sign", before comparing any
|
|
258
351
|
file, and fails when it's missing or invalid
|
|
@@ -270,7 +363,9 @@ ignore file inside the checked directory can't hide added files. Use
|
|
|
270
363
|
|
|
271
364
|
A manifest can't list itself. A copy of the manifest and of its .sig inside
|
|
272
365
|
the checked files is therefore skipped, but it must be identical to the
|
|
273
|
-
--checksums file.
|
|
366
|
+
--checksums file. A path that the manifest lists is never skipped, and a
|
|
367
|
+
file that isn't a matching copy is reported as extra. See --embedded for
|
|
368
|
+
where the copies are looked for.`,
|
|
274
369
|
options: {
|
|
275
370
|
checksums: { type: 'string' },
|
|
276
371
|
zip: { type: 'string' },
|
|
@@ -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
|
-
|
|
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,
|
package/dist/commands/zip.js
CHANGED
|
@@ -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
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
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
|
};
|