@wpelevator/wp-release 0.14.0 → 0.16.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (58) hide show
  1. package/README.md +256 -36
  2. package/dist/changeset-config.d.ts +3 -0
  3. package/dist/changeset-release.d.ts +9 -3
  4. package/dist/changeset-release.js +23 -14
  5. package/dist/cli.d.ts +17 -7
  6. package/dist/cli.js +27 -2
  7. package/dist/commands/args.d.ts +11 -0
  8. package/dist/commands/args.js +23 -0
  9. package/dist/commands/artifact-signing.d.ts +20 -0
  10. package/dist/commands/artifact-signing.js +42 -0
  11. package/dist/commands/build.d.ts +32 -0
  12. package/dist/commands/build.js +91 -0
  13. package/dist/commands/changeset-add.d.ts +55 -6
  14. package/dist/commands/changeset-add.js +12 -3
  15. package/dist/commands/changeset-args.js +2 -2
  16. package/dist/commands/changeset-project-args.d.ts +63 -0
  17. package/dist/commands/changeset-project-args.js +114 -0
  18. package/dist/commands/changeset-status.d.ts +50 -1
  19. package/dist/commands/changeset-status.js +4 -2
  20. package/dist/commands/changeset-tag.d.ts +55 -6
  21. package/dist/commands/changeset-tag.js +7 -4
  22. package/dist/commands/changeset-version.d.ts +55 -6
  23. package/dist/commands/changeset-version.js +4 -2
  24. package/dist/commands/checksums.d.ts +4 -1
  25. package/dist/commands/checksums.js +27 -6
  26. package/dist/commands/command.d.ts +3 -0
  27. package/dist/commands/deploy-svn.d.ts +11 -15
  28. package/dist/commands/deploy-svn.js +25 -5
  29. package/dist/commands/package-source.d.ts +43 -0
  30. package/dist/commands/package-source.js +101 -0
  31. package/dist/commands/sign.d.ts +0 -9
  32. package/dist/commands/sign.js +10 -47
  33. package/dist/commands/verify.d.ts +7 -1
  34. package/dist/commands/verify.js +53 -17
  35. package/dist/commands/zip.d.ts +25 -2
  36. package/dist/commands/zip.js +136 -110
  37. package/dist/index.d.ts +9 -2
  38. package/dist/index.js +3 -1
  39. package/dist/integrity.d.ts +33 -0
  40. package/dist/integrity.js +115 -0
  41. package/dist/output-files.d.ts +7 -0
  42. package/dist/output-files.js +25 -0
  43. package/dist/package-build.d.ts +33 -0
  44. package/dist/package-build.js +112 -0
  45. package/dist/package-files.d.ts +25 -0
  46. package/dist/package-files.js +24 -0
  47. package/dist/package-zip.d.ts +14 -2
  48. package/dist/package-zip.js +129 -38
  49. package/dist/release-tags.js +6 -0
  50. package/dist/signing.d.ts +36 -0
  51. package/dist/signing.js +52 -1
  52. package/dist/single-project.d.ts +32 -0
  53. package/dist/single-project.js +144 -0
  54. package/dist/summary.d.ts +9 -0
  55. package/dist/summary.js +25 -0
  56. package/dist/zip.d.ts +3 -0
  57. package/dist/zip.js +16 -6
  58. package/package.json +6 -1
