@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,112 @@
1
+ import { CHECKSUMS_EXTENSION, createChecksums, writeChecksums, } from "./checksums.js";
2
+ import { checkOutputFiles } from "./output-files.js";
3
+ import { copyFile, lstat, mkdir, mkdtemp, realpath, rename, rm, } from 'node:fs/promises';
4
+ import { basename, dirname, isAbsolute, join, relative, resolve, } from 'node:path';
5
+ import { ValidationError } from "./errors.js";
6
+ import { isErrorCode } from "./file-selection.js";
7
+ import { getPackageFilesToRelease } from "./package-files.js";
8
+ /**
9
+ * Get the manifest path alongside a build directory.
10
+ *
11
+ * @param outputDir Build directory path.
12
+ * @return Absolute manifest path.
13
+ */
14
+ export function getBuildChecksumsPath(outputDir) {
15
+ return resolve(outputDir) + CHECKSUMS_EXTENSION;
16
+ }
17
+ /**
18
+ * Resolve existing ancestors too, so symlinked parents cannot overlap the source.
19
+ *
20
+ * @param path Directory path.
21
+ * @return Canonical path, including any missing child directories.
22
+ */
23
+ async function resolveDirectoryPath(path) {
24
+ try {
25
+ return await realpath(path);
26
+ }
27
+ catch (error) {
28
+ if (isErrorCode(error, 'ENOENT')) {
29
+ return join(await resolveDirectoryPath(dirname(path)), basename(path));
30
+ }
31
+ throw error;
32
+ }
33
+ }
34
+ /**
35
+ * Copy the release selection into a directory, replacing an existing build only with force.
36
+ *
37
+ * @param input Source selection, destination and overwrite option.
38
+ * @return Destination and number of copied files.
39
+ */
40
+ export async function buildPackage(input) {
41
+ const sourceDir = await realpath(input.sourceDir);
42
+ const outputDir = resolve(input.outputDir);
43
+ const canonicalOutput = await resolveDirectoryPath(outputDir);
44
+ const sourceFromOutput = relative(canonicalOutput, sourceDir);
45
+ const outputContainsSource = !sourceFromOutput ||
46
+ (!isAbsolute(sourceFromOutput) &&
47
+ sourceFromOutput !== '..' &&
48
+ !sourceFromOutput.startsWith('../'));
49
+ if (outputContainsSource) {
50
+ throw new ValidationError('The build output must not be the source directory or an ancestor of it.');
51
+ }
52
+ const existing = await lstat(outputDir).catch((error) => {
53
+ if (isErrorCode(error, 'ENOENT')) {
54
+ return null;
55
+ }
56
+ throw error;
57
+ });
58
+ if (existing && (!input.force || !existing.isDirectory())) {
59
+ throw new ValidationError(`${outputDir} already exists; use --force to replace an existing directory.`);
60
+ }
61
+ const checksumsPath = input.checksums
62
+ ? getBuildChecksumsPath(outputDir)
63
+ : null;
64
+ if (checksumsPath) {
65
+ checkOutputFiles([checksumsPath], input.force ?? false);
66
+ }
67
+ const { files, rules } = await getPackageFilesToRelease({
68
+ sourceDir,
69
+ selection: input.selection,
70
+ outputs: [
71
+ canonicalOutput,
72
+ ...(checksumsPath
73
+ ? [canonicalOutput + CHECKSUMS_EXTENSION]
74
+ : []),
75
+ ],
76
+ excludeSignatures: !!checksumsPath,
77
+ operation: 'build',
78
+ });
79
+ await mkdir(dirname(outputDir), { recursive: true });
80
+ const staging = await mkdtemp(join(dirname(outputDir), '.wp-release-build-'));
81
+ try {
82
+ for (const file of files) {
83
+ const target = join(staging, file);
84
+ await mkdir(dirname(target), { recursive: true });
85
+ await copyFile(join(sourceDir, file), target);
86
+ }
87
+ const manifest = checksumsPath
88
+ ? await createChecksums({
89
+ sourceDir: staging,
90
+ rules,
91
+ slug: input.slug ?? basename(sourceDir),
92
+ version: input.version,
93
+ type: input.type,
94
+ })
95
+ : null;
96
+ if (existing) {
97
+ await rm(outputDir, { recursive: true });
98
+ }
99
+ await rename(staging, outputDir);
100
+ if (checksumsPath && manifest) {
101
+ await writeChecksums(checksumsPath, manifest, input.force);
102
+ }
103
+ }
104
+ finally {
105
+ await rm(staging, { recursive: true, force: true });
106
+ }
107
+ return {
108
+ outputDir,
109
+ files: files.length,
110
+ ...(checksumsPath ? { checksumsPath } : {}),
111
+ };
112
+ }
@@ -0,0 +1,25 @@
1
+ import type { FileRules, FileSelection } from './file-selection.ts';
2
+ interface PackageFilesInput {
3
+ /** Directory whose release files are selected. */
4
+ sourceDir: string;
5
+ /** Resolved ignore file and exclusion options. */
6
+ selection: FileSelection;
7
+ /** Absolute output paths to leave out of the release. */
8
+ outputs: string[];
9
+ /** Also exclude signatures of the output files. */
10
+ excludeSignatures?: boolean;
11
+ /** Operation name used in an empty-selection error. */
12
+ operation: 'build' | 'archive';
13
+ }
14
+ /**
15
+ * Prepare the release file selection shared by directory builds and ZIPs.
16
+ *
17
+ * @param input Source, file selection and output exclusions.
18
+ * @return Sorted file paths and the rules used to select them.
19
+ * @throws {ValidationError} When no release files are selected.
20
+ */
21
+ export declare function getPackageFilesToRelease(input: PackageFilesInput): Promise<{
22
+ files: string[];
23
+ rules: FileRules;
24
+ }>;
25
+ export {};
@@ -0,0 +1,24 @@
1
+ import { ValidationError } from "./errors.js";
2
+ import { ignoreOwnFile, ignoreOwnSignature, loadFileRules, selectFiles, } from "./file-selection.js";
3
+ /**
4
+ * Prepare the release file selection shared by directory builds and ZIPs.
5
+ *
6
+ * @param input Source, file selection and output exclusions.
7
+ * @return Sorted file paths and the rules used to select them.
8
+ * @throws {ValidationError} When no release files are selected.
9
+ */
10
+ export async function getPackageFilesToRelease(input) {
11
+ const { sourceDir, selection, outputs, excludeSignatures, operation } = input;
12
+ const rules = await loadFileRules(selection);
13
+ outputs.forEach((path) => {
14
+ ignoreOwnFile(rules, sourceDir, path);
15
+ if (excludeSignatures) {
16
+ ignoreOwnSignature(rules, sourceDir, path);
17
+ }
18
+ });
19
+ const files = await selectFiles(sourceDir, rules);
20
+ if (!files.length) {
21
+ throw new ValidationError(`No files to ${operation} in ${sourceDir}.`);
22
+ }
23
+ return { files, rules };
24
+ }
@@ -1,3 +1,5 @@
1
+ import type { IntegrityConfig } from './integrity.ts';
2
+ import type { SignFilesResult, SignatureFormat } from './signing.ts';
1
3
  import type { PackageType } from './checksums.ts';
