@wpelevator/wp-release 0.15.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 (49) hide show
  1. package/README.md +246 -37
  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 +1 -0
  6. package/dist/cli.js +2 -0
  7. package/dist/commands/artifact-signing.d.ts +20 -0
  8. package/dist/commands/artifact-signing.js +42 -0
  9. package/dist/commands/build.d.ts +32 -0
  10. package/dist/commands/build.js +91 -0
  11. package/dist/commands/changeset-add.d.ts +55 -6
  12. package/dist/commands/changeset-add.js +12 -3
  13. package/dist/commands/changeset-args.js +2 -2
  14. package/dist/commands/changeset-project-args.d.ts +63 -0
  15. package/dist/commands/changeset-project-args.js +114 -0
  16. package/dist/commands/changeset-status.d.ts +50 -1
  17. package/dist/commands/changeset-status.js +4 -2
  18. package/dist/commands/changeset-tag.d.ts +55 -6
  19. package/dist/commands/changeset-tag.js +7 -4
  20. package/dist/commands/changeset-version.d.ts +55 -6
  21. package/dist/commands/changeset-version.js +4 -2
  22. package/dist/commands/checksums.d.ts +4 -1
  23. package/dist/commands/checksums.js +27 -6
  24. package/dist/commands/package-source.d.ts +43 -0
  25. package/dist/commands/package-source.js +101 -0
  26. package/dist/commands/verify.d.ts +7 -1
  27. package/dist/commands/verify.js +53 -17
  28. package/dist/commands/zip.d.ts +20 -8
  29. package/dist/commands/zip.js +109 -124
  30. package/dist/index.d.ts +6 -0
  31. package/dist/index.js +2 -0
  32. package/dist/integrity.d.ts +33 -0
  33. package/dist/integrity.js +115 -0
  34. package/dist/output-files.d.ts +7 -0
  35. package/dist/output-files.js +25 -0
  36. package/dist/package-build.d.ts +33 -0
  37. package/dist/package-build.js +112 -0
  38. package/dist/package-files.d.ts +25 -0
  39. package/dist/package-files.js +24 -0
  40. package/dist/package-zip.d.ts +14 -2
  41. package/dist/package-zip.js +129 -38
  42. package/dist/release-tags.js +6 -0
  43. package/dist/single-project.d.ts +32 -0
  44. package/dist/single-project.js +144 -0
  45. package/dist/summary.d.ts +9 -0
  46. package/dist/summary.js +25 -0
  47. package/dist/zip.d.ts +3 -0
  48. package/dist/zip.js +16 -6
  49. package/package.json +6 -1
@@ -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,9 +8,9 @@ export interface ZipResult extends ZipPackageResult {
8
8
  version: string | null;
9
9
  /** Zipped package, with --package. */
10
10
  package?: string;
11
- /** Absolute paths of the signatures of the ZIP and the manifest, with --sign. */
11
+ /** External signature paths and package-relative embedded manifest signature paths. */
12
12
  signatures?: string[];
13
- /** Base64 public key of the signing key, with --sign. */
13
+ /** Base64 public key of the signing key, with ZIP or manifest signing. */
14
14
  publicKey?: string;
15
15
  }
