@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.
- package/README.md +246 -37
- package/dist/changeset-config.d.ts +3 -0
- package/dist/changeset-release.d.ts +9 -3
- package/dist/changeset-release.js +23 -14
- package/dist/cli.d.ts +1 -0
- package/dist/cli.js +2 -0
- package/dist/commands/artifact-signing.d.ts +20 -0
- package/dist/commands/artifact-signing.js +42 -0
- package/dist/commands/build.d.ts +32 -0
- package/dist/commands/build.js +91 -0
- package/dist/commands/changeset-add.d.ts +55 -6
- package/dist/commands/changeset-add.js +12 -3
- package/dist/commands/changeset-args.js +2 -2
- package/dist/commands/changeset-project-args.d.ts +63 -0
- package/dist/commands/changeset-project-args.js +114 -0
- package/dist/commands/changeset-status.d.ts +50 -1
- package/dist/commands/changeset-status.js +4 -2
- package/dist/commands/changeset-tag.d.ts +55 -6
- package/dist/commands/changeset-tag.js +7 -4
- package/dist/commands/changeset-version.d.ts +55 -6
- package/dist/commands/changeset-version.js +4 -2
- package/dist/commands/checksums.d.ts +4 -1
- package/dist/commands/checksums.js +27 -6
- package/dist/commands/package-source.d.ts +43 -0
- package/dist/commands/package-source.js +101 -0
- package/dist/commands/verify.d.ts +7 -1
- package/dist/commands/verify.js +53 -17
- package/dist/commands/zip.d.ts +20 -8
- package/dist/commands/zip.js +109 -124
- package/dist/index.d.ts +6 -0
- package/dist/index.js +2 -0
- package/dist/integrity.d.ts +33 -0
- package/dist/integrity.js +115 -0
- package/dist/output-files.d.ts +7 -0
- package/dist/output-files.js +25 -0
- package/dist/package-build.d.ts +33 -0
- package/dist/package-build.js +112 -0
- package/dist/package-files.d.ts +25 -0
- package/dist/package-files.js +24 -0
- package/dist/package-zip.d.ts +14 -2
- package/dist/package-zip.js +129 -38
- package/dist/release-tags.js +6 -0
- package/dist/single-project.d.ts +32 -0
- package/dist/single-project.js +144 -0
- package/dist/summary.d.ts +9 -0
- package/dist/summary.js +25 -0
- package/dist/zip.d.ts +3 -0
- package/dist/zip.js +16 -6
- package/package.json +6 -1
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { lstatSync } from 'node:fs';
|
|
2
|
+
import { ValidationError } from "./errors.js";
|
|
3
|
+
import { isErrorCode } from "./file-selection.js";
|
|
4
|
+
/**
|
|
5
|
+
* Validate generated file destinations before writing release artifacts.
|
|
6
|
+
*
|
|
7
|
+
* @param paths Output file paths.
|
|
8
|
+
* @param force Whether existing regular files can be overwritten.
|
|
9
|
+
*/
|
|
10
|
+
export function checkOutputFiles(paths, force) {
|
|
11
|
+
for (const path of paths) {
|
|
12
|
+
let existing;
|
|
13
|
+
try {
|
|
14
|
+
existing = lstatSync(path);
|
|
15
|
+
}
|
|
16
|
+
catch (error) {
|
|
17
|
+
if (!isErrorCode(error, 'ENOENT')) {
|
|
18
|
+
throw error;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
if (existing && (!force || !existing.isFile())) {
|
|
22
|
+
throw new ValidationError(`${path} already exists, pass --force to overwrite a regular file.`);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import type { PackageType } from './checksums.ts';
|
|
2
|
+
import type { FileSelection } from './file-selection.ts';
|
|
3
|
+
export interface BuildPackageInput {
|
|
4
|
+
sourceDir: string;
|
|
5
|
+
selection: FileSelection;
|
|
6
|
+
outputDir: string;
|
|
7
|
+
force?: boolean;
|
|
8
|
+
/** Write a checksums manifest alongside the build directory. */
|
|
9
|
+
checksums?: boolean;
|
|
10
|
+
/** Slug and version for the manifest. */
|
|
11
|
+
slug?: string;
|
|
12
|
+
version?: string | null;
|
|
13
|
+
type?: PackageType;
|
|
14
|
+
}
|
|
15
|
+
export interface BuildPackageResult {
|
|
16
|
+
outputDir: string;
|
|
17
|
+
files: number;
|
|
18
|
+
checksumsPath?: string;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Get the manifest path alongside a build directory.
|
|
22
|
+
*
|
|
23
|
+
* @param outputDir Build directory path.
|
|
24
|
+
* @return Absolute manifest path.
|
|
25
|
+
*/
|
|
26
|
+
export declare function getBuildChecksumsPath(outputDir: string): string;
|
|
27
|
+
/**
|
|
28
|
+
* Copy the release selection into a directory, replacing an existing build only with force.
|
|
29
|
+
*
|
|
30
|
+
* @param input Source selection, destination and overwrite option.
|
|
31
|
+
* @return Destination and number of copied files.
|
|
32
|
+
*/
|
|
33
|
+
export declare function buildPackage(input: BuildPackageInput): Promise<BuildPackageResult>;
|
|
@@ -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
|
+
}
|
package/dist/package-zip.d.ts
CHANGED
|
@@ -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
|
-
/**
|
|
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
|
|
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
|
}
|
package/dist/package-zip.js
CHANGED
|
@@ -1,8 +1,13 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
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 {
|
|
5
|
-
import {
|
|
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
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
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
|
-
|
|
56
|
-
|
|
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
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
67
|
+
selection: input.selection,
|
|
68
|
+
outputs,
|
|
69
|
+
excludeSignatures: true,
|
|
70
|
+
operation: 'archive',
|
|
62
71
|
});
|
|
63
|
-
|
|
64
|
-
|
|
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
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
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
|
-
|
|
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
|
}
|
package/dist/release-tags.js
CHANGED
|
@@ -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
|
}
|
|
@@ -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>;
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
import { readdir } from 'node:fs/promises';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { packageJsonVersion, composerJsonVersion, wordpressPluginVersion, wordpressThemeVersion, wordpressStableTag, } from "./changeset-config.js";
|
|
4
|
+
import { ValidationError } from "./errors.js";
|
|
5
|
+
import { readJsonIfExists } from "./json-files.js";
|
|
6
|
+
import { parseFileHeaders, readFileStart } from "./package-meta.js";
|
|
7
|
+
/** CLI names of version descriptor kinds. */
|
|
8
|
+
export const VERSION_KINDS = {
|
|
9
|
+
'package-json': 'package-json',
|
|
10
|
+
'composer-json': 'composer-json',
|
|
11
|
+
'wordpress-plugin': 'wordpress-plugin-header',
|
|
12
|
+
'wordpress-theme': 'wordpress-theme-header',
|
|
13
|
+
'wordpress-stable-tag': 'wordpress-stable-tag',
|
|
14
|
+
'php-constant': 'php-constant',
|
|
15
|
+
'git-tag': 'git-tag',
|
|
16
|
+
};
|
|
17
|
+
function descriptorId(descriptor) {
|
|
18
|
+
if ('git-tag' === descriptor.kind) {
|
|
19
|
+
return descriptor.pattern ?? '';
|
|
20
|
+
}
|
|
21
|
+
return 'php-constant' === descriptor.kind
|
|
22
|
+
? `${descriptor.file}#${descriptor.constant}`
|
|
23
|
+
: descriptor.file ?? '';
|
|
24
|
+
}
|
|
25
|
+
function selectSource(versions, selector) {
|
|
26
|
+
let candidates = versions;
|
|
27
|
+
if (selector) {
|
|
28
|
+
const separator = selector.indexOf(':');
|
|
29
|
+
const kind = separator < 0 ? selector : selector.slice(0, separator);
|
|
30
|
+
const id = separator < 0 ? undefined : selector.slice(separator + 1);
|
|
31
|
+
const descriptorKind = VERSION_KINDS[kind];
|
|
32
|
+
candidates = versions.filter((descriptor) => descriptor.kind === descriptorKind &&
|
|
33
|
+
(undefined === id || descriptorId(descriptor) === id));
|
|
34
|
+
}
|
|
35
|
+
else if (versions.length > 1) {
|
|
36
|
+
// A Stable tag can accompany a unique authoritative version file.
|
|
37
|
+
candidates = versions.filter((descriptor) => 'wordpress-stable-tag' !== descriptor.kind);
|
|
38
|
+
}
|
|
39
|
+
if (candidates.length !== 1) {
|
|
40
|
+
throw new ValidationError('Cannot choose the current version source. Pass --version-from <kind> or --version-from <kind>:<file> (PHP constants use <file>#<constant>), or add a wp-release config.');
|
|
41
|
+
}
|
|
42
|
+
return candidates[0];
|
|
43
|
+
}
|
|
44
|
+
async function detectVersions(cwd) {
|
|
45
|
+
const versions = [];
|
|
46
|
+
const entries = await readdir(cwd, { withFileTypes: true });
|
|
47
|
+
for (const entry of entries
|
|
48
|
+
.filter((item) => item.isFile() &&
|
|
49
|
+
(item.name.endsWith('.php') || 'style.css' === item.name))
|
|
50
|
+
.sort((a, b) => a.name.localeCompare(b.name))) {
|
|
51
|
+
const contents = await readFileStart(path.join(cwd, entry.name));
|
|
52
|
+
const theme = 'style.css' === entry.name;
|
|
53
|
+
const nameHeader = theme ? 'Theme Name' : 'Plugin Name';
|
|
54
|
+
const headers = parseFileHeaders({
|
|
55
|
+
contents: contents ?? '',
|
|
56
|
+
headers: [nameHeader, 'Version'],
|
|
57
|
+
});
|
|
58
|
+
if (headers[nameHeader]) {
|
|
59
|
+
versions.push(theme
|
|
60
|
+
? wordpressThemeVersion({ file: entry.name })
|
|
61
|
+
: wordpressPluginVersion({ file: entry.name }));
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
for (const file of ['package.json', 'composer.json']) {
|
|
65
|
+
const manifest = await readJsonIfExists(path.join(cwd, file)).catch((error) => {
|
|
66
|
+
throw new ValidationError(`Could not read ${file}: ${error instanceof Error ? error.message : String(error)}`);
|
|
67
|
+
});
|
|
68
|
+
if (manifest && 'package.json' === file && manifest.workspaces) {
|
|
69
|
+
throw new ValidationError('Automatic changeset configuration is for a single root project. Configure workspace packages in a wp-release config or pass explicit --version-* descriptors.');
|
|
70
|
+
}
|
|
71
|
+
if (manifest &&
|
|
72
|
+
'string' === typeof manifest.version &&
|
|
73
|
+
manifest.version.trim()) {
|
|
74
|
+
versions.push('package.json' === file
|
|
75
|
+
? packageJsonVersion()
|
|
76
|
+
: composerJsonVersion());
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
if (versions.length > 1) {
|
|
80
|
+
throw new ValidationError('Cannot choose among multiple root version sources. Pass explicit --version-* descriptors or add a wp-release config.');
|
|
81
|
+
}
|
|
82
|
+
if (1 === versions.length &&
|
|
83
|
+
'wordpress-plugin-header' === versions[0]?.kind) {
|
|
84
|
+
const readmes = await Promise.all(['readme.txt', 'readme.md'].map(async (file) => {
|
|
85
|
+
const contents = await readFileStart(path.join(cwd, file));
|
|
86
|
+
const headers = parseFileHeaders({
|
|
87
|
+
contents: contents ?? '',
|
|
88
|
+
headers: ['Stable tag'],
|
|
89
|
+
});
|
|
90
|
+
return headers['Stable tag'] ? file : undefined;
|
|
91
|
+
}));
|
|
92
|
+
const readme = readmes.find(Boolean);
|
|
93
|
+
if (readme) {
|
|
94
|
+
versions.push(wordpressStableTag({ file: readme }));
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
return versions;
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* Build the root package config, without creating a configuration file.
|
|
101
|
+
*
|
|
102
|
+
* @param cwd Project root directory.
|
|
103
|
+
* @param options Explicit descriptors and shared release settings.
|
|
104
|
+
* @param tagFormat Tag format override, otherwise v{version}.
|
|
105
|
+
* @return Root package config, or undefined without a version source.
|
|
106
|
+
*/
|
|
107
|
+
export async function resolveSingleProjectConfig(cwd, options = {}, tagFormat) {
|
|
108
|
+
const versions = options.versions ?? (await detectVersions(cwd));
|
|
109
|
+
if (!versions.length) {
|
|
110
|
+
return undefined;
|
|
111
|
+
}
|
|
112
|
+
const ids = versions.map((descriptor) => `${descriptor.kind}:${descriptorId(descriptor)}`);
|
|
113
|
+
if (new Set(ids).size !== ids.length) {
|
|
114
|
+
throw new ValidationError('A version descriptor was supplied more than once. Pass each --version-* file or constant only once.');
|
|
115
|
+
}
|
|
116
|
+
const from = selectSource(versions, options.versionFrom);
|
|
117
|
+
const to = versions.filter((descriptor) => 'git-tag' !== descriptor.kind);
|
|
118
|
+
// Composer targets preserve the existing optional-version convention.
|
|
119
|
+
const targets = to.map((descriptor) => 'composer-json' === descriptor.kind
|
|
120
|
+
? { ...descriptor, ifPresent: descriptor.ifPresent ?? true }
|
|
121
|
+
: descriptor);
|
|
122
|
+
let idFrom = 'path';
|
|
123
|
+
if ('package-json' === from.kind && 'package.json' === from.file) {
|
|
124
|
+
idFrom = 'package.json#name';
|
|
125
|
+
}
|
|
126
|
+
else if ('composer-json' === from.kind &&
|
|
127
|
+
'composer.json' === from.file) {
|
|
128
|
+
idFrom = 'composer.json#name';
|
|
129
|
+
}
|
|
130
|
+
return {
|
|
131
|
+
...(options.baseBranch ? { baseBranch: options.baseBranch } : {}),
|
|
132
|
+
...(options.remote ? { remote: options.remote } : {}),
|
|
133
|
+
...(undefined !== options.readmeChangelog
|
|
134
|
+
? { readmeChangelog: options.readmeChangelog }
|
|
135
|
+
: {}),
|
|
136
|
+
...(options.changelogInsertAfter
|
|
137
|
+
? { changelogInsertAfter: options.changelogInsertAfter }
|
|
138
|
+
: {}),
|
|
139
|
+
root: '.',
|
|
140
|
+
changesets: options.changesets ?? '.changeset',
|
|
141
|
+
tagFormat: tagFormat ?? 'v{version}',
|
|
142
|
+
packages: [{ glob: '.', idFrom, version: { from, to: targets } }],
|
|
143
|
+
};
|
|
144
|
+
}
|
package/dist/summary.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { ChecksumsResult } from './commands/checksums.ts';
|
|
2
2
|
import type { SignResult } from './commands/sign.ts';
|
|
3
|
+
import type { BuildPackageResult } from './package-build.ts';
|
|
3
4
|
import type { ZipPackageResult } from './package-zip.ts';
|
|
4
5
|
import type { ChangesetTagResult } from './changeset-release.ts';
|
|
5
6
|
import type { PackageRelease } from './changesets.ts';
|
|
@@ -69,6 +70,14 @@ export declare function formatTable(header: string[], rows: string[][]): string;
|
|
|
69
70
|
* @return Markdown code.
|
|
70
71
|
*/
|
|
71
72
|
export declare function formatFile(context: SummaryContext, file: string, line?: number | null): string;
|
|
73
|
+
/**
|
|
74
|
+
* Get the summary of a directory build.
|
|
75
|
+
*
|
|
76
|
+
* @param context Workspace directory.
|
|
77
|
+
* @param result Output directory and number of copied files.
|
|
78
|
+
* @return Markdown summary.
|
|
79
|
+
*/
|
|
80
|
+
export declare function getBuildSummary(context: SummaryContext, result: BuildPackageResult): string;
|
|
72
81
|
/**
|
|
73
82
|
* Get the summary of a ZIP.
|
|
74
83
|
*
|