2
4
  import type { FileSelection } from './file-selection.ts';
3
5
  export interface ZipPackageInput {
@@ -15,8 +17,13 @@ export interface ZipPackageInput {
15
17
  version?: string | null;
16
18
  /** Package type for the checksums manifest. */
17
19
  type?: PackageType;
18
- /** Also write the checksums manifest next to the archive. */
20
+ /** Generate a checksums manifest beside the archive or embedded in it. */
19
21
  checksums?: boolean;
22
+ checksumsFile?: string;
23
+ embedChecksums?: boolean;
24
+ integrity?: IntegrityConfig;
25
+ /** Sign the manifest only, before embedding it. */
26
+ signChecksums?: SignatureFormat[];
20
27
  /** Overwrite existing output files. */
21
28
  force?: boolean;
22
29
  }
@@ -24,8 +31,13 @@ export interface ZipPackageInput {
24
31
  export interface ZipPackageResult {
25
32
  /** Absolute path of the ZIP. */
26
33
  zipPath: string;
27
- /** Absolute path of the checksums manifest, set with checksums. */
34
+ /** Absolute path of the external manifest; absent when disabled or embedded. */
28
35
  checksumsPath?: string;
36
+ /** Relative path inside the package, only in embedded mode. */
37
+ checksumsEmbedded?: string;
38
+ /** Manifest signatures, external paths or package-relative embedded paths. */
39
+ checksumsSignatures?: string[];
40
+ checksumsSigning?: SignFilesResult;
29
41
  /** Number of archived files. */
30
42
  files: number;
31
43
  }
@@ -1,8 +1,13 @@
1
- import { existsSync } from 'node:fs';
2
- import { CHECKSUMS_EXTENSION, createChecksums, writeChecksums, } from "./checksums.js";
1
+ import { mkdir, mkdtemp, readFile, rm, writeFile } from 'node:fs/promises';
2
+ import { tmpdir } from 'node:os';
3
+ import { basename, dirname, join, resolve } from 'node:path';
4
+ import { checkChecksumsFile, resolveManifestCodec, encodeIntegrity, } from "./integrity.js";
5
+ import { checkSigningInput, signFiles, getSignaturePath } from "./signing.js";
6
+ import { CHECKSUMS_EXTENSION, createChecksums } from "./checksums.js";
3
7
  import { ValidationError } from "./errors.js";
4
- import { ignoreOwnFile, ignoreOwnSignature, loadFileRules, selectFiles, } from "./file-selection.js";
5
- import { createZip } from "./zip.js";
8
+ import { checkOutputFiles } from "./output-files.js";
9
+ import { getPackageFilesToRelease } from "./package-files.js";
10
+ import { createZip, isValidRootFolder } from "./zip.js";
6
11
  const ZIP_EXTENSION = '.zip';
7
12
  /**
8
13
  * Get the checksums manifest path next to an archive, with .checksums.json
@@ -17,19 +22,6 @@ export function getChecksumsPath(zipPath) {
17
22
  : zipPath;
18
23
  return name + CHECKSUMS_EXTENSION;
19
24
  }
20
- /**
21
- * Fail before writing anything when an output file exists.
22
- *
23
- * @param paths Output paths.
24
- * @param force Whether existing files are overwritten.
25
- * @throws {ValidationError} If a file exists and force isn't set.
26
- */
27
- function checkOverwrite(paths, force) {
28
- const existing = paths.filter((path) => !force && existsSync(path));
29
- if (existing.length) {
30
- throw new ValidationError(`${existing.join(' and ')} already exists, pass --force to overwrite it.`);
31
- }
32
- }
33
25
  /**
34
26
  * Write a ZIP of the selected files of a plugin, theme or package, and
35
27
  * optionally the checksums manifest of the same files next to it. The zip
@@ -44,35 +36,134 @@ function checkOverwrite(paths, force) {
44
36
  */
45
37
  export async function zipPackage(input) {
46
38
  const { sourceDir, zipPath, force = false } = input;
47
- const checksumsPath = input.checksums ? getChecksumsPath(zipPath) : null;
48
- const outputs = [zipPath, checksumsPath].filter((path) => null !== path);
49
- const rules = await loadFileRules(input.selection);
50
- outputs.forEach((path) => ignoreOwnSignature(ignoreOwnFile(rules, sourceDir, path), sourceDir, path));
51
- const files = await selectFiles(sourceDir, rules);
52
- if (!files.length) {
53
- throw new ValidationError(`No files to archive in ${sourceDir}.`);
39
+ if (input.rootFolder && !isValidRootFolder(input.rootFolder)) {
40
+ throw new ValidationError(`Invalid root folder "${input.rootFolder}", use a single folder name.`);
41
+ }
42
+ const enabled = input.checksums ||
43
+ input.checksumsFile ||
44
+ input.embedChecksums ||
45
+ input.signChecksums;
46
+ const codec = resolveManifestCodec(input.integrity?.codec);
47
+ const file = checkChecksumsFile(input.checksumsFile ??
48
+ (codec.id === 'wordpress-json'
49
+ ? basename(getChecksumsPath(zipPath))
50
+ : codec.filename));
51
+ const checksumsPath = enabled && !input.embedChecksums
52
+ ? resolve(dirname(zipPath), file)
53
+ : null;
54
+ if (checksumsPath === resolve(zipPath)) {
55
+ throw new ValidationError('The ZIP and checksums file must have different destinations.');
54
56
  }
55
- checkOverwrite(outputs, force);
56
- await createZip({
57
+ if (input.signChecksums) {
58
+ checkSigningInput({
59
+ paths: checksumsPath ? [checksumsPath] : [],
60
+ formats: input.signChecksums,
61
+ force,
62
+ });
63
+ }
64
+ const outputs = [zipPath, checksumsPath].filter((path) => null !== path);
65
+ const { files, rules } = await getPackageFilesToRelease({
57
66
  sourceDir,
58
- files,
59
- zipPath,
60
- rootFolder: input.rootFolder,
61
- force,
67
+ selection: input.selection,
68
+ outputs,
69
+ excludeSignatures: true,
70
+ operation: 'archive',
62
71
  });
63
- if (checksumsPath) {
64
- const manifest = await createChecksums({
72
+ checkOutputFiles(outputs, force);
73
+ const entries = {};
74
+ let checksumsSigning;
75
+ let stage;
76
+ const metadata = [
77
+ file,
78
+ ...['wordpress', 'minisign', 'ssh'].map((format) => getSignaturePath(file, format)),
79
+ ];
80
+ if (enabled &&
81
+ input.embedChecksums &&
82
+ metadata.some((path) => files.some((selected) => selected === path ||
83
+ selected.startsWith(path + '/') ||
84
+ path.startsWith(selected + '/')))) {
85
+ throw new ValidationError('Embedded checksums collide with selected source files. Remove the generated metadata or exclude it from the payload.');
86
+ }
87
+ try {
88
+ if (enabled) {
89
+ const manifest = await createChecksums({
90
+ sourceDir,
91
+ rules,
92
+ slug: input.slug,
93
+ version: input.version ?? null,
94
+ type: input.type,
95
+ });
96
+ const algorithms = input.integrity?.algorithms;
97
+ if (algorithms) {
98
+ if (!algorithms.length ||
99
+ algorithms.some((algorithm) => !['md5', 'sha256'].includes(algorithm))) {
100
+ throw new ValidationError('Checksums algorithms must be md5 and/or sha256.');
101
+ }
102
+ Object.values(manifest.files).forEach((hashes) => {
103
+ if (!algorithms.includes('md5')) {
104
+ delete hashes.md5;
105
+ }
106
+ if (!algorithms.includes('sha256')) {
107
+ delete hashes.sha256;
108
+ }
109
+ });
110
+ }
111
+ const bytes = await encodeIntegrity(codec, manifest);
112
+ stage = await mkdtemp(join(tmpdir(), 'wp-release-integrity-'));
113
+ const stagedFile = join(stage, file);
114
+ await mkdir(dirname(stagedFile), { recursive: true });
115
+ await writeFile(stagedFile, bytes);
116
+ if (input.signChecksums) {
117
+ checksumsSigning = await signFiles({
118
+ paths: [stagedFile],
119
+ formats: input.signChecksums,
120
+ });
121
+ }
122
+ if (input.embedChecksums) {
123
+ entries[file] = bytes;
124
+ for (const format of input.signChecksums ?? []) {
125
+ entries[getSignaturePath(file, format)] =
126
+ await readFile(getSignaturePath(stagedFile, format));
127
+ }
128
+ }
129
+ else if (checksumsPath) {
130
+ await mkdir(dirname(checksumsPath), { recursive: true });
131
+ await writeFile(checksumsPath, bytes, {
132
+ flag: force ? 'w' : 'wx',
133
+ });
134
+ for (const format of input.signChecksums ?? []) {
135
+ await writeFile(getSignaturePath(checksumsPath, format), await readFile(getSignaturePath(stagedFile, format)), { flag: force ? 'w' : 'wx' });
136
+ }
137
+ }
138
+ }
139
+ await mkdir(dirname(zipPath), { recursive: true });
140
+ await createZip({
65
141
  sourceDir,
66
- rules,
67
- slug: input.slug,
68
- version: input.version ?? null,
69
- type: input.type,
142
+ files,
143
+ entries,
144
+ zipPath,
145
+ rootFolder: input.rootFolder,
146
+ force,
70
147
  });
71
- await writeChecksums(checksumsPath, manifest, force);
72
148
  }
149
+ finally {
150
+ if (stage) {
151
+ await rm(stage, { recursive: true, force: true });
152
+ }
153
+ }
154
+ const signatures = (input.signChecksums ?? []).map((format) => getSignaturePath(checksumsPath ?? file, format));
73
155
  return {
74
156
  zipPath,
75
157
  ...(checksumsPath ? { checksumsPath } : {}),
76
- files: files.length,
158
+ ...(enabled && input.embedChecksums
159
+ ? { checksumsEmbedded: file }
160
+ : {}),
161
+ ...(checksumsSigning
162
+ ? {
163
+ checksumsSigning: { ...checksumsSigning, signatures },
164
+ checksumsSignatures: signatures,
165
+ }
166
+ : {}),
167
+ files: files.length + Object.keys(entries).length,
77
168
  };
78
169
  }
@@ -12,6 +12,12 @@ import { compareVersions, isPrerelease, isVersion } from "./versions.js";
12
12
  * @throws {ValidationError} For unknown packages, or an expected version without a single package.
13
13
  */
14
14
  export function selectTagPackages(packages, names, version) {
15
+ if (undefined !== version &&
16
+ !names.length &&
17
+ 1 === packages.length &&
18
+ '.' === packages[0]?.path) {
19
+ names = [packages[0].name];
20
+ }
15
21
  if (undefined !== version && 1 !== names.length) {
16
22
  throw new ValidationError('An expected version requires exactly one package.');
17
23
  }
package/dist/signing.d.ts CHANGED
@@ -119,6 +119,42 @@ export declare function getPublicKeys(key: KeyObject): Record<SignatureFormat, s
119
119
  * @throws {ValidationError} If the signature file exists and force isn't set.
120
120
  */
121
121
  export declare function writeSignature(path: string, privateKey: KeyObject, force?: boolean, format?: SignatureFormat): Promise<string>;
122
+ export interface SignFilesInput {
123
+ /** Absolute paths of the files to sign. */
124
+ paths: string[];
125
+ /** Signature formats to write, the WordPress .sig by default. */
126
+ formats?: SignatureFormat[];
127
+ /** Base64 signing key, from the WP_RELEASE_SIGNING_KEY environment variable by default. */
128
+ key?: string;
129
+ /** Overwrite existing signature files. */
130
+ force?: boolean;
131
+ }
132
+ export interface SignFilesResult {
133
+ /** Absolute paths of the written signature files. */
134
+ signatures: string[];
135
+ /** Base64 public key of the signing key. */
136
+ publicKey: string;
137
+ /** Public key of the signing key in the form of each signature format. */
138
+ publicKeys: Record<SignatureFormat, string>;
139
+ }
140
+ /**
141
+ * Validate the signing key and destinations before writing files. The input
142
+ * files need not exist yet, so zip can check before creating its artifacts.
143
+ *
144
+ * @param input Files, formats, key and whether to overwrite.
145
+ * @return Parsed signing key.
146
+ * @throws {ValidationError} Without a valid key or when a signature already exists without force.
147
+ */
148
+ export declare function checkSigningInput(input: SignFilesInput): KeyObject;
149
+ /**
150
+ * Sign files with a signature file of each format next to each, in order,
151
+ * so that a failure leaves a predictable set of files.
152
+ *
153
+ * @param input Files, formats, key and whether to overwrite.
154
+ * @return The written signature files and the public keys.
155
+ * @throws {ValidationError} Without a key, for an invalid key, a missing file or an existing signature file without force.
156
+ */
157
+ export declare function signFiles(input: SignFilesInput): Promise<SignFilesResult>;
122
158
  /**
123
159
  * Check a file against a signature file. A wordpress .sig file can hold one
124
160
  * signature per line, and is valid when any of them match, so that a file
package/dist/signing.js CHANGED
@@ -1,5 +1,6 @@
1
1
  import { createPrivateKey, generateKeyPairSync, sign, verify, } from 'node:crypto';
2
- import { readFile, writeFile } from 'node:fs/promises';
2
+ import { existsSync } from 'node:fs';
3
+ import { readFile, stat, writeFile } from 'node:fs/promises';
3
4
  import { PUBLIC_KEY_BYTES, createRawPublicKey, digestFile, getRawPublicKey, } from "./ed25519.js";
4
5
  import { ValidationError } from "./errors.js";
5
6
  import { MINISIGN_EXTENSION, createMinisignSignature, getMinisignPublicKey, parseMinisignPublicKey, verifyMinisignSignature, } from "./minisign.js";
@@ -214,6 +215,56 @@ export async function writeSignature(path, privateKey, force = false, format = D
214
215
  });
215
216
  return signaturePath;
216
217
  }
218
+ /**
219
+ * Validate the signing key and destinations before writing files. The input
220
+ * files need not exist yet, so zip can check before creating its artifacts.
221
+ *
222
+ * @param input Files, formats, key and whether to overwrite.
223
+ * @return Parsed signing key.
224
+ * @throws {ValidationError} Without a valid key or when a signature already exists without force.
225
+ */
226
+ export function checkSigningInput(input) {
227
+ const encoded = input.key ?? process.env[SIGNING_KEY_ENV];
228
+ const formats = input.formats ?? [DEFAULT_SIGNATURE_FORMAT];
229
+ if (!encoded?.trim()) {
230
+ throw new ValidationError(`Set the ${SIGNING_KEY_ENV} environment variable to the base64 Ed25519 signing key.`);
231
+ }
232
+ const privateKey = parseSigningKey(encoded);
233
+ const existing = input.paths
234
+ .flatMap((path) => formats.map((format) => getSignaturePath(path, format)))
235
+ .filter((path) => !input.force && existsSync(path));
236
+ if (existing.length) {
237
+ throw new ValidationError(`${existing.join(' and ')} already exists, pass --force to overwrite it.`);
238
+ }
239
+ return privateKey;
240
+ }
241
+ /**
242
+ * Sign files with a signature file of each format next to each, in order,
243
+ * so that a failure leaves a predictable set of files.
244
+ *
245
+ * @param input Files, formats, key and whether to overwrite.
246
+ * @return The written signature files and the public keys.
247
+ * @throws {ValidationError} Without a key, for an invalid key, a missing file or an existing signature file without force.
248
+ */
249
+ export async function signFiles(input) {
250
+ const formats = input.formats ?? [DEFAULT_SIGNATURE_FORMAT];
251
+ const privateKey = checkSigningInput(input);
252
+ await Promise.all(input.paths.map(async (path) => {
253
+ if (!(await stat(path).catch(() => null))?.isFile()) {
254
+ throw new ValidationError(`${path} is not a file.`);
255
+ }
256
+ }));
257
+ const signatures = [];
258
+ const jobs = input.paths.flatMap((path) => formats.map((format) => ({ path, format })));
259
+ for (const { path, format } of jobs) {
260
+ signatures.push(await writeSignature(path, privateKey, input.force, format));
261
+ }
262
+ return {
263
+ signatures,
264
+ publicKey: getPublicKey(privateKey),
265
+ publicKeys: getPublicKeys(privateKey),
266
+ };
267
+ }
217
268
  /**
218
269
  * Check a file against a signature file. A wordpress .sig file can hold one
219
270
  * signature per line, and is valid when any of them match, so that a file
@@ -0,0 +1,32 @@
1
+ import type { ChangesetConfig, PackageVersionSource } from './changeset-config.ts';
2
+ /** Options used only when changesets have no JavaScript release config. */
3
+ export interface SingleProjectOptions {
4
+ /** Explicit descriptors replace automatic version detection. Git tags are source-only. */
5
+ versions?: PackageVersionSource[];
6
+ /** Descriptor kind, optionally qualified with :file or :file#constant. */
7
+ versionFrom?: string;
8
+ baseBranch?: string;
9
+ remote?: string;
10
+ changesets?: string;
11
+ readmeChangelog?: boolean;
12
+ changelogInsertAfter?: string;
13
+ }
14
+ /** CLI names of version descriptor kinds. */
15
+ export declare const VERSION_KINDS: {
16
+ readonly 'package-json': "package-json";
17
+ readonly 'composer-json': "composer-json";
18
+ readonly 'wordpress-plugin': "wordpress-plugin-header";
19
+ readonly 'wordpress-theme': "wordpress-theme-header";
20
+ readonly 'wordpress-stable-tag': "wordpress-stable-tag";
21
+ readonly 'php-constant': "php-constant";
22
+ readonly 'git-tag': "git-tag";
23
+ };
24
+ /**
25
+ * Build the root package config, without creating a configuration file.
26
+ *
27
+ * @param cwd Project root directory.
28
+ * @param options Explicit descriptors and shared release settings.
29
+ * @param tagFormat Tag format override, otherwise v{version}.
30
+ * @return Root package config, or undefined without a version source.
31
+ */
32
+ export declare function resolveSingleProjectConfig(cwd: string, options?: SingleProjectOptions, tagFormat?: string): Promise<ChangesetConfig | undefined>;