@@ -0,0 +1,101 @@
1
+ import { basename, resolve } from 'node:path';
2
+ import { ValidationError } from "../errors.js";
3
+ import { resolveFileSelection } from "../file-selection.js";
4
+ import { getPackageFiles } from "../package-deploy.js";
5
+ import { readPackageMeta } from "../package-meta.js";
6
+ import { checkDirectory, getFileSelectionInput, getSourceDirArg, } from "./args.js";
7
+ import { getBoolean, getString, getStrings } from "./command.js";
8
+ /** Options of a source directory and its files, which --package takes from the config. */
9
+ const SOURCE_FLAGS = [
10
+ 'source-dir',
11
+ 'distignore',
12
+ 'no-distignore',
13
+ 'exclude',
14
+ 'exclude-gitattributes',
15
+ ];
16
+ /**
17
+ * Get the configured package to release, failing for the options that set the
18
+ * source directory and the files instead.
19
+ *
20
+ * @param values Parsed CLI values.
21
+ * @param positionals Positional arguments.
22
+ * @return The package identifier or path, or undefined without --package.
23
+ * @throws {ValidationError} For an empty --package, or --package with a source directory or file selection option.
24
+ */
25
+ export function getPackageArg(values, positionals) {
26
+ const name = getString(values, 'package')?.trim();
27
+ const conflicts = SOURCE_FLAGS.filter((flag) => undefined !== getString(values, flag) ||
28
+ getBoolean(values, flag) ||
29
+ getStrings(values, flag).length);
30
+ if ('' === name) {
31
+ throw new ValidationError('--package <name> must not be empty.');
32
+ }
33
+ if (name && (positionals.length || conflicts.length)) {
34
+ throw new ValidationError(`--package takes the directory and files of the package from the config, so it can't be combined with ${positionals.length
35
+ ? 'a source directory'
36
+ : conflicts.map((flag) => `--${flag}`).join(', ')}.`);
37
+ }
38
+ return name;
39
+ }
40
+ /**
41
+ * Describe the files of a directory with the slug and the version from the
42
+ * options, or from the plugin or theme headers.
43
+ *
44
+ * @param values Parsed CLI values.
45
+ * @param sourceDir Directory to release.
46
+ * @param selection File selection.
47
+ * @param slug Default slug.
48
+ * @return Source directory, file selection, slug, version and type.
49
+ */
50
+ async function describeSource(values, sourceDir, selection, slug) {
51
+ const name = getString(values, 'slug') ?? slug;
52
+ const meta = await readPackageMeta({ dir: sourceDir, slug: name });
53
+ return {
54
+ sourceDir,
55
+ selection,
56
+ slug: name,
57
+ version: getString(values, 'version') ?? meta?.version ?? null,
58
+ ...(meta ? { type: meta.type } : {}),
59
+ };
60
+ }
61
+ /**
62
+ * Get the files of a source directory from the CLI options.
63
+ *
64
+ * @param values Parsed CLI values.
65
+ * @param positionals Positional arguments.
66
+ * @param cwd Working directory.
67
+ * @return Source directory, file selection, slug and version.
68
+ */
69
+ export async function getDirectorySource(values, positionals, cwd) {
70
+ const sourceDir = resolve(cwd, getSourceDirArg(values, positionals));
71
+ await checkDirectory(sourceDir);
72
+ return describeSource(values, sourceDir, resolveFileSelection({
73
+ cwd,
74
+ sourceDir,
75
+ ...getFileSelectionInput(values),
76
+ }), basename(cwd));
77
+ }
78
+ /**
79
+ * Get the directory of a configured package with the files config of its
80
+ * package group, which its deploys use too.
81
+ *
82
+ * @param values Parsed CLI values.
83
+ * @param cwd Repository root with the wp-release config.
84
+ * @param name Package identifier or path under the configured root.
85
+ * @param configFile Config file, instead of the conventional one.
86
+ * @return Source directory, file selection, slug, version and package.
87
+ * @throws {ValidationError} Without a config or for an unknown package.
88
+ */
89
+ export async function getPackageSource(values, cwd, name, configFile) {
90
+ const pkg = await getPackageFiles({
91
+ cwd,
92
+ package: name,
93
+ ...(configFile ? { configFile } : {}),
94
+ });
95
+ const source = await describeSource(values, pkg.directory, resolveFileSelection({
96
+ cwd,
97
+ sourceDir: pkg.directory,
98
+ ...pkg.files,
99
+ }), basename(pkg.directory));
100
+ return { ...source, package: pkg.name };
101
+ }
@@ -11,15 +11,6 @@ export interface SignResult {
11
11
  /** Public key of the signing key in the form of each signature format. */
12
12
  publicKeys: Record<SignatureFormat, string>;
13
13
  }
