@wpelevator/wp-release 0.3.0 → 0.4.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 +55 -8
- package/dist/checksums.js +3 -2
- package/dist/cli.d.ts +30 -1
- package/dist/cli.js +32 -0
- package/dist/commands/args.d.ts +10 -0
- package/dist/commands/args.js +13 -0
- package/dist/commands/checksums.d.ts +9 -7
- package/dist/commands/checksums.js +3 -3
- package/dist/commands/command.d.ts +2 -2
- package/dist/commands/deploy-svn.js +5 -2
- package/dist/commands/sign.d.ts +21 -0
- package/dist/commands/sign.js +55 -0
- package/dist/commands/verify.d.ts +14 -11
- package/dist/commands/verify.js +289 -27
- package/dist/commands/zip.d.ts +11 -8
- package/dist/commands/zip.js +4 -4
- package/dist/download.d.ts +15 -0
- package/dist/download.js +27 -0
- package/dist/index.d.ts +7 -2
- package/dist/index.js +3 -1
- package/dist/signing.d.ts +69 -0
- package/dist/signing.js +140 -0
- package/dist/svn-deploy.d.ts +12 -14
- package/dist/svn-deploy.js +44 -27
- package/package.json +5 -3
package/README.md
CHANGED
|
@@ -4,7 +4,7 @@ Package, checksum, verify and version-check WordPress plugins and themes, and de
|
|
|
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.
|
|
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.
|
|
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
|
|
|
@@ -28,7 +28,8 @@ npx wp-release --help
|
|
|
28
28
|
|---|---|
|
|
29
29
|
| `wp-release zip [<source-dir>] [options]` | Create a ZIP archive with a `{slug}/` root folder, respecting `.distignore`. |
|
|
30
30
|
| `wp-release checksums [<source-dir>] [options]` | Write the MD5 and SHA-256 hashes of every distributed file to a checksums manifest. |
|
|
31
|
-
| `wp-release
|
|
31
|
+
| `wp-release sign <file>... [options]` | Sign files with Ed25519, writing a detached `<file>.sig` next to each. |
|
|
32
|
+
| `wp-release verify [<source-dir>] --checksums <file> [options]` | Check a plugin or theme directory, or a ZIP, against a checksums manifest. |
|
|
32
33
|
| `wp-release version-check [<source-dir>] [options]` | Check that the `Version` header, readme `Stable tag`, PHP constants and git tag agree. |
|
|
33
34
|
| `wp-release deploy-svn [<source-dir>] [options]` | Deploy a plugin to the WordPress.org plugin SVN repository. |
|
|
34
35
|
|
|
@@ -40,8 +41,9 @@ Run `npx wp-release <command> --help` for the options of each command. Every com
|
|
|
40
41
|
|
|
41
42
|
| Capability | CLI command | GitHub Action |
|
|
42
43
|
|---|---|---|
|
|
43
|
-
| Build a ZIP | `wp-release zip` | |
|
|
44
|
-
| Create a checksums manifest | `wp-release checksums` | |
|
|
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) |
|
|
45
47
|
| Verify files against a manifest | `wp-release verify` | |
|
|
46
48
|
| Check that the versions agree | `wp-release version-check` | |
|
|
47
49
|
| Deploy to WordPress.org SVN | `wp-release deploy-svn` | [`wpelevator/wp-release-deploy-svn`](../../actions/wp-release-deploy-svn) |
|
|
@@ -130,8 +132,50 @@ wp-release verify wp-content/plugins/my-plugin --checksums my-plugin.1.2.0.check
|
|
|
130
132
|
- A file matches when its SHA-256 equals the manifest. Manifests without SHA-256 fall back to MD5, and a hash can also be a list of accepted values.
|
|
131
133
|
- `verify` doesn't read `.distignore`, so that an ignore file inside the checked directory can't hide added files. `--exclude <patterns>` skips paths in both the directory and the manifest, for files that aren't part of the release, such as caches.
|
|
132
134
|
- Manifest paths outside the package folder (absolute or with `..`) are rejected.
|
|
135
|
+
- `--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
|
+
- 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).
|
|
133
138
|
|
|
134
|
-
A checksums manifest proves only that the files match the manifest.
|
|
139
|
+
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
|
+
|
|
141
|
+
### Manifest inside the plugin
|
|
142
|
+
|
|
143
|
+
A release can ship its signed manifest inside the plugin, so that an installed copy can be checked later with the public key, and also publish the same two files as release assets. Both use the same files:
|
|
144
|
+
|
|
145
|
+
```bash
|
|
146
|
+
wp-release checksums build --slug my-plugin --output build/checksums.json
|
|
147
|
+
wp-release sign build/checksums.json
|
|
148
|
+
wp-release zip build --slug my-plugin
|
|
149
|
+
wp-release deploy-svn build --trunk --svn-tag 1.2.0
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
- Write the manifest into the directory that is zipped and deployed. It lists every file except itself, and `checksums`, `zip` and `verify` also skip its `<name>.sig`, so running `checksums` again on a build that already has the pair gives the same manifest. Sign it last, since the signature covers the exact bytes of the manifest.
|
|
153
|
+
- `zip` and `deploy-svn` carry the pair like any other file. Don't use `zip --checksums` here, since it would write a second manifest that lists the first one. Attach `build/checksums.json` and `build/checksums.json.sig` to the GitHub release as they are.
|
|
154
|
+
- The signature covers file hashes and not a ZIP, so it stays valid for the ZIP that WordPress.org builds from SVN. Check what WordPress.org serves after a deploy, with the manifest and public key from your build:
|
|
155
|
+
|
|
156
|
+
```bash
|
|
157
|
+
wp-release verify --zip https://downloads.wordpress.org/plugin/my-plugin.1.2.0.zip \
|
|
158
|
+
--checksums build/checksums.json --public-key "$PUBLIC_KEY"
|
|
159
|
+
```
|
|
160
|
+
|
|
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`. The copy and its `.sig` are then skipped when comparing files, must exist, and must be identical to the `--checksums` file and its signature. Without `--embedded`, `verify` looks for the manifest name at the root and only checks it when it's there.
|
|
162
|
+
|
|
163
|
+
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
|
+
|
|
165
|
+
### Sign
|
|
166
|
+
|
|
167
|
+
`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
|
+
|
|
169
|
+
```bash
|
|
170
|
+
WP_RELEASE_SIGNING_KEY=… wp-release sign my-plugin.1.2.0.zip my-plugin.1.2.0.checksums.json
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
- The signature is the one WordPress core's `verify_file_signature()` checks: a base64 Ed25519 signature of the raw SHA-384 digest of the file. The tests check it against `sodium_crypto_sign_verify_detached()` in PHP. A `.sig` file can hold several signatures, one per line, and a file is valid when any of them match, which allows a key rotation window.
|
|
174
|
+
- The key is read from the `WP_RELEASE_SIGNING_KEY` environment variable only, as a base64 32-byte Ed25519 seed or a 64-byte libsodium secret key, the formats that `sodium_crypto_sign_seed_keypair()` and the update-pilot-server package signing key take. There's no flag for it, since it would show in the process list, and it's never logged.
|
|
175
|
+
- `--verbose` prints the public key of the signing key, which `--json` also returns as `publicKey`. Pass it to `verify --public-key` and to the plugin that checks the signatures.
|
|
176
|
+
- An existing `.sig` file isn't overwritten without `--force`.
|
|
177
|
+
|
|
178
|
+
Generating a key pair isn't part of the package yet. With PHP: `php -r 'echo base64_encode( random_bytes( 32 ) );'` gives a seed, and the public key is printed by `sign --verbose`.
|
|
135
179
|
|
|
136
180
|
### Version check
|
|
137
181
|
|
|
@@ -154,7 +198,7 @@ wp-release version-check --constant MY_PLUGIN_VERSION --git-tag v1.2.0
|
|
|
154
198
|
|---|---|---|
|
|
155
199
|
| `--trunk` | off | Sync the full source directory to `trunk/` |
|
|
156
200
|
| `--readme-only` | off | Sync only the readme to `trunk/`, for updates between releases such as bumping `Tested up to`. Can't be combined with `--svn-tag`. |
|
|
157
|
-
| `--svn-tag <version>` | off |
|
|
201
|
+
| `--svn-tag <version>` | off | Create `tags/<version>`. With `--trunk`, the tag is a copy of the synced `trunk/`. Without it, the tag is built directly from the source files and `trunk/` is left alone, for pre-release tags. A leading `v` is stripped, so `v1.2.0` creates `tags/1.2.0`. |
|
|
158
202
|
| `--assets-dir <dir>` | not set | Sync this directory with banners, icons and screenshots to the SVN `assets/` directory. There's no default directory, so assets are only deployed when this is set, and it must exist. |
|
|
159
203
|
| `--no-assets` | | Skip the `--assets-dir` sync for one run, for example when a config always passes `--assets-dir`. |
|
|
160
204
|
|
|
@@ -164,6 +208,9 @@ Typical runs:
|
|
|
164
208
|
# Deploy 1.2.0 to trunk and the tag, and update the assets
|
|
165
209
|
wp-release deploy-svn dist --trunk --svn-tag 1.2.0 --assets-dir .wordpress-org
|
|
166
210
|
|
|
211
|
+
# Tag a pre-release build without touching trunk
|
|
212
|
+
wp-release deploy-svn dist --svn-tag 1.2.0-rc.1
|
|
213
|
+
|
|
167
214
|
# Update only the readme between releases
|
|
168
215
|
wp-release deploy-svn dist --readme-only
|
|
169
216
|
|
|
@@ -177,9 +224,9 @@ wp-release deploy-svn --from-zip my-plugin.1.2.0.zip --trunk --svn-tag 1.2.0
|
|
|
177
224
|
How a deploy works:
|
|
178
225
|
|
|
179
226
|
1. The options are validated, including the slug, version and readme name, before they're used in URLs or paths.
|
|
180
|
-
2. With `--trunk`, the files must be a plugin whose `Version` header matches the version
|
|
227
|
+
2. With `--trunk` or `--svn-tag`, the files must be a plugin whose `Version` header matches the version. When `--trunk` and `--svn-tag` are combined, the readme `Stable tag` must match too; a tag-only run skips that, since a pre-release tag doesn't change the stable tag. `--readme-only` needs the readme. Nothing is checked out when a check fails.
|
|
181
228
|
3. The repository is checked out sparsely into a new temporary directory, with only the directories that the flags write to. A missing `assets/` (or `trunk/`, `tags/`) directory is created. The deploy stops when `tags/<version>` exists, unless `--force` replaces it.
|
|
182
|
-
4. The files are synced in Node.js, without `rsync`: new and changed files are copied, files that are no longer selected are deleted, and `svn add` and `svn delete` stage them, including files that svn ignores by default. `trunk/` is then copied to the tag with `svn copy
|
|
229
|
+
4. The files are synced in Node.js, without `rsync`: new and changed files are copied, files that are no longer selected are deleted, and `svn add` and `svn delete` stage them, including files that svn ignores by default. With `--trunk`, `trunk/` is then copied to the tag with `svn copy`. Without it, the same selected files are synced into `tags/<version>` instead, and with `--force` an existing tag is updated to match, deleting files that are no longer in the build.
|
|
183
230
|
5. Asset images get `svn:mime-type` set for PNG, JPEG, GIF and SVG, the formats that WordPress.org documents for plugin assets. Other formats keep the type that svn assigns.
|
|
184
231
|
6. When `svn status` is empty, the deploy ends without a commit. A dry run logs the status and stops before the commit. Otherwise the changes are committed, and the temporary directory is always removed.
|
|
185
232
|
|
package/dist/checksums.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { createReadStream } from 'node:fs';
|
|
2
|
-
import { readFile, writeFile } from 'node:fs/promises';
|
|
2
|
+
import { mkdir, readFile, writeFile } from 'node:fs/promises';
|
|
3
3
|
import { createHash } from 'node:crypto';
|
|
4
|
-
import { join } from 'node:path';
|
|
4
|
+
import { dirname, join } from 'node:path';
|
|
5
5
|
import { ValidationError } from "./errors.js";
|
|
6
6
|
import { isErrorCode, selectFiles } from "./file-selection.js";
|
|
7
7
|
export const CHECKSUMS_EXTENSION = '.checksums.json';
|
|
@@ -81,6 +81,7 @@ function isPackageType(type) {
|
|
|
81
81
|
* @throws {ValidationError} If the file exists and force isn't set.
|
|
82
82
|
*/
|
|
83
83
|
export async function writeChecksums(path, manifest, force = false) {
|
|
84
|
+
await mkdir(dirname(path), { recursive: true });
|
|
84
85
|
try {
|
|
85
86
|
await writeFile(path, JSON.stringify(manifest, null, '\t') + '\n', {
|
|
86
87
|
flag: force ? 'w' : 'wx',
|
package/dist/cli.d.ts
CHANGED
|
@@ -1,5 +1,9 @@
|
|
|
1
|
+
import type { ChecksumsResult } from './commands/checksums.ts';
|
|
2
|
+
import type { SignResult } from './commands/sign.ts';
|
|
3
|
+
import type { ZipResult } from './commands/zip.ts';
|
|
4
|
+
import { Logger } from './logger.ts';
|
|
1
5
|
import type { OutputStream } from './logger.ts';
|
|
2
|
-
import type { Command } from './commands/command.ts';
|
|
6
|
+
import type { Command, CommandOutcome } from './commands/command.ts';
|
|
3
7
|
export interface CliIo {
|
|
4
8
|
/** Stream for regular output. */
|
|
5
9
|
stdout?: OutputStream;
|
|
@@ -32,6 +36,31 @@ export declare function getCommandHelp(command: Command): string;
|
|
|
32
36
|
* @return Command definition.
|
|
33
37
|
*/
|
|
34
38
|
export declare function findCommand(args: string[]): Command | undefined;
|
|
39
|
+
/**
|
|
40
|
+
* Results of the commands that can be run with execute(), by command name.
|
|
41
|
+
*/
|
|
42
|
+
export interface CommandResults {
|
|
43
|
+
zip: ZipResult;
|
|
44
|
+
checksums: ChecksumsResult;
|
|
45
|
+
sign: SignResult;
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Run a command by name with the given arguments, for wrappers like the
|
|
49
|
+
* GitHub Actions that need the result and the thrown errors instead of an
|
|
50
|
+
* exit code.
|
|
51
|
+
*
|
|
52
|
+
* @param name Command name, such as zip.
|
|
53
|
+
* @param args Arguments after the command name.
|
|
54
|
+
* @param context Working directory and logger.
|
|
55
|
+
* @param context.cwd Working directory that relative paths resolve against.
|
|
56
|
+
* @param context.logger Logger for the command output.
|
|
57
|
+
* @return Exit code and result of the command.
|
|
58
|
+
* @throws {Error} If the command is unknown, the arguments are invalid or the command fails.
|
|
59
|
+
*/
|
|
60
|
+
export declare function execute<Name extends keyof CommandResults>(name: Name, args: string[], context: {
|
|
61
|
+
cwd: string;
|
|
62
|
+
logger: Logger;
|
|
63
|
+
}): Promise<CommandOutcome<CommandResults[Name]>>;
|
|
35
64
|
/**
|
|
36
65
|
* Run the CLI.
|
|
37
66
|
*
|
package/dist/cli.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { parseArgs } from 'node:util';
|
|
2
2
|
import zip from "./commands/zip.js";
|
|
3
3
|
import checksums from "./commands/checksums.js";
|
|
4
|
+
import sign from "./commands/sign.js";
|
|
4
5
|
import verify from "./commands/verify.js";
|
|
5
6
|
import versionCheck from "./commands/version-check.js";
|
|
6
7
|
import deploySvn from "./commands/deploy-svn.js";
|
|
@@ -12,6 +13,7 @@ export const EXIT_INVALID = 2;
|
|
|
12
13
|
export const commands = [
|
|
13
14
|
zip,
|
|
14
15
|
checksums,
|
|
16
|
+
sign,
|
|
15
17
|
verify,
|
|
16
18
|
versionCheck,
|
|
17
19
|
deploySvn,
|
|
@@ -135,6 +137,36 @@ async function runCommand(command, args, io) {
|
|
|
135
137
|
return getErrorExitCode(error);
|
|
136
138
|
}
|
|
137
139
|
}
|
|
140
|
+
/**
|
|
141
|
+
* Run a command by name with the given arguments, for wrappers like the
|
|
142
|
+
* GitHub Actions that need the result and the thrown errors instead of an
|
|
143
|
+
* exit code.
|
|
144
|
+
*
|
|
145
|
+
* @param name Command name, such as zip.
|
|
146
|
+
* @param args Arguments after the command name.
|
|
147
|
+
* @param context Working directory and logger.
|
|
148
|
+
* @param context.cwd Working directory that relative paths resolve against.
|
|
149
|
+
* @param context.logger Logger for the command output.
|
|
150
|
+
* @return Exit code and result of the command.
|
|
151
|
+
* @throws {Error} If the command is unknown, the arguments are invalid or the command fails.
|
|
152
|
+
*/
|
|
153
|
+
export async function execute(name, args, context) {
|
|
154
|
+
const command = commands.find(({ name: commandName }) => name === commandName);
|
|
155
|
+
if (!command) {
|
|
156
|
+
throw new Error(`Unknown command "${name}".`);
|
|
157
|
+
}
|
|
158
|
+
const { values, positionals } = parseArgs({
|
|
159
|
+
args,
|
|
160
|
+
options: command.options,
|
|
161
|
+
allowPositionals: true,
|
|
162
|
+
});
|
|
163
|
+
// The command table is keyed by name, so the result matches CommandResults.
|
|
164
|
+
return command.run({
|
|
165
|
+
values,
|
|
166
|
+
positionals,
|
|
167
|
+
...context,
|
|
168
|
+
});
|
|
169
|
+
}
|
|
138
170
|
/**
|
|
139
171
|
* Run the CLI.
|
|
140
172
|
*
|
package/dist/commands/args.d.ts
CHANGED
|
@@ -49,6 +49,16 @@ export declare function isDirectory(path: string): Promise<boolean>;
|
|
|
49
49
|
* @return The same matcher.
|
|
50
50
|
*/
|
|
51
51
|
export declare function ignoreOwnFile(rules: FileRules, sourceDir: string, path: string): FileRules;
|
|
52
|
+
/**
|
|
53
|
+
* Ignore the signature file that "wp-release sign" writes next to a file the
|
|
54
|
+
* command writes, when it's inside the selected directory.
|
|
55
|
+
*
|
|
56
|
+
* @param rules Matcher from loadFileRules().
|
|
57
|
+
* @param sourceDir Selected directory.
|
|
58
|
+
* @param path Absolute path of the signed file.
|
|
59
|
+
* @return The same matcher.
|
|
60
|
+
*/
|
|
61
|
+
export declare function ignoreOwnSignature(rules: FileRules, sourceDir: string, path: string): FileRules;
|
|
52
62
|
/**
|
|
53
63
|
* Option for the source directory, which can also be the first positional
|
|
54
64
|
* argument.
|
package/dist/commands/args.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { stat } from 'node:fs/promises';
|
|
2
2
|
import { isAbsolute, relative, resolve } from 'node:path';
|
|
3
3
|
import { ValidationError } from "../errors.js";
|
|
4
|
+
import { SIGNATURE_EXTENSION } from "../signing.js";
|
|
4
5
|
import { getBoolean, getString, getStrings } from "./command.js";
|
|
5
6
|
/**
|
|
6
7
|
* Options shared by the commands that select files with .distignore and
|
|
@@ -81,6 +82,18 @@ export function ignoreOwnFile(rules, sourceDir, path) {
|
|
|
81
82
|
}
|
|
82
83
|
return rules;
|
|
83
84
|
}
|
|
85
|
+
/**
|
|
86
|
+
* Ignore the signature file that "wp-release sign" writes next to a file the
|
|
87
|
+
* command writes, when it's inside the selected directory.
|
|
88
|
+
*
|
|
89
|
+
* @param rules Matcher from loadFileRules().
|
|
90
|
+
* @param sourceDir Selected directory.
|
|
91
|
+
* @param path Absolute path of the signed file.
|
|
92
|
+
* @return The same matcher.
|
|
93
|
+
*/
|
|
94
|
+
export function ignoreOwnSignature(rules, sourceDir, path) {
|
|
95
|
+
return ignoreOwnFile(rules, sourceDir, path + SIGNATURE_EXTENSION);
|
|
96
|
+
}
|
|
84
97
|
/**
|
|
85
98
|
* Option for the source directory, which can also be the first positional
|
|
86
99
|
* argument.
|
|
@@ -1,3 +1,11 @@
|
|
|
1
|
+
import type { CommandOutcome } from './command.ts';
|
|
2
|
+
/** Result of the checksums command. */
|
|
3
|
+
export interface ChecksumsResult {
|
|
4
|
+
/** Absolute path of the manifest. */
|
|
5
|
+
checksumsPath: string;
|
|
6
|
+
/** Number of hashed files. */
|
|
7
|
+
files: number;
|
|
8
|
+
}
|
|
1
9
|
declare const _default: {
|
|
2
10
|
name: string;
|
|
3
11
|
usage: string;
|
|
@@ -23,12 +31,6 @@ declare const _default: {
|
|
|
23
31
|
type: "string";
|
|
24
32
|
};
|
|
25
33
|
};
|
|
26
|
-
run({ values, positionals, cwd, logger }: import("./command.ts").CommandContext): Promise<
|
|
27
|
-
exitCode: number;
|
|
28
|
-
result: {
|
|
29
|
-
checksumsPath: string;
|
|
30
|
-
files: number;
|
|
31
|
-
};
|
|
32
|
-
}>;
|
|
34
|
+
run({ values, positionals, cwd, logger, }: import("./command.ts").CommandContext): Promise<CommandOutcome<ChecksumsResult>>;
|
|
33
35
|
};
|
|
34
36
|
export default _default;
|
|
@@ -3,7 +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 { checkDirectory, fileSelectionArgs, getFileSelectionInput, getSourceDirArg, ignoreOwnFile, resolveOutput, sourceDirArgs, } from "./args.js";
|
|
6
|
+
import { checkDirectory, fileSelectionArgs, getFileSelectionInput, getSourceDirArg, ignoreOwnFile, ignoreOwnSignature, resolveOutput, sourceDirArgs, } from "./args.js";
|
|
7
7
|
import { getBoolean, getString } from "./command.js";
|
|
8
8
|
export default {
|
|
9
9
|
name: 'checksums',
|
|
@@ -42,7 +42,7 @@ matches the ZIP contents.`,
|
|
|
42
42
|
...fileSelectionArgs,
|
|
43
43
|
force: { type: 'boolean' },
|
|
44
44
|
},
|
|
45
|
-
async run({ values, positionals, cwd, logger }) {
|
|
45
|
+
async run({ values, positionals, cwd, logger, }) {
|
|
46
46
|
const sourceDir = resolve(cwd, getSourceDirArg(values, positionals));
|
|
47
47
|
const slug = getString(values, 'slug') ?? basename(cwd);
|
|
48
48
|
await checkDirectory(sourceDir);
|
|
@@ -59,7 +59,7 @@ matches the ZIP contents.`,
|
|
|
59
59
|
sourceDir,
|
|
60
60
|
...getFileSelectionInput(values),
|
|
61
61
|
});
|
|
62
|
-
const rules = ignoreOwnFile(await loadFileRules(selection), sourceDir, checksumsPath);
|
|
62
|
+
const rules = ignoreOwnSignature(ignoreOwnFile(await loadFileRules(selection), sourceDir, checksumsPath), sourceDir, checksumsPath);
|
|
63
63
|
const manifest = await createChecksums({
|
|
64
64
|
sourceDir,
|
|
65
65
|
rules,
|
|
@@ -14,11 +14,11 @@ export interface CommandContext {
|
|
|
14
14
|
/** Logger for the command output. */
|
|
15
15
|
logger: Logger;
|
|
16
16
|
}
|
|
17
|
-
export interface CommandOutcome {
|
|
17
|
+
export interface CommandOutcome<Result = unknown> {
|
|
18
18
|
/** Process exit code. */
|
|
19
19
|
exitCode: number;
|
|
20
20
|
/** Result printed with --json. */
|
|
21
|
-
result:
|
|
21
|
+
result: Result;
|
|
22
22
|
}
|
|
23
23
|
/**
|
|
24
24
|
* CLI command definition.
|
|
@@ -44,8 +44,9 @@ export default {
|
|
|
44
44
|
Writes (none by default, assets only with --assets-dir):
|
|
45
45
|
--trunk Sync the full source directory to trunk/
|
|
46
46
|
--readme-only Sync only the readme to trunk/ (not with --svn-tag)
|
|
47
|
-
--svn-tag <version>
|
|
48
|
-
|
|
47
|
+
--svn-tag <version> Create tags/<version>: a copy of trunk/ with --trunk,
|
|
48
|
+
otherwise from the source files, leaving trunk/
|
|
49
|
+
alone. A leading "v" is stripped.
|
|
49
50
|
--assets-dir <dir> Sync this directory with banners, icons and
|
|
50
51
|
screenshots to assets/
|
|
51
52
|
--no-assets Don't sync --assets-dir for this run
|
|
@@ -72,6 +73,8 @@ or rejected.
|
|
|
72
73
|
Examples:
|
|
73
74
|
wp-release deploy-svn dist --trunk --svn-tag 1.2.0 --assets-dir .wordpress-org
|
|
74
75
|
Deploy 1.2.0 to trunk and the tag, and update the assets
|
|
76
|
+
wp-release deploy-svn dist --svn-tag 1.2.0-rc.1
|
|
77
|
+
Tag a pre-release build without touching trunk
|
|
75
78
|
wp-release deploy-svn dist --readme-only
|
|
76
79
|
Update only the readme
|
|
77
80
|
wp-release deploy-svn --assets-dir .wordpress-org
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { CommandOutcome } from './command.ts';
|
|
2
|
+
/** Result of the sign command. */
|
|
3
|
+
export interface SignResult {
|
|
4
|
+
/** Absolute paths of the written .sig files. */
|
|
5
|
+
signatures: string[];
|
|
6
|
+
/** Base64 public key of the signing key. */
|
|
7
|
+
publicKey: string;
|
|
8
|
+
}
|
|
9
|
+
declare const _default: {
|
|
10
|
+
name: string;
|
|
11
|
+
usage: string;
|
|
12
|
+
summary: string;
|
|
13
|
+
help: string;
|
|
14
|
+
options: {
|
|
15
|
+
force: {
|
|
16
|
+
type: "boolean";
|
|
17
|
+
};
|
|
18
|
+
};
|
|
19
|
+
run({ values, positionals, cwd, logger, }: import("./command.ts").CommandContext): Promise<CommandOutcome<SignResult>>;
|
|
20
|
+
};
|
|
21
|
+
export default _default;
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { resolve } from 'node:path';
|
|
2
|
+
import { stat } from 'node:fs/promises';
|
|
3
|
+
import { ValidationError } from "../errors.js";
|
|
4
|
+
import { SIGNING_KEY_ENV, getPublicKey, parseSigningKey, writeSignature, } from "../signing.js";
|
|
5
|
+
import { getBoolean } from "./command.js";
|
|
6
|
+
export default {
|
|
7
|
+
name: 'sign',
|
|
8
|
+
usage: 'sign <file>... [options]',
|
|
9
|
+
summary: 'Sign files with Ed25519, writing a detached <file>.sig next to each.',
|
|
10
|
+
help: `Arguments:
|
|
11
|
+
<file>... Files to sign, such as the ZIP and the checksums
|
|
12
|
+
manifest
|
|
13
|
+
|
|
14
|
+
Options:
|
|
15
|
+
--force Overwrite existing .sig files
|
|
16
|
+
|
|
17
|
+
The signing key is read from the ${SIGNING_KEY_ENV} environment variable, as a
|
|
18
|
+
base64 32-byte Ed25519 seed or 64-byte libsodium secret key. There's no flag
|
|
19
|
+
for it, since it would show in the process list.
|
|
20
|
+
|
|
21
|
+
The signature is the one WordPress core's verify_file_signature() checks: a
|
|
22
|
+
base64 Ed25519 signature of the raw SHA-384 digest of the file. Sign both
|
|
23
|
+
the ZIP and the checksums manifest. Use --verbose to print the public key
|
|
24
|
+
that belongs to the signing key.`,
|
|
25
|
+
options: {
|
|
26
|
+
force: { type: 'boolean' },
|
|
27
|
+
},
|
|
28
|
+
async run({ values, positionals, cwd, logger, }) {
|
|
29
|
+
const encoded = process.env[SIGNING_KEY_ENV];
|
|
30
|
+
if (!positionals.length) {
|
|
31
|
+
throw new ValidationError('Pass the files to sign.');
|
|
32
|
+
}
|
|
33
|
+
if (!encoded?.trim()) {
|
|
34
|
+
throw new ValidationError(`Set the ${SIGNING_KEY_ENV} environment variable to the base64 Ed25519 signing key.`);
|
|
35
|
+
}
|
|
36
|
+
const privateKey = parseSigningKey(encoded);
|
|
37
|
+
const publicKey = getPublicKey(privateKey);
|
|
38
|
+
const paths = positionals.map((file) => resolve(cwd, file));
|
|
39
|
+
await Promise.all(paths.map(async (path) => {
|
|
40
|
+
if (!(await stat(path).catch(() => null))?.isFile()) {
|
|
41
|
+
throw new ValidationError(`${path} is not a file.`);
|
|
42
|
+
}
|
|
43
|
+
}));
|
|
44
|
+
logger.debug(`Signing with the public key ${publicKey}.`);
|
|
45
|
+
const signatures = [];
|
|
46
|
+
const force = getBoolean(values, 'force');
|
|
47
|
+
// Sign in order, so that a failure leaves a predictable set of files.
|
|
48
|
+
for (const path of paths) {
|
|
49
|
+
const signaturePath = await writeSignature(path, privateKey, force);
|
|
50
|
+
logger.info(signaturePath);
|
|
51
|
+
signatures.push(signaturePath);
|
|
52
|
+
}
|
|
53
|
+
return { exitCode: 0, result: { signatures, publicKey } };
|
|
54
|
+
},
|
|
55
|
+
};
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { CommandOutcome } from './command.ts';
|
|
1
2
|
declare const _default: {
|
|
2
3
|
name: string;
|
|
3
4
|
usage: string;
|
|
@@ -11,17 +12,19 @@ declare const _default: {
|
|
|
11
12
|
checksums: {
|
|
12
13
|
type: "string";
|
|
13
14
|
};
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
exitCode: number;
|
|
17
|
-
result: {
|
|
18
|
-
valid: boolean;
|
|
19
|
-
checksumsPath: string;
|
|
20
|
-
matched: number;
|
|
21
|
-
changed: string[];
|
|
22
|
-
missing: string[];
|
|
23
|
-
extra: string[];
|
|
15
|
+
zip: {
|
|
16
|
+
type: "string";
|
|
24
17
|
};
|
|
25
|
-
|
|
18
|
+
'root-folder': {
|
|
19
|
+
type: "string";
|
|
20
|
+
};
|
|
21
|
+
embedded: {
|
|
22
|
+
type: "string";
|
|
23
|
+
};
|
|
24
|
+
'public-key': {
|
|
25
|
+
type: "string";
|
|
26
|
+
};
|
|
27
|
+
};
|
|
28
|
+
run({ values, positionals, cwd, logger }: import("./command.ts").CommandContext): Promise<CommandOutcome<unknown>>;
|
|
26
29
|
};
|
|
27
30
|
export default _default;
|