16
16
  declare const _default: {
@@ -19,22 +19,34 @@ declare const _default: {
19
19
  summary: string;
20
20
  help: string;
21
21
  options: {
22
- package: {
22
+ 'checksums-file': {
23
+ type: "string";
24
+ };
25
+ 'checksums-format': {
26
+ type: "string";
27
+ };
28
+ 'checksums-algorithms': {
23
29
  type: "string";
30
+ multiple: true;
24
31
  };
25
- checksums: {
32
+ 'embed-checksums': {
26
33
  type: "boolean";
27
34
  };
28
- sign: {
35
+ 'no-embed-checksums': {
29
36
  type: "boolean";
30
37
  };
31
- 'sign-format': {
32
- type: "string";
33
- multiple: true;
38
+ 'no-sign-checksums': {
39
+ type: "boolean";
40
+ };
41
+ sign: {
42
+ type: "boolean";
34
43
  };
35
44
  force: {
36
45
  type: "boolean";
37
46
  };
47
+ package: {
48
+ type: "string";
49
+ };
38
50
  slug: {
39
51
  type: "string";
40
52
  };
@@ -1,108 +1,15 @@
1
- import { basename, resolve } from 'node:path';
1
+ import { checksumSigningArgs, getArtifactSigning } from "./artifact-signing.js";
2
2
  import { ValidationError } from "../errors.js";
3
- import { resolveFileSelection } from "../file-selection.js";
3
+ import { basename, dirname, resolve, sep } from 'node:path';
4
+ import { loadChangesetConfig } from "../changeset-config.js";
5
+ import { resolveManifestCodec, checkChecksumsFile } from "../integrity.js";
6
+ import { getPackageArg, getPackageSource, getDirectorySource, } from "./package-source.js";
4
7
  import { FILENAME_FORMAT, formatFilename } from "../filename.js";
5
- import { getPackageFiles } from "../package-deploy.js";
6
- import { readPackageMeta } from "../package-meta.js";
7
8
  import { getChecksumsPath, zipPackage } from "../package-zip.js";
8
- import { SIGNING_KEY_ENV, checkSigningInput, signFiles } from "../signing.js";
9
+ import { SIGNING_KEY_ENV, signFiles, getSignaturePath } from "../signing.js";
9
10
  import { getSignSummary, getZipSummary } from "../summary.js";
10
- import { checkDirectory, fileSelectionArgs, getFileSelectionInput, getSourceDirArg, parseSignatureFormats, resolveOutput, sourceDirArgs, } from "./args.js";
11
+ import { fileSelectionArgs, parseSignatureFormats, resolveOutput, sourceDirArgs, } from "./args.js";
11
12
  import { getBoolean, getString, getStrings } from "./command.js";
12
- /** Options of a source directory and its files, which --package takes from the config. */
13
- const SOURCE_FLAGS = [
14
- 'source-dir',
15
- 'distignore',
16
- 'no-distignore',
17
- 'exclude',
18
- 'exclude-gitattributes',
19
- ];
20
- /**
21
- * Get the configured package to zip, failing for the options that set the
22
- * source directory and the files instead.
23
- *
24
- * @param values Parsed CLI values.
25
- * @param positionals Positional arguments.
26
- * @return The package identifier or path, or undefined without --package.
27
- * @throws {ValidationError} For an empty --package, or --package with a source directory or file selection option.
28
- */
29
- function getPackageArg(values, positionals) {
30
- const name = getString(values, 'package')?.trim();
31
- const conflicts = SOURCE_FLAGS.filter((flag) => undefined !== getString(values, flag) ||
32
- getBoolean(values, flag) ||
33
- getStrings(values, flag).length);
34
- if ('' === name) {
35
- throw new ValidationError('--package <name> must not be empty.');
36
- }
37
- if (name && (positionals.length || conflicts.length)) {
38
- throw new ValidationError(`--package takes the directory and files of the package from the config, so it can't be combined with ${positionals.length
39
- ? 'a source directory'
40
- : conflicts.map((flag) => `--${flag}`).join(', ')}.`);
41
- }
42
- return name;
43
- }
44
- /**
45
- * Describe the files of a directory with the slug and the version from the
46
- * options, or from the plugin or theme headers.
47
- *
48
- * @param values Parsed CLI values.
49
- * @param sourceDir Directory to zip.
50
- * @param selection File selection.
51
- * @param slug Default slug.
52
- * @return Source directory, file selection, slug, version and type.
53
- */
54
- async function describeSource(values, sourceDir, selection, slug) {
55
- const name = getString(values, 'slug') ?? slug;
56
- const meta = await readPackageMeta({ dir: sourceDir, slug: name });
57
- return {
58
- sourceDir,
59
- selection,
60
- slug: name,
61
- version: getString(values, 'version') ?? meta?.version ?? null,
62
- ...(meta ? { type: meta.type } : {}),
63
- };
64
- }
65
- /**
66
- * Get the files of a source directory from the CLI options.
67
- *
68
- * @param values Parsed CLI values.
69
- * @param positionals Positional arguments.
70
- * @param cwd Working directory.
71
- * @return Source directory, file selection, slug and version.
72
- */
73
- async function getDirectorySource(values, positionals, cwd) {
74
- const sourceDir = resolve(cwd, getSourceDirArg(values, positionals));
75
- await checkDirectory(sourceDir);
76
- return describeSource(values, sourceDir, resolveFileSelection({
77
- cwd,
78
- sourceDir,
79
- ...getFileSelectionInput(values),
80
- }), basename(cwd));
81
- }
82
- /**
83
- * Get the directory of a configured package with the files config of its
84
- * package group, which its deploys use too.
85
- *
86
- * @param values Parsed CLI values.
87
- * @param cwd Repository root with the wp-release config.
88
- * @param name Package identifier or path under the configured root.
89
- * @param configFile Config file, instead of the conventional one.
90
- * @return Source directory, file selection, slug, version and package.
91
- * @throws {ValidationError} Without a config or for an unknown package.
92
- */
93
- async function getPackageSource(values, cwd, name, configFile) {
94
- const pkg = await getPackageFiles({
95
- cwd,
96
- package: name,
97
- ...(configFile ? { configFile } : {}),
98
- });
99
- const source = await describeSource(values, pkg.directory, resolveFileSelection({
100
- cwd,
101
- sourceDir: pkg.directory,
102
- ...pkg.files,
103
- }), basename(pkg.directory));
104
- return { ...source, package: pkg.name };
105
- }
106
13
  export default {
107
14
  name: 'zip',
108
15
  usage: 'zip [<source-dir>] [options] | zip --package <name> [options]',
@@ -135,18 +42,24 @@ Options:
135
42
  --checksums Also write the checksums manifest of the archived
136
43
  files next to the ZIP, with .checksums.json
137
44
  instead of .zip
138
- --sign Also sign the ZIP and the checksums manifest with
139
- the ${SIGNING_KEY_ENV} key, like
140
- "wp-release sign", writing a WordPress .sig next
141
- to each
45
+ --checksums-file <file> Relative manifest filename; implies checksums.
46
+ Beside the ZIP, or inside its package root with
47
+ --embed-checksums. No absolute paths or dot segments.
48
+ --checksums-format <format> wordpress-json (default) or sha256sum
49
+ --checksums-algorithms <names> md5,sha256 (default), comma-separated or repeated
50
+ --embed-checksums Embed manifest instead of writing it beside ZIP
51
+ --no-embed-checksums Override embedding configured in integrity
52
+ --sign-checksums Sign the manifest only, before embedding it
53
+ --no-sign-checksums Override configured manifest signing
54
+ --sign Sign only the ZIP with the ${SIGNING_KEY_ENV} key
142
55
  --sign-format <names> Signature formats of --sign, comma-separated or
143
56
  repeated, like wordpress,minisign,ssh, as the
144
57
  --format of "wp-release sign" (default:
145
- wordpress); implies --sign
58
+ wordpress); requires --sign or --sign-checksums
146
59
  --force Overwrite existing output files
147
60
 
148
- Entries are sorted and use a fixed timestamp and file mode, so the same files
149
- always give the same ZIP.
61
+ Entries are sorted and use a fixed timestamp and file mode. Embedded minisign
62
+ signatures include their signing time; other built-in metadata is deterministic.
150
63
 
151
64
  Examples:
152
65
  wp-release zip dist --slug my-plugin --checksums
@@ -163,9 +76,14 @@ Examples:
163
76
  ...sourceDirArgs,
164
77
  ...fileSelectionArgs,
165
78
  package: { type: 'string' },
166
- checksums: { type: 'boolean' },
79
+ ...checksumSigningArgs,
80
+ 'checksums-file': { type: 'string' },
81
+ 'checksums-format': { type: 'string' },
82
+ 'checksums-algorithms': { type: 'string', multiple: true },
83
+ 'embed-checksums': { type: 'boolean' },
84
+ 'no-embed-checksums': { type: 'boolean' },
85
+ 'no-sign-checksums': { type: 'boolean' },
167
86
  sign: { type: 'boolean' },
168
- 'sign-format': { type: 'string', multiple: true },
169
87
  force: { type: 'boolean' },
170
88
  },
171
89
  async run({ values, positionals, cwd, configFile, logger, }) {
@@ -179,21 +97,68 @@ Examples:
179
97
  output: getString(values, 'output'),
180
98
  filename: `${filename}.zip`,
181
99
  });
100
+ const loaded = await loadChangesetConfig({ cwd, configFile });
101
+ const configured = loaded?.config.integrity;
102
+ const algorithms = getStrings(values, 'checksums-algorithms').flatMap((value) => value.split(','));
103
+ const integrity = {
104
+ ...configured,
105
+ codec: getString(values, 'checksums-format') ?? configured?.codec,
106
+ ...(algorithms.length
107
+ ? { algorithms: algorithms }
108
+ : {}),
109
+ };
110
+ const codec = resolveManifestCodec(integrity.codec);
111
+ const embed = !getBoolean(values, 'no-embed-checksums') &&
112
+ (getBoolean(values, 'embed-checksums') ||
113
+ Boolean(configured?.embed));
114
+ const checksumsFile = checkChecksumsFile(getString(values, 'checksums-file') ??
115
+ configured?.file ??
116
+ (codec.id === 'wordpress-json'
117
+ ? basename(getChecksumsPath(zipPath))
118
+ : codec.filename));
119
+ const manifestSign = !getBoolean(values, 'no-sign-checksums') &&
120
+ (getBoolean(values, 'sign-checksums') ||
121
+ Boolean(configured?.sign));
122
+ const checksums = getBoolean(values, 'checksums') ||
123
+ embed ||
124
+ manifestSign ||
125
+ Boolean(getString(values, 'checksums-file') ?? configured?.file);
182
126
  const signFormats = getStrings(values, 'sign-format');
183
- const signing = getBoolean(values, 'sign') || signFormats.length
184
- ? {
185
- paths: [
186
- zipPath,
187
- ...(getBoolean(values, 'checksums')
188
- ? [getChecksumsPath(zipPath)]
189
- : []),
190
- ],
191
- formats: parseSignatureFormats(signFormats),
192
- force: getBoolean(values, 'force'),
193
- }
127
+ const configuredFormats = configured?.sign
128
+ ? configured.sign.formats ?? []
129
+ : [];
130
+ const formats = parseSignatureFormats(signFormats.length ? signFormats : configuredFormats);
131
+ if (signFormats.length &&
132
+ !manifestSign &&
133
+ !getBoolean(values, 'sign')) {
134
+ throw new ValidationError('--sign-format requires --sign or --sign-checksums; it does not enable signing.');
135
+ }
136
+ const signing = getBoolean(values, 'sign')
137
+ ? getArtifactSigning({
138
+ ...values,
139
+ 'sign-checksums': false,
140
+ 'sign-format': formats,
141
+ }, {
142
+ artifact: zipPath,
143
+ checksums: resolve(dirname(zipPath), checksumsFile),
144
+ })
194
145
  : undefined;
195
- if (signing) {
196
- checkSigningInput(signing);
146
+ const externalManifest = checksums && !embed
147
+ ? resolve(dirname(zipPath), checksumsFile)
148
+ : undefined;
149
+ const artifacts = [
150
+ zipPath,
151
+ ...(externalManifest ? [externalManifest] : []),
152
+ ...(signing
153
+ ? formats.map((format) => getSignaturePath(zipPath, format))
154
+ : []),
155
+ ...(externalManifest && manifestSign
156
+ ? formats.map((format) => getSignaturePath(externalManifest, format))
157
+ : []),
158
+ ];
159
+ if (new Set(artifacts).size !== artifacts.length ||
160
+ artifacts.some((path, index) => artifacts.some((other, otherIndex) => index !== otherIndex && other.startsWith(path + sep)))) {
161
+ throw new ValidationError('ZIP, manifest and signature destinations must be distinct.');
197
162
  }
198
163
  const zipped = await zipPackage({
199
164
  sourceDir: source.sourceDir,
@@ -205,7 +170,11 @@ Examples:
205
170
  slug: source.slug,
206
171
  version: source.version,
207
172
  ...(source.type ? { type: source.type } : {}),
208
- checksums: getBoolean(values, 'checksums'),
173
+ checksums,
174
+ checksumsFile: checksums ? checksumsFile : undefined,
175
+ embedChecksums: embed,
176
+ integrity,
177
+ signChecksums: manifestSign ? formats : undefined,
209
178
  force: getBoolean(values, 'force'),
210
179
  });
211
180
  logger.debug(`Archived ${zipped.files} files from ${source.sourceDir}.`);
@@ -219,9 +188,18 @@ Examples:
219
188
  signed?.signatures.forEach((path) => logger.info(path));
220
189
  const result = {
221
190
  ...zipped,
191
+ ...(zipped.checksumsSigning
192
+ ? {
193
+ signatures: zipped.checksumsSignatures,
194
+ publicKey: zipped.checksumsSigning.publicKey,
195
+ }
196
+ : {}),
222
197
  ...(signed
223
198
  ? {
224
- signatures: signed.signatures,
199
+ signatures: [
200
+ ...signed.signatures,
201
+ ...(zipped.checksumsSignatures ?? []),
202
+ ],
225
203
  publicKey: signed.publicKey,
226
204
  }
227
205
  : {}),
@@ -230,6 +208,13 @@ Examples:
230
208
  ...(source.package ? { package: source.package } : {}),
231
209
  };
232
210
  logger.summary((context) => getZipSummary(context, result));
211
+ const checksumsSigning = zipped.checksumsSigning;
212
+ if (checksumsSigning) {
213
+ logger.summary((context) => getSignSummary(context, {
214
+ ...checksumsSigning,
215
+ formats,
216
+ }));
217
+ }
233
218
  if (signed) {
234
219
  logger.summary((context) => getSignSummary(context, signed));
235
220
  }
package/dist/index.d.ts CHANGED
@@ -59,3 +59,9 @@ export type { SummaryContext } from './summary.ts';
59
59
  export type { CheckoutPath, DeployResult } from './svn-deploy.ts';
60
60
  export type { GitCommandInput, GitCommandOutput, GitRunner, RepositoryLocation, } from './git.ts';
61
61
  export type { GitDeployInput, GitDeployOptions, GitDeployResult, } from './git-deploy.ts';
62
+ export type { SingleProjectOptions } from './single-project.ts';
63
+ export { buildPackage } from './package-build.ts';
64
+ export type { BuildPackageInput, BuildPackageResult } from './package-build.ts';
65
+ export type { BuildResult } from './commands/build.ts';
66
+ export { wordpressCodec, sha256sumCodec, resolveManifestCodec, readIntegrity, } from './integrity.ts';
67
+ export type { ManifestCodec, IntegrityConfig } from './integrity.ts';
package/dist/index.js CHANGED
@@ -27,3 +27,5 @@ export { GitDeploy, getDefaultToken, getGitAuthEnv, isValidRefName, resolveGitDe
27
27
  export { getGitDeployInputFromArgs } from "./commands/deploy-git.js";
28
28
  export { getDeployPackages, getGitDeploys, getPackageFiles, getSvnDeploys, } from "./package-deploy.js";
29
29
  export { zipPackage } from "./package-zip.js";
30
+ export { buildPackage } from "./package-build.js";
31
+ export { wordpressCodec, sha256sumCodec, resolveManifestCodec, readIntegrity, } from "./integrity.js";
@@ -0,0 +1,33 @@
1
+ import type { ChecksumsManifest, ChecksumAlgorithm } from './checksums.ts';
2
+ import type { SignatureFormat } from './signing.ts';
3
+ /** Custom codecs receive the validated WordPress-compatible integrity model. */
4
+ export interface ManifestCodec {
5
+ id: string;
6
+ filename: string;
7
+ encode: (manifest: ChecksumsManifest) => Uint8Array | Promise<Uint8Array>;
8
+ decode: (bytes: Uint8Array) => ChecksumsManifest | Promise<ChecksumsManifest>;
9
+ }
10
+ export interface IntegrityConfig {
11
+ codec?: 'wordpress-json' | 'sha256sum' | ManifestCodec;
12
+ algorithms?: ChecksumAlgorithm[];
13
+ file?: string;
14
+ embed?: boolean;
15
+ sign?: false | {
16
+ formats?: SignatureFormat[];
17
+ };
18
+ }
19
+ export declare const wordpressCodec: ManifestCodec;
20
+ export declare const sha256sumCodec: ManifestCodec;
21
+ export declare function resolveManifestCodec(value?: IntegrityConfig['codec']): ManifestCodec;
22
+ /**
23
+ * Relative artifact paths cannot escape their destination or use Windows paths.
24
+ * @param file
25
+ */
26
+ export declare function checkChecksumsFile(file: string): string;
27
+ export declare function readIntegrity(path: string, codec?: IntegrityConfig['codec']): Promise<ChecksumsManifest>;
28
+ /**
29
+ * Encode once and ensure the format preserves the selected payload hashes.
30
+ * @param codec
31
+ * @param manifest
32
+ */
33
+ export declare function encodeIntegrity(codec: ManifestCodec, manifest: ChecksumsManifest): Promise<Buffer>;
@@ -0,0 +1,115 @@
1
+ import { readFile } from 'node:fs/promises';
2
+ import { basename } from 'node:path';
3
+ import { parseChecksums, readChecksums } from "./checksums.js";
4
+ import { ValidationError } from "./errors.js";
5
+ export const wordpressCodec = {
6
+ id: 'wordpress-json',
7
+ filename: 'checksums.json',
8
+ encode: (manifest) => Buffer.from(JSON.stringify(manifest, null, '\t') + '\n'),
9
+ decode: (bytes) => parseChecksums(Buffer.from(bytes).toString('utf8')),
10
+ };
11
+ export const sha256sumCodec = {
12
+ id: 'sha256sum',
13
+ filename: 'SHA256SUMS',
14
+ encode(manifest) {
15
+ const lines = Object.entries(manifest.files)
16
+ .sort()
17
+ .map(([path, hashes]) => {
18
+ if ('string' !== typeof hashes.sha256) {
19
+ throw new ValidationError('sha256sum requires one SHA-256 digest per file.');
20
+ }
21
+ const escaped = /[\\\n\r]/.test(path);
22
+ const name = path
23
+ .replaceAll('\\', '\\\\')
24
+ .replaceAll('\n', '\\n')
25
+ .replaceAll('\r', '\\r');
26
+ return `${escaped ? '\\' : ''}${hashes.sha256} ${name}`;
27
+ });
28
+ return Buffer.from(lines.join('\n') + '\n');
29
+ },
30
+ decode(bytes) {
31
+ const files = Object.create(null);
32
+ for (const line of Buffer.from(bytes)
33
+ .toString('utf8')
34
+ .split('\n')
35
+ .filter(Boolean)) {
36
+ const match = /^(\\?)([a-fA-F0-9]{64}) [ *](.+)$/.exec(line);
37
+ if (!match) {
38
+ throw new ValidationError('Invalid SHA256SUMS entry.');
39
+ }
40
+ const path = match[1]
41
+ ? (match[3] ?? '').replace(/\\([\\nr])/g, (_, char) => ({ n: '\n', r: '\r', '\\': '\\' })[char] ?? '')
42
+ : match[3] ?? '';
43
+ if (Object.hasOwn(files, path)) {
44
+ throw new ValidationError(`Duplicate checksum path: ${path}`);
45
+ }
46
+ files[path] = { sha256: (match[2] ?? '').toLowerCase() };
47
+ }
48
+ return parseChecksums(JSON.stringify({ files }));
49
+ },
50
+ };
51
+ export function resolveManifestCodec(value = 'wordpress-json') {
52
+ if (typeof value === 'string') {
53
+ if (!['wordpress-json', 'sha256sum'].includes(value)) {
54
+ throw new ValidationError(`Unknown checksums format: ${value}.`);
55
+ }
56
+ return value === 'sha256sum' ? sha256sumCodec : wordpressCodec;
57
+ }
58
+ if (!value ||
59
+ typeof value.encode !== 'function' ||
60
+ typeof value.decode !== 'function' ||
61
+ !value.id ||
62
+ !value.filename) {
63
+ throw new ValidationError('A checksums codec needs id, filename, encode and decode.');
64
+ }
65
+ return value;
66
+ }
67
+ /**
68
+ * Relative artifact paths cannot escape their destination or use Windows paths.
69
+ * @param file
70
+ */
71
+ export function checkChecksumsFile(file) {
72
+ if (!file ||
73
+ /[\\\x00-\x1f:]/.test(file) ||
74
+ file
75
+ .split('/')
76
+ .some((part) => !part || part === '.' || part === '..')) {
77
+ throw new ValidationError('--checksums-file must be a relative file path with forward slashes and no dot segments.');
78
+ }
79
+ return file;
80
+ }
81
+ export async function readIntegrity(path, codec) {
82
+ const selected = resolveManifestCodec(codec ??
83
+ (basename(path) === 'SHA256SUMS'
84
+ ? 'sha256sum'
85
+ : 'wordpress-json'));
86
+ const manifest = selected === wordpressCodec
87
+ ? await readChecksums(path)
88
+ : await selected.decode(await readFile(path));
89
+ return parseChecksums(JSON.stringify(manifest), path);
90
+ }
91
+ /**
92
+ * Encode once and ensure the format preserves the selected payload hashes.
93
+ * @param codec
94
+ * @param manifest
95
+ */
96
+ export async function encodeIntegrity(codec, manifest) {
97
+ const bytes = Buffer.from(await codec.encode(manifest));
98
+ const decoded = parseChecksums(JSON.stringify(await codec.decode(bytes)));
99
+ const expected = Object.keys(manifest.files);
100
+ const actual = Object.keys(decoded.files);
101
+ const preserved = expected.length === actual.length &&
102
+ actual.every((path) => {
103
+ const hashes = decoded.files[path];
104
+ return (Boolean(manifest.files[path]) &&
105
+ Object.entries(hashes ?? {}).every(([algorithm, digest]) => digest ===
106
+ manifest.files[path]?.[algorithm]));
107
+ });
108
+ if (!preserved ||
109
+ ['plugin', 'theme', 'version'].some((key) => decoded[key] !== undefined &&
110
+ decoded[key] !==
111
+ manifest[key])) {
112
+ throw new ValidationError('The checksums codec must preserve payload paths, digests and any represented package identity.');
113
+ }
114
+ return bytes;
115
+ }
@@ -0,0 +1,7 @@
1
+ /**
2
+ * Validate generated file destinations before writing release artifacts.
3
+ *
4
+ * @param paths Output file paths.
5
+ * @param force Whether existing regular files can be overwritten.
6
+ */
7
+ export declare function checkOutputFiles(paths: string[], force: boolean): void;