14
- /**
15
- * Get the signature formats from the --format values, which can be repeated
16
- * or comma-separated.
17
- *
18
- * @param values Option values.
19
- * @return Unique formats, the default when none are given.
20
- * @throws {ValidationError} If a format is unknown.
21
- */
22
- export declare function parseSignatureFormats(values: string[]): SignatureFormat[];
23
14
  declare const _default: {
24
15
  name: string;
25
16
  usage: string;
@@ -1,30 +1,9 @@
1
1
  import { resolve } from 'node:path';
2
- import { stat } from 'node:fs/promises';
3
2
  import { ValidationError } from "../errors.js";
4
3
  import { getSignSummary } from "../summary.js";
5
- import { DEFAULT_SIGNATURE_FORMAT, SIGNATURE_FORMATS, SIGNING_KEY_ENV, getPublicKey, getPublicKeys, isSignatureFormat, parseSigningKey, writeSignature, } from "../signing.js";
4
+ import { SIGNING_KEY_ENV, signFiles } from "../signing.js";
5
+ import { parseSignatureFormats } from "./args.js";
6
6
  import { getBoolean, getStrings } from "./command.js";
7
- /**
8
- * Get the signature formats from the --format values, which can be repeated
9
- * or comma-separated.
10
- *
11
- * @param values Option values.
12
- * @return Unique formats, the default when none are given.
13
- * @throws {ValidationError} If a format is unknown.
14
- */
15
- export function parseSignatureFormats(values) {
16
- const names = values
17
- .flatMap((value) => value.split(','))
18
- .map((name) => name.trim())
19
- .filter(Boolean);
20
- const unknown = names.filter((name) => !isSignatureFormat(name));
21
- if (unknown.length) {
22
- throw new ValidationError(`Unknown signature format ${unknown.join(', ')}, use ${Object.keys(SIGNATURE_FORMATS).join(', ')}.`);
23
- }
24
- return names.length
25
- ? [...new Set(names.filter(isSignatureFormat))]
26
- : [DEFAULT_SIGNATURE_FORMAT];
27
- }
28
7
  export default {
29
8
  name: 'sign',
30
9
  usage: 'sign <file>... [options]',
@@ -58,34 +37,18 @@ form of each format.`,
58
37
  force: { type: 'boolean' },
59
38
  },
60
39
  async run({ values, positionals, cwd, logger, }) {
61
- const encoded = process.env[SIGNING_KEY_ENV];
62
40
  const formats = parseSignatureFormats(getStrings(values, 'format'));
63
41
  if (!positionals.length) {
64
42
  throw new ValidationError('Pass the files to sign.');
65
43
  }
66
- if (!encoded?.trim()) {
67
- throw new ValidationError(`Set the ${SIGNING_KEY_ENV} environment variable to the base64 Ed25519 signing key.`);
68
- }
69
- const privateKey = parseSigningKey(encoded);
70
- const publicKey = getPublicKey(privateKey);
71
- const publicKeys = getPublicKeys(privateKey);
72
- const paths = positionals.map((file) => resolve(cwd, file));
73
- await Promise.all(paths.map(async (path) => {
74
- if (!(await stat(path).catch(() => null))?.isFile()) {
75
- throw new ValidationError(`${path} is not a file.`);
76
- }
77
- }));
78
- formats.forEach((format) => logger.debug(`Signing ${format} with the public key ${publicKeys[format]}.`));
79
- const signatures = [];
80
- const force = getBoolean(values, 'force');
81
- const jobs = paths.flatMap((path) => formats.map((format) => ({ path, format })));
82
- // Sign in order, so that a failure leaves a predictable set of files.
83
- for (const { path, format } of jobs) {
84
- const signaturePath = await writeSignature(path, privateKey, force, format);
85
- logger.info(signaturePath);
86
- signatures.push(signaturePath);
87
- }
88
- const result = { signatures, formats, publicKey, publicKeys };
44
+ const signed = await signFiles({
45
+ paths: positionals.map((file) => resolve(cwd, file)),
46
+ formats,
47
+ force: getBoolean(values, 'force'),
48
+ });
49
+ formats.forEach((format) => logger.debug(`Signed ${format} with the public key ${signed.publicKeys[format]}.`));
50
+ signed.signatures.forEach((path) => logger.info(path));
51
+ const result = { ...signed, formats };
89
52
  logger.summary((context) => getSignSummary(context, result));
90
53
  return { exitCode: 0, result };
91
54
  },
@@ -12,6 +12,12 @@ declare const _default: {
12
12
  checksums: {
13
13
  type: "string";
14
14
  };
15
+ 'checksums-format': {
16
+ type: "string";
17
+ };
18
+ 'checksums-file': {
19
+ type: "string";
20
+ };
15
21
  zip: {
16
22
  type: "string";
17
23
  };
@@ -28,6 +34,6 @@ declare const _default: {
28
34
  type: "string";
29
35
  };
30
36
  };
31
- run({ values, positionals, cwd, logger }: import("./command.ts").CommandContext): Promise<CommandOutcome<unknown>>;
37
+ run({ values, positionals, cwd, configFile, logger }: import("./command.ts").CommandContext): Promise<CommandOutcome<unknown>>;
32
38
  };
33
39
  export default _default;
@@ -1,8 +1,10 @@
1
+ import { readIntegrity, checkChecksumsFile, resolveManifestCodec, } from "../integrity.js";
2
+ import { loadChangesetConfig } from "../changeset-config.js";
1
3
  import { existsSync } from 'node:fs';
2
4
  import { mkdir, mkdtemp, readFile, rm } from 'node:fs/promises';
3
5
  import { tmpdir } from 'node:os';
4
6
  import { basename, isAbsolute, join, posix, resolve } from 'node:path';
5
- import { readChecksums, verifyChecksums } from "../checksums.js";
7
+ import { verifyChecksums } from "../checksums.js";
6
8
  import { downloadFile, isUrl } from "../download.js";
7
9
  import { ignoreOwnFile, ignoreOwnSignature, loadFileRules, parseExclude, } from "../file-selection.js";
8
10
  import { ValidationError } from "../errors.js";
@@ -105,7 +107,7 @@ function getEmbeddedPath(checksumsPath, embedded) {
105
107
  ['.', '..'].includes(path))) {
106
108
  throw new ValidationError(`--embedded must be a path inside the plugin with forward slashes, got "${embedded}".`);
107
109
  }
108
- return embedded || path.endsWith('.json') ? path : null;
110
+ return path;
109
111
  }
110
112
  /**
111
113
  * Get the manifest and signature pairs to look for inside the checked files.
@@ -180,9 +182,22 @@ async function hasSameContents(original, file) {
180
182
  */
181
183
  async function checkEmbeddedCopies(input) {
182
184
  const { dir, manifest, checksumsPath, embedded, format } = input;
183
- const pairs = getCopyPairs(checksumsPath, getEmbeddedPath(checksumsPath, embedded), format);
185
+ const path = getEmbeddedPath(checksumsPath, embedded);
186
+ const pairs = getCopyPairs(checksumsPath, path, format).filter((pair) => pair.original !== join(dir, pair.copy) ||
187
+ existsSync(pair.original));
188
+ for (const other of Object.keys(SIGNATURE_FORMATS)) {
189
+ if (other !== format &&
190
+ path &&
191
+ existsSync(getSignaturePath(checksumsPath, other))) {
192
+ pairs.push({
193
+ original: getSignaturePath(checksumsPath, other),
194
+ copy: getSignaturePath(path, other),
195
+ });
196
+ }
197
+ }
184
198
  if (embedded) {
185
- checkExplicitPairs(pairs, manifest);
199
+ checkExplicitPairs(pairs.filter((pair) => pair.original !== join(dir, pair.copy) ||
200
+ existsSync(pair.original)), manifest);
186
201
  }
187
202
  const found = pairs.filter(({ original, copy }) => existsSync(join(dir, copy)) &&
188
203
  (embedded ||
@@ -336,8 +351,10 @@ export default {
336
351
  accepted as --source-dir <dir>
337
352
 
338
353
  Options:
339
- --checksums <file> Checksums manifest from "wp-release checksums" or the
340
- WordPress.org checksums API (required)
354
+ --checksums-file <file> Relative embedded manifest path (default: checksums.json)
355
+ --checksums-format <name> wordpress-json or sha256sum; custom codecs via --config
356
+ --checksums <file> External checksums manifest from "wp-release checksums" or the
357
+ WordPress.org checksums API (optional; otherwise reads embedded manifest)
341
358
  --zip <file|url> Check the files of this ZIP, a local file or an
342
359
  http(s) URL such as the ZIP that WordPress.org
343
360
  builds for a release, instead of <source-dir>
@@ -381,6 +398,8 @@ file that isn't a matching copy is reported as extra. See --embedded for
381
398
  where the copies are looked for.`,
382
399
  options: {
383
400
  checksums: { type: 'string' },
401
+ 'checksums-format': { type: 'string' },
402
+ 'checksums-file': { type: 'string' },
384
403
  zip: { type: 'string' },
385
404
  'root-folder': { type: 'string' },
386
405
  embedded: { type: 'string' },
@@ -389,42 +408,59 @@ where the copies are looked for.`,
389
408
  ...sourceDirArgs,
390
409
  exclude: { type: 'string', multiple: true },
391
410
  },
392
- async run({ values, positionals, cwd, logger }) {
411
+ async run({ values, positionals, cwd, configFile, logger }) {
393
412
  const zip = getString(values, 'zip');
394
413
  const sourceArg = positionals.length
395
414
  ? getSourceDirArg(values, positionals)
396
415
  : getString(values, 'source-dir');
397
416
  const checksums = getString(values, 'checksums');
398
- if (!checksums) {
399
- throw new ValidationError('Pass the checksums manifest with --checksums <file>.');
400
- }
401
417
  if (zip && sourceArg) {
402
418
  throw new ValidationError('Pass either --zip or a source directory, not both.');
403
419
  }
404
- const checksumsPath = resolve(cwd, checksums);
420
+ // Only load caller-selected configuration, never configuration inside the package.
421
+ const loaded = configFile
422
+ ? await loadChangesetConfig({ cwd, configFile })
423
+ : undefined;
424
+ const integrity = loaded?.config.integrity;
425
+ const codec = getString(values, 'checksums-format') ?? integrity?.codec;
426
+ const file = checkChecksumsFile(getString(values, 'checksums-file') ??
427
+ integrity?.file ??
428
+ resolveManifestCodec(codec).filename);
405
429
  const publicKey = getString(values, 'public-key');
406
430
  const format = getString(values, 'signature-format') ?? DEFAULT_SIGNATURE_FORMAT;
407
431
  if (!isSignatureFormat(format)) {
408
432
  throw new ValidationError(`Unknown signature format ${format}, use ${Object.keys(SIGNATURE_FORMATS).join(', ')}.`);
409
433
  }
410
- if (publicKey) {
411
- await checkSignature(checksumsPath, publicKey, format);
412
- logger.info(`Signature of ${checksumsPath} is valid.`);
413
- }
414
- const manifest = await readChecksums(checksumsPath);
415
434
  const target = await resolveTarget({
416
435
  sourceDir: resolve(cwd, sourceArg ?? '.'),
417
436
  zip,
418
437
  cwd,
419
438
  rootFolder: getString(values, 'root-folder'),
420
439
  });
440
+ const checksumsPath = checksums
441
+ ? resolve(cwd, checksums)
442
+ : join(target.dir, file);
421
443
  try {
444
+ if (publicKey) {
445
+ await checkSignature(checksumsPath, publicKey, format);
446
+ if (!checksums) {
447
+ for (const other of Object.keys(SIGNATURE_FORMATS)) {
448
+ if (other !== format &&
449
+ existsSync(getSignaturePath(checksumsPath, other))) {
450
+ await checkSignature(checksumsPath, publicKey, other);
451
+ }
452
+ }
453
+ }
454
+ logger.info(`Signature of ${checksumsPath} is valid.`);
455
+ }
456
+ const manifest = await readIntegrity(checksumsPath, codec);
422
457
  return await verifyTarget({
423
458
  target,
424
459
  manifest,
425
460
  checksumsPath,
426
461
  exclude: getStrings(values, 'exclude'),
427
- embedded: getString(values, 'embedded'),
462
+ embedded: getString(values, 'embedded') ??
463
+ (checksums ? undefined : file),
428
464
  format,
429
465
  logger,
430
466
  });
@@ -8,6 +8,10 @@ export interface ZipResult extends ZipPackageResult {
8
8
  version: string | null;
9
9
  /** Zipped package, with --package. */
10
10
  package?: string;
11
+ /** External signature paths and package-relative embedded manifest signature paths. */
12
+ signatures?: string[];
13
+ /** Base64 public key of the signing key, with ZIP or manifest signing. */
14
+ publicKey?: string;
11
15
  }
12
16
  declare const _default: {
13
17
  name: string;
@@ -15,15 +19,34 @@ declare const _default: {
15
19
  summary: string;
16
20
  help: string;
17
21
  options: {
18
- package: {
22
+ 'checksums-file': {
23
+ type: "string";
24
+ };
25
+ 'checksums-format': {
19
26
  type: "string";
20
27
  };
21
- checksums: {
28
+ 'checksums-algorithms': {
29
+ type: "string";
30
+ multiple: true;
31
+ };
32
+ 'embed-checksums': {
33
+ type: "boolean";
34
+ };
35
+ 'no-embed-checksums': {
36
+ type: "boolean";
37
+ };
38
+ 'no-sign-checksums': {
39
+ type: "boolean";
40
+ };
41
+ sign: {
22
42
  type: "boolean";
23
43
  };
24
44
  force: {
25
45
  type: "boolean";
26
46
  };
47
+ package: {
48
+ type: "string";
49
+ };
27
50
  slug: {
28
51
  type: "string";
29
52
  };