@wpelevator/wp-release 0.0.0-stage → 0.1.1
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/LICENSE +21 -0
- package/README.md +316 -2
- package/dist/bin/wp-release.d.ts +2 -0
- package/dist/bin/wp-release.js +3 -0
- package/dist/checksums.d.ts +138 -0
- package/dist/checksums.js +216 -0
- package/dist/cli.d.ts +42 -0
- package/dist/cli.js +163 -0
- package/dist/commands/args.d.ts +81 -0
- package/dist/commands/args.js +116 -0
- package/dist/commands/checksums.d.ts +34 -0
- package/dist/commands/checksums.js +76 -0
- package/dist/commands/command.d.ts +63 -0
- package/dist/commands/command.js +33 -0
- package/dist/commands/deploy-svn.d.ts +74 -0
- package/dist/commands/deploy-svn.js +103 -0
- package/dist/commands/verify.d.ts +27 -0
- package/dist/commands/verify.js +60 -0
- package/dist/commands/version-check.d.ts +38 -0
- package/dist/commands/version-check.js +80 -0
- package/dist/commands/zip.d.ts +41 -0
- package/dist/commands/zip.js +138 -0
- package/dist/deploy-options.d.ts +136 -0
- package/dist/deploy-options.js +168 -0
- package/dist/errors.d.ts +16 -0
- package/dist/errors.js +16 -0
- package/dist/file-selection.d.ts +78 -0
- package/dist/file-selection.js +133 -0
- package/dist/filename.d.ts +17 -0
- package/dist/filename.js +23 -0
- package/dist/index.d.ts +30 -0
- package/dist/index.js +15 -0
- package/dist/logger.d.ts +42 -0
- package/dist/logger.js +56 -0
- package/dist/package-meta.d.ts +112 -0
- package/dist/package-meta.js +222 -0
- package/dist/svn-deploy.d.ts +208 -0
- package/dist/svn-deploy.js +485 -0
- package/dist/svn.d.ts +209 -0
- package/dist/svn.js +298 -0
- package/dist/sync.d.ts +32 -0
- package/dist/sync.js +90 -0
- package/dist/unzip.d.ts +45 -0
- package/dist/unzip.js +121 -0
- package/dist/version-check.d.ts +75 -0
- package/dist/version-check.js +193 -0
- package/dist/zip.d.ts +32 -0
- package/dist/zip.js +53 -0
- package/package.json +61 -5
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import { existsSync } from 'node:fs';
|
|
2
|
+
import { basename, resolve } from 'node:path';
|
|
3
|
+
import { CHECKSUMS_EXTENSION, createChecksums, writeChecksums, } from "../checksums.js";
|
|
4
|
+
import { ValidationError } from "../errors.js";
|
|
5
|
+
import { loadFileRules, resolveFileSelection, selectFiles, } from "../file-selection.js";
|
|
6
|
+
import { FILENAME_FORMAT, formatFilename } from "../filename.js";
|
|
7
|
+
import { readPackageMeta } from "../package-meta.js";
|
|
8
|
+
import { createZip } from "../zip.js";
|
|
9
|
+
import { checkDirectory, fileSelectionArgs, getFileSelectionInput, getSourceDirArg, ignoreOwnFile, resolveOutput, sourceDirArgs, } from "./args.js";
|
|
10
|
+
import { getBoolean, getString } from "./command.js";
|
|
11
|
+
const ZIP_EXTENSION = '.zip';
|
|
12
|
+
/**
|
|
13
|
+
* Get the checksums manifest path next to an archive, with .checksums.json
|
|
14
|
+
* instead of .zip.
|
|
15
|
+
*
|
|
16
|
+
* @param zipPath Archive path.
|
|
17
|
+
* @return Manifest path.
|
|
18
|
+
*/
|
|
19
|
+
function getChecksumsPath(zipPath) {
|
|
20
|
+
const name = zipPath.endsWith(ZIP_EXTENSION)
|
|
21
|
+
? zipPath.slice(0, -ZIP_EXTENSION.length)
|
|
22
|
+
: zipPath;
|
|
23
|
+
return name + CHECKSUMS_EXTENSION;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Fail before writing anything when an output file exists.
|
|
27
|
+
*
|
|
28
|
+
* @param paths Output paths.
|
|
29
|
+
* @param force Whether existing files are overwritten.
|
|
30
|
+
* @throws {ValidationError} If a file exists and force isn't set.
|
|
31
|
+
*/
|
|
32
|
+
function checkOverwrite(paths, force) {
|
|
33
|
+
const existing = paths.filter((path) => !force && existsSync(path));
|
|
34
|
+
if (existing.length) {
|
|
35
|
+
throw new ValidationError(`${existing.join(' and ')} already exists, pass --force to overwrite it.`);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
export default {
|
|
39
|
+
name: 'zip',
|
|
40
|
+
usage: 'zip [<source-dir>] [options]',
|
|
41
|
+
summary: 'Create a ZIP archive of a plugin or theme with a {slug}/ root folder.',
|
|
42
|
+
help: `Arguments:
|
|
43
|
+
<source-dir> Directory to archive (default: .), also accepted
|
|
44
|
+
as --source-dir <dir>
|
|
45
|
+
|
|
46
|
+
Options:
|
|
47
|
+
--output <file> ZIP file (default: the --filename-format name with
|
|
48
|
+
.zip in the current directory)
|
|
49
|
+
--slug <slug> Plugin or theme slug (default: project directory name)
|
|
50
|
+
--version <version> Version for the filename (default: Version header)
|
|
51
|
+
--root-folder <name> Folder inside the ZIP (default: --slug)
|
|
52
|
+
--no-root-folder Place files at the ZIP root
|
|
53
|
+
--filename-format <format> Filename with {slug} and {version} (default: {slug}.{version})
|
|
54
|
+
--distignore <file> Ignore file (default: <source-dir>/.distignore when present)
|
|
55
|
+
--no-distignore Don't read any ignore file
|
|
56
|
+
--exclude <patterns> Extra gitignore-style patterns, comma-separated or
|
|
57
|
+
repeated, like build/,assets/*,images/**/*
|
|
58
|
+
--checksums Also write the checksums manifest of the archived
|
|
59
|
+
files next to the ZIP, with .checksums.json
|
|
60
|
+
instead of .zip
|
|
61
|
+
--force Overwrite existing output files
|
|
62
|
+
|
|
63
|
+
Entries are sorted and use a fixed timestamp and file mode, so the same files
|
|
64
|
+
always give the same ZIP.`,
|
|
65
|
+
options: {
|
|
66
|
+
slug: { type: 'string' },
|
|
67
|
+
version: { type: 'string' },
|
|
68
|
+
'root-folder': { type: 'string' },
|
|
69
|
+
'no-root-folder': { type: 'boolean' },
|
|
70
|
+
'filename-format': { type: 'string' },
|
|
71
|
+
output: { type: 'string' },
|
|
72
|
+
...sourceDirArgs,
|
|
73
|
+
...fileSelectionArgs,
|
|
74
|
+
checksums: { type: 'boolean' },
|
|
75
|
+
force: { type: 'boolean' },
|
|
76
|
+
},
|
|
77
|
+
async run({ values, positionals, cwd, logger }) {
|
|
78
|
+
const sourceDir = resolve(cwd, getSourceDirArg(values, positionals));
|
|
79
|
+
const slug = getString(values, 'slug') ?? basename(cwd);
|
|
80
|
+
const force = getBoolean(values, 'force');
|
|
81
|
+
await checkDirectory(sourceDir);
|
|
82
|
+
const meta = await readPackageMeta({ dir: sourceDir, slug });
|
|
83
|
+
const version = getString(values, 'version') ?? meta?.version ?? null;
|
|
84
|
+
const filename = formatFilename(getString(values, 'filename-format') ?? FILENAME_FORMAT, { slug, version });
|
|
85
|
+
const zipPath = resolveOutput({
|
|
86
|
+
cwd,
|
|
87
|
+
output: getString(values, 'output'),
|
|
88
|
+
filename: filename + ZIP_EXTENSION,
|
|
89
|
+
});
|
|
90
|
+
const checksumsPath = getBoolean(values, 'checksums')
|
|
91
|
+
? getChecksumsPath(zipPath)
|
|
92
|
+
: null;
|
|
93
|
+
const selection = resolveFileSelection({
|
|
94
|
+
cwd,
|
|
95
|
+
sourceDir,
|
|
96
|
+
...getFileSelectionInput(values),
|
|
97
|
+
});
|
|
98
|
+
const rules = ignoreOwnFile(await loadFileRules(selection), sourceDir, zipPath);
|
|
99
|
+
if (checksumsPath) {
|
|
100
|
+
ignoreOwnFile(rules, sourceDir, checksumsPath);
|
|
101
|
+
}
|
|
102
|
+
const files = await selectFiles(sourceDir, rules);
|
|
103
|
+
if (!files.length) {
|
|
104
|
+
throw new ValidationError(`No files to archive in ${sourceDir}.`);
|
|
105
|
+
}
|
|
106
|
+
checkOverwrite([zipPath, checksumsPath ?? ''].filter(Boolean), force);
|
|
107
|
+
await createZip({
|
|
108
|
+
sourceDir,
|
|
109
|
+
files,
|
|
110
|
+
zipPath,
|
|
111
|
+
rootFolder: getBoolean(values, 'no-root-folder')
|
|
112
|
+
? null
|
|
113
|
+
: getString(values, 'root-folder') ?? slug,
|
|
114
|
+
force,
|
|
115
|
+
});
|
|
116
|
+
logger.debug(`Archived ${files.length} files from ${sourceDir}.`);
|
|
117
|
+
logger.info(zipPath);
|
|
118
|
+
if (checksumsPath) {
|
|
119
|
+
const manifest = await createChecksums({
|
|
120
|
+
sourceDir,
|
|
121
|
+
rules,
|
|
122
|
+
slug,
|
|
123
|
+
version,
|
|
124
|
+
type: meta?.type,
|
|
125
|
+
});
|
|
126
|
+
await writeChecksums(checksumsPath, manifest, force);
|
|
127
|
+
logger.info(checksumsPath);
|
|
128
|
+
}
|
|
129
|
+
return {
|
|
130
|
+
exitCode: 0,
|
|
131
|
+
result: {
|
|
132
|
+
zipPath,
|
|
133
|
+
...(checksumsPath ? { checksumsPath } : {}),
|
|
134
|
+
files: files.length,
|
|
135
|
+
},
|
|
136
|
+
};
|
|
137
|
+
},
|
|
138
|
+
};
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
import type { FileSelection } from './file-selection.ts';
|
|
2
|
+
export declare const SVN_URL = "https://plugins.svn.wordpress.org/{slug}/";
|
|
3
|
+
/**
|
|
4
|
+
* Deploy options from the CLI, the GitHub Action or a library caller. Unset
|
|
5
|
+
* options fall back to the defaults in resolveDeployOptions().
|
|
6
|
+
*/
|
|
7
|
+
export interface DeployInput {
|
|
8
|
+
/** Directory that relative paths resolve against. */
|
|
9
|
+
cwd?: string;
|
|
10
|
+
/** Plugin slug, defaults to the cwd directory name. */
|
|
11
|
+
slug?: string;
|
|
12
|
+
/** Version to check, defaults to svnTag, then the Version header of the deployed files. */
|
|
13
|
+
version?: string;
|
|
14
|
+
/** Sync the full source directory to trunk/. */
|
|
15
|
+
trunk?: boolean;
|
|
16
|
+
/** Sync only the readme to trunk/. */
|
|
17
|
+
readmeOnly?: boolean;
|
|
18
|
+
/** Copy trunk/ to tags/<svnTag>. */
|
|
19
|
+
svnTag?: string;
|
|
20
|
+
/** Sync the assets directory, defaults to on when assetsDir is set. */
|
|
21
|
+
assets?: boolean;
|
|
22
|
+
/** Directory with banners, icons and screenshots. */
|
|
23
|
+
assetsDir?: string;
|
|
24
|
+
/** Directory with the plugin files, defaults to cwd. */
|
|
25
|
+
sourceDir?: string;
|
|
26
|
+
/** Ignore file path, or false to not read one. */
|
|
27
|
+
distignore?: string | false;
|
|
28
|
+
/** Extra gitignore-style patterns. */
|
|
29
|
+
exclude?: string | string[];
|
|
30
|
+
/** Readme file name. */
|
|
31
|
+
readme?: string;
|
|
32
|
+
/** ZIP archive to deploy instead of sourceDir. */
|
|
33
|
+
fromZip?: string;
|
|
34
|
+
/** SVN repository URL with a {slug} placeholder. */
|
|
35
|
+
svnUrl?: string;
|
|
36
|
+
/** SVN username, defaults to the SVN_USERNAME env var. */
|
|
37
|
+
svnUsername?: string;
|
|
38
|
+
/** SVN password, defaults to the SVN_PASSWORD env var. */
|
|
39
|
+
svnPassword?: string;
|
|
40
|
+
/** Git ref of the run, like refs/tags/1.2.0. */
|
|
41
|
+
gitRef?: string;
|
|
42
|
+
/** SVN commit message. */
|
|
43
|
+
message?: string;
|
|
44
|
+
/** Replace an existing tag. */
|
|
45
|
+
force?: boolean;
|
|
46
|
+
/** Run everything except the commit. */
|
|
47
|
+
dryRun?: boolean;
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Deploy options with the defaults applied, from resolveDeployOptions().
|
|
51
|
+
*/
|
|
52
|
+
export interface DeployOptions extends FileSelection {
|
|
53
|
+
cwd: string;
|
|
54
|
+
slug: string;
|
|
55
|
+
version: string | null;
|
|
56
|
+
trunk: boolean;
|
|
57
|
+
readmeOnly: boolean;
|
|
58
|
+
svnTag: string | null;
|
|
59
|
+
assets: boolean;
|
|
60
|
+
assetsDir: string | null;
|
|
61
|
+
sourceDir: string;
|
|
62
|
+
readme: string;
|
|
63
|
+
fromZip: string | null;
|
|
64
|
+
svnUrl: string;
|
|
65
|
+
svnUsername: string | null;
|
|
66
|
+
/** Never logged or printed. */
|
|
67
|
+
svnPassword: string | null;
|
|
68
|
+
gitRef: string | null;
|
|
69
|
+
message: string | null;
|
|
70
|
+
force: boolean;
|
|
71
|
+
dryRun: boolean;
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Check if a version has a pre-release suffix like 1.2.0-rc.1.
|
|
75
|
+
*
|
|
76
|
+
* @param version Version string.
|
|
77
|
+
* @return Whether it is a pre-release.
|
|
78
|
+
*/
|
|
79
|
+
export declare function isPrerelease(version: string): boolean;
|
|
80
|
+
/**
|
|
81
|
+
* Strip the leading "v" from a version or tag like v1.2.0, since WordPress.org
|
|
82
|
+
* tags and plugin versions never use the prefix.
|
|
83
|
+
*
|
|
84
|
+
* @param tag Tag name.
|
|
85
|
+
* @return Tag without the prefix.
|
|
86
|
+
*/
|
|
87
|
+
export declare function normalizeTag(tag: string): string;
|
|
88
|
+
/**
|
|
89
|
+
* Check if a git ref points to a tag.
|
|
90
|
+
*
|
|
91
|
+
* @param ref Git ref like refs/tags/1.0.0.
|
|
92
|
+
* @return Whether the ref is a tag.
|
|
93
|
+
*/
|
|
94
|
+
export declare function isTagRef(ref?: string | null): boolean;
|
|
95
|
+
/**
|
|
96
|
+
* Check if a slug is a valid WordPress.org plugin slug, so that it's safe in
|
|
97
|
+
* SVN URLs and paths.
|
|
98
|
+
*
|
|
99
|
+
* @param slug Plugin slug.
|
|
100
|
+
* @return Whether the slug is valid.
|
|
101
|
+
*/
|
|
102
|
+
export declare function isValidSlug(slug: string): boolean;
|
|
103
|
+
/**
|
|
104
|
+
* Check if a version can be used as an SVN tag directory name, without path
|
|
105
|
+
* separators or "." and ".." names.
|
|
106
|
+
*
|
|
107
|
+
* @param version Version string.
|
|
108
|
+
* @return Whether the version is valid.
|
|
109
|
+
*/
|
|
110
|
+
export declare function isValidVersion(version: string): boolean;
|
|
111
|
+
/**
|
|
112
|
+
* Collect the problems with resolved deploy options.
|
|
113
|
+
*
|
|
114
|
+
* @param options Resolved options.
|
|
115
|
+
* @return Error messages, empty when valid.
|
|
116
|
+
*/
|
|
117
|
+
export declare function getDeployOptionErrors(options: DeployOptions): string[];
|
|
118
|
+
/**
|
|
119
|
+
* Collect the notices about valid but unusual deploy options.
|
|
120
|
+
*
|
|
121
|
+
* @param options Resolved options.
|
|
122
|
+
* @return Warning messages.
|
|
123
|
+
*/
|
|
124
|
+
export declare function getDeployOptionWarnings(options: DeployOptions): string[];
|
|
125
|
+
/**
|
|
126
|
+
* Apply defaults to deploy options and validate them. Trunk, readme and tag
|
|
127
|
+
* writes happen only when requested, and assets are synced whenever an
|
|
128
|
+
* assets directory is set, unless turned off. The version defaults to the
|
|
129
|
+
* SVN tag; without either, SvnDeploy reads the Version header of the
|
|
130
|
+
* deployed files, which can be in a ZIP.
|
|
131
|
+
*
|
|
132
|
+
* @param input Options from the CLI, action or library caller.
|
|
133
|
+
* @return Resolved options.
|
|
134
|
+
* @throws {ValidationError} When the options are invalid.
|
|
135
|
+
*/
|
|
136
|
+
export declare function resolveDeployOptions(input?: DeployInput): DeployOptions;
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
import { existsSync } from 'node:fs';
|
|
2
|
+
import { basename, resolve } from 'node:path';
|
|
3
|
+
import { ValidationError } from "./errors.js";
|
|
4
|
+
import { resolveFileSelection } from "./file-selection.js";
|
|
5
|
+
export const SVN_URL = 'https://plugins.svn.wordpress.org/{slug}/';
|
|
6
|
+
/**
|
|
7
|
+
* Check if a version has a pre-release suffix like 1.2.0-rc.1.
|
|
8
|
+
*
|
|
9
|
+
* @param version Version string.
|
|
10
|
+
* @return Whether it is a pre-release.
|
|
11
|
+
*/
|
|
12
|
+
export function isPrerelease(version) {
|
|
13
|
+
return /^[^-+]+-/.test(version);
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Strip the leading "v" from a version or tag like v1.2.0, since WordPress.org
|
|
17
|
+
* tags and plugin versions never use the prefix.
|
|
18
|
+
*
|
|
19
|
+
* @param tag Tag name.
|
|
20
|
+
* @return Tag without the prefix.
|
|
21
|
+
*/
|
|
22
|
+
export function normalizeTag(tag) {
|
|
23
|
+
return tag.replace(/^v(?=\d)/, '');
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Check if a git ref points to a tag.
|
|
27
|
+
*
|
|
28
|
+
* @param ref Git ref like refs/tags/1.0.0.
|
|
29
|
+
* @return Whether the ref is a tag.
|
|
30
|
+
*/
|
|
31
|
+
export function isTagRef(ref) {
|
|
32
|
+
return Boolean(ref?.startsWith('refs/tags/'));
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Check if a slug is a valid WordPress.org plugin slug, so that it's safe in
|
|
36
|
+
* SVN URLs and paths.
|
|
37
|
+
*
|
|
38
|
+
* @param slug Plugin slug.
|
|
39
|
+
* @return Whether the slug is valid.
|
|
40
|
+
*/
|
|
41
|
+
export function isValidSlug(slug) {
|
|
42
|
+
return /^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(slug);
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Check if a version can be used as an SVN tag directory name, without path
|
|
46
|
+
* separators or "." and ".." names.
|
|
47
|
+
*
|
|
48
|
+
* @param version Version string.
|
|
49
|
+
* @return Whether the version is valid.
|
|
50
|
+
*/
|
|
51
|
+
export function isValidVersion(version) {
|
|
52
|
+
return /^[0-9A-Za-z][0-9A-Za-z.+_-]*$/.test(version);
|
|
53
|
+
}
|
|
54
|
+
function isValidFilename(name) {
|
|
55
|
+
return (Boolean(name) && !/[\\/]/.test(name) && !/^\.\.?$/.test(name));
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Collect the problems with resolved deploy options.
|
|
59
|
+
*
|
|
60
|
+
* @param options Resolved options.
|
|
61
|
+
* @return Error messages, empty when valid.
|
|
62
|
+
*/
|
|
63
|
+
export function getDeployOptionErrors(options) {
|
|
64
|
+
const errors = [];
|
|
65
|
+
const { trunk, readmeOnly, svnTag, version, assets, assetsDir, slug } = options;
|
|
66
|
+
if (svnTag && !trunk) {
|
|
67
|
+
errors.push('--svn-tag requires --trunk, since the tag is copied from trunk.');
|
|
68
|
+
}
|
|
69
|
+
if (trunk && readmeOnly) {
|
|
70
|
+
errors.push('Use either --trunk or --readme-only, not both.');
|
|
71
|
+
}
|
|
72
|
+
if (svnTag && isPrerelease(svnTag)) {
|
|
73
|
+
errors.push(`Pre-release version ${svnTag} can't be tagged on WordPress.org.`);
|
|
74
|
+
}
|
|
75
|
+
if (svnTag && !isValidVersion(svnTag)) {
|
|
76
|
+
errors.push(`SVN tag "${svnTag}" isn't a valid version, use only letters, digits, dots, dashes, plus signs and underscores.`);
|
|
77
|
+
}
|
|
78
|
+
if (version && !isValidVersion(version)) {
|
|
79
|
+
errors.push(`Version "${version}" isn't valid, use only letters, digits, dots, dashes, plus signs and underscores.`);
|
|
80
|
+
}
|
|
81
|
+
if (svnTag && version && svnTag !== version) {
|
|
82
|
+
errors.push(`SVN tag ${svnTag} doesn't match version ${version}.`);
|
|
83
|
+
}
|
|
84
|
+
if (readmeOnly && isTagRef(options.gitRef)) {
|
|
85
|
+
errors.push(`--readme-only isn't allowed when running for tag ${options.gitRef}. Did you mean --trunk --svn-tag?`);
|
|
86
|
+
}
|
|
87
|
+
if (assets && !assetsDir) {
|
|
88
|
+
errors.push('Syncing assets requires --assets-dir.');
|
|
89
|
+
}
|
|
90
|
+
if (assets && assetsDir && !existsSync(assetsDir)) {
|
|
91
|
+
errors.push(`Assets directory ${assetsDir} doesn't exist.`);
|
|
92
|
+
}
|
|
93
|
+
if (!isValidFilename(options.readme)) {
|
|
94
|
+
errors.push(`Readme "${options.readme}" must be a file name in the source directory.`);
|
|
95
|
+
}
|
|
96
|
+
if (!slug) {
|
|
97
|
+
errors.push('Missing the plugin slug.');
|
|
98
|
+
}
|
|
99
|
+
if (slug && !isValidSlug(slug)) {
|
|
100
|
+
errors.push(`Slug "${slug}" isn't valid, use only lowercase letters, digits and dashes.`);
|
|
101
|
+
}
|
|
102
|
+
return errors;
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* Collect the notices about valid but unusual deploy options.
|
|
106
|
+
*
|
|
107
|
+
* @param options Resolved options.
|
|
108
|
+
* @return Warning messages.
|
|
109
|
+
*/
|
|
110
|
+
export function getDeployOptionWarnings(options) {
|
|
111
|
+
const warnings = [];
|
|
112
|
+
if (options.trunk && !options.svnTag) {
|
|
113
|
+
warnings.push('Syncing the full build to trunk without a tag makes trunk differ from the stable tag.');
|
|
114
|
+
}
|
|
115
|
+
if (options.trunk && options.version && isPrerelease(options.version)) {
|
|
116
|
+
warnings.push(`Syncing pre-release version ${options.version} to trunk.`);
|
|
117
|
+
}
|
|
118
|
+
return warnings;
|
|
119
|
+
}
|
|
120
|
+
/**
|
|
121
|
+
* Apply defaults to deploy options and validate them. Trunk, readme and tag
|
|
122
|
+
* writes happen only when requested, and assets are synced whenever an
|
|
123
|
+
* assets directory is set, unless turned off. The version defaults to the
|
|
124
|
+
* SVN tag; without either, SvnDeploy reads the Version header of the
|
|
125
|
+
* deployed files, which can be in a ZIP.
|
|
126
|
+
*
|
|
127
|
+
* @param input Options from the CLI, action or library caller.
|
|
128
|
+
* @return Resolved options.
|
|
129
|
+
* @throws {ValidationError} When the options are invalid.
|
|
130
|
+
*/
|
|
131
|
+
export function resolveDeployOptions(input = {}) {
|
|
132
|
+
const cwd = resolve(input.cwd ?? process.cwd());
|
|
133
|
+
const slug = input.slug || basename(cwd);
|
|
134
|
+
const svnTag = input.svnTag ? normalizeTag(input.svnTag) : null;
|
|
135
|
+
const sourceDir = resolve(cwd, input.sourceDir || '.');
|
|
136
|
+
const assetsDir = input.assetsDir ? resolve(cwd, input.assetsDir) : null;
|
|
137
|
+
const options = {
|
|
138
|
+
cwd,
|
|
139
|
+
slug,
|
|
140
|
+
version: input.version ? normalizeTag(input.version) : svnTag,
|
|
141
|
+
trunk: Boolean(input.trunk),
|
|
142
|
+
readmeOnly: Boolean(input.readmeOnly),
|
|
143
|
+
assets: input.assets ?? Boolean(assetsDir),
|
|
144
|
+
svnTag,
|
|
145
|
+
sourceDir,
|
|
146
|
+
...resolveFileSelection({
|
|
147
|
+
cwd,
|
|
148
|
+
sourceDir,
|
|
149
|
+
distignore: input.distignore,
|
|
150
|
+
exclude: input.exclude,
|
|
151
|
+
}),
|
|
152
|
+
assetsDir,
|
|
153
|
+
readme: input.readme || 'readme.txt',
|
|
154
|
+
fromZip: input.fromZip ? resolve(cwd, input.fromZip) : null,
|
|
155
|
+
svnUrl: (input.svnUrl || SVN_URL).replace('{slug}', slug),
|
|
156
|
+
svnUsername: input.svnUsername || process.env.SVN_USERNAME || null,
|
|
157
|
+
svnPassword: input.svnPassword || process.env.SVN_PASSWORD || null,
|
|
158
|
+
gitRef: input.gitRef || null,
|
|
159
|
+
message: input.message || null,
|
|
160
|
+
force: Boolean(input.force),
|
|
161
|
+
dryRun: Boolean(input.dryRun),
|
|
162
|
+
};
|
|
163
|
+
const errors = getDeployOptionErrors(options);
|
|
164
|
+
if (errors.length) {
|
|
165
|
+
throw new ValidationError(errors.join('\n'));
|
|
166
|
+
}
|
|
167
|
+
return options;
|
|
168
|
+
}
|
package/dist/errors.d.ts
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Invalid options or failed release checks, reported with exit code 2.
|
|
3
|
+
*/
|
|
4
|
+
export declare class ValidationError extends Error {
|
|
5
|
+
name: string;
|
|
6
|
+
exitCode: number;
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* Missing SVN credentials or a failed login or permission check, reported
|
|
10
|
+
* with exit code 3.
|
|
11
|
+
*/
|
|
12
|
+
export declare class CredentialsError extends Error {
|
|
13
|
+
static readonly EXIT_CODE = 3;
|
|
14
|
+
name: string;
|
|
15
|
+
exitCode: number;
|
|
16
|
+
}
|
package/dist/errors.js
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Invalid options or failed release checks, reported with exit code 2.
|
|
3
|
+
*/
|
|
4
|
+
export class ValidationError extends Error {
|
|
5
|
+
name = 'ValidationError';
|
|
6
|
+
exitCode = 2;
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* Missing SVN credentials or a failed login or permission check, reported
|
|
10
|
+
* with exit code 3.
|
|
11
|
+
*/
|
|
12
|
+
export class CredentialsError extends Error {
|
|
13
|
+
static EXIT_CODE = 3;
|
|
14
|
+
name = 'CredentialsError';
|
|
15
|
+
exitCode = CredentialsError.EXIT_CODE;
|
|
16
|
+
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import ignore from 'ignore';
|
|
2
|
+
export declare const DISTIGNORE_FILENAME = ".distignore";
|
|
3
|
+
/**
|
|
4
|
+
* Version control metadata that is never released, at any depth. Without
|
|
5
|
+
* these, a repository without an ignore file would ship its .git/ directory,
|
|
6
|
+
* with the full history, in every ZIP and SVN deploy. Matching the name
|
|
7
|
+
* without a trailing slash also covers the .git file of worktrees and
|
|
8
|
+
* submodules.
|
|
9
|
+
*/
|
|
10
|
+
export declare const ALWAYS_EXCLUDED: string[];
|
|
11
|
+
/**
|
|
12
|
+
* Gitignore-style matcher from the ignore package, with ignores( path ).
|
|
13
|
+
*/
|
|
14
|
+
export type FileRules = ignore.Ignore;
|
|
15
|
+
export interface FileSelectionInput {
|
|
16
|
+
/** Directory that relative paths resolve against. */
|
|
17
|
+
cwd: string;
|
|
18
|
+
/** Directory with the files to select. */
|
|
19
|
+
sourceDir: string;
|
|
20
|
+
/** Ignore file path, or false to not read one. */
|
|
21
|
+
distignore?: string | false;
|
|
22
|
+
/** Extra gitignore-style patterns, comma or newline separated. */
|
|
23
|
+
exclude?: string | string[];
|
|
24
|
+
}
|
|
25
|
+
export interface FileSelection {
|
|
26
|
+
/** Ignore file path, or null when disabled. */
|
|
27
|
+
distignore: string | null;
|
|
28
|
+
/** Whether the ignore file must exist. */
|
|
29
|
+
distignoreRequired: boolean;
|
|
30
|
+
/** Extra gitignore-style patterns. */
|
|
31
|
+
exclude: string[];
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Parse exclude patterns from a comma or newline separated string, an array
|
|
35
|
+
* of such strings (repeated CLI flags), or nothing.
|
|
36
|
+
*
|
|
37
|
+
* @param value Exclude patterns.
|
|
38
|
+
* @return Trimmed, non-empty patterns.
|
|
39
|
+
*/
|
|
40
|
+
export declare function parseExclude(value?: string | string[]): string[];
|
|
41
|
+
/**
|
|
42
|
+
* Resolve which ignore file and extra patterns select the files to package
|
|
43
|
+
* or deploy. The default ignore file is optional, while an explicitly passed
|
|
44
|
+
* one must exist.
|
|
45
|
+
*
|
|
46
|
+
* @param input File selection input.
|
|
47
|
+
* @return Ignore file path (or null), whether it's required, and the patterns.
|
|
48
|
+
*/
|
|
49
|
+
export declare function resolveFileSelection(input: FileSelectionInput): FileSelection;
|
|
50
|
+
/**
|
|
51
|
+
* Load the ignore file and exclude patterns into one gitignore-style matcher.
|
|
52
|
+
* The default ignore file is skipped when it doesn't exist. Version control
|
|
53
|
+
* metadata in ALWAYS_EXCLUDED is added last, so that no rule can re-include
|
|
54
|
+
* it.
|
|
55
|
+
*
|
|
56
|
+
* @param selection File selection from resolveFileSelection().
|
|
57
|
+
* @return Matcher with ignores( path ).
|
|
58
|
+
*/
|
|
59
|
+
export declare function loadFileRules(selection: Partial<FileSelection>): Promise<FileRules>;
|
|
60
|
+
/**
|
|
61
|
+
* Check if an error is a Node.js system error with the given code.
|
|
62
|
+
*
|
|
63
|
+
* @param error Caught error.
|
|
64
|
+
* @param code Error code like ENOENT.
|
|
65
|
+
* @return Whether the error has the code.
|
|
66
|
+
*/
|
|
67
|
+
export declare function isErrorCode(error: unknown, code: string): boolean;
|
|
68
|
+
/**
|
|
69
|
+
* List the files in a directory that the rules don't ignore, as sorted
|
|
70
|
+
* relative paths with forward slashes. Ignored directories aren't read, so
|
|
71
|
+
* like in git, a file can't be re-included from an ignored directory.
|
|
72
|
+
*
|
|
73
|
+
* @param sourceDir Directory to list.
|
|
74
|
+
* @param rules Matcher from loadFileRules().
|
|
75
|
+
* @return Relative file paths, sorted by code point.
|
|
76
|
+
* @throws {ValidationError} For selected symbolic links, which ZIP archives and WordPress.org SVN don't support.
|
|
77
|
+
*/
|
|
78
|
+
export declare function selectFiles(sourceDir: string, rules: FileRules): Promise<string[]>;
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import { readdir, readFile } from 'node:fs/promises';
|
|
2
|
+
import { join, posix, resolve } from 'node:path';
|
|
3
|
+
import ignore from 'ignore';
|
|
4
|
+
import { ValidationError } from "./errors.js";
|
|
5
|
+
export const DISTIGNORE_FILENAME = '.distignore';
|
|
6
|
+
/**
|
|
7
|
+
* Version control metadata that is never released, at any depth. Without
|
|
8
|
+
* these, a repository without an ignore file would ship its .git/ directory,
|
|
9
|
+
* with the full history, in every ZIP and SVN deploy. Matching the name
|
|
10
|
+
* without a trailing slash also covers the .git file of worktrees and
|
|
11
|
+
* submodules.
|
|
12
|
+
*/
|
|
13
|
+
export const ALWAYS_EXCLUDED = ['.git', '.svn', '.hg'];
|
|
14
|
+
/**
|
|
15
|
+
* Parse exclude patterns from a comma or newline separated string, an array
|
|
16
|
+
* of such strings (repeated CLI flags), or nothing.
|
|
17
|
+
*
|
|
18
|
+
* @param value Exclude patterns.
|
|
19
|
+
* @return Trimmed, non-empty patterns.
|
|
20
|
+
*/
|
|
21
|
+
export function parseExclude(value) {
|
|
22
|
+
return [value]
|
|
23
|
+
.flat()
|
|
24
|
+
.filter((item) => 'string' === typeof item)
|
|
25
|
+
.flatMap((item) => item.split(/[,\n]/))
|
|
26
|
+
.map((pattern) => pattern.trim())
|
|
27
|
+
.filter(Boolean);
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Resolve which ignore file and extra patterns select the files to package
|
|
31
|
+
* or deploy. The default ignore file is optional, while an explicitly passed
|
|
32
|
+
* one must exist.
|
|
33
|
+
*
|
|
34
|
+
* @param input File selection input.
|
|
35
|
+
* @return Ignore file path (or null), whether it's required, and the patterns.
|
|
36
|
+
*/
|
|
37
|
+
export function resolveFileSelection(input) {
|
|
38
|
+
const { cwd, sourceDir, distignore, exclude } = input;
|
|
39
|
+
const selection = {
|
|
40
|
+
distignore: join(sourceDir, DISTIGNORE_FILENAME),
|
|
41
|
+
distignoreRequired: false,
|
|
42
|
+
exclude: parseExclude(exclude),
|
|
43
|
+
};
|
|
44
|
+
if (false === distignore) {
|
|
45
|
+
selection.distignore = null;
|
|
46
|
+
}
|
|
47
|
+
if ('string' === typeof distignore && distignore) {
|
|
48
|
+
selection.distignore = resolve(cwd, distignore);
|
|
49
|
+
selection.distignoreRequired = true;
|
|
50
|
+
}
|
|
51
|
+
return selection;
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Load the ignore file and exclude patterns into one gitignore-style matcher.
|
|
55
|
+
* The default ignore file is skipped when it doesn't exist. Version control
|
|
56
|
+
* metadata in ALWAYS_EXCLUDED is added last, so that no rule can re-include
|
|
57
|
+
* it.
|
|
58
|
+
*
|
|
59
|
+
* @param selection File selection from resolveFileSelection().
|
|
60
|
+
* @return Matcher with ignores( path ).
|
|
61
|
+
*/
|
|
62
|
+
export async function loadFileRules(selection) {
|
|
63
|
+
const { distignore = null, distignoreRequired = false, exclude = [], } = selection;
|
|
64
|
+
const rules = await readIgnoreFile(distignore, distignoreRequired);
|
|
65
|
+
return ignore().add(rules).add(exclude).add(ALWAYS_EXCLUDED);
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Read the ignore file, or nothing when there's no file to read.
|
|
69
|
+
*
|
|
70
|
+
* @param path Ignore file path, or null.
|
|
71
|
+
* @param required Whether the file must exist.
|
|
72
|
+
* @return Ignore file contents.
|
|
73
|
+
*/
|
|
74
|
+
async function readIgnoreFile(path, required) {
|
|
75
|
+
if (!path) {
|
|
76
|
+
return '';
|
|
77
|
+
}
|
|
78
|
+
try {
|
|
79
|
+
return await readFile(path, 'utf8');
|
|
80
|
+
}
|
|
81
|
+
catch (error) {
|
|
82
|
+
if (!isErrorCode(error, 'ENOENT')) {
|
|
83
|
+
throw error;
|
|
84
|
+
}
|
|
85
|
+
if (required) {
|
|
86
|
+
throw new ValidationError(`Ignore file ${path} doesn't exist.`);
|
|
87
|
+
}
|
|
88
|
+
return '';
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* Check if an error is a Node.js system error with the given code.
|
|
93
|
+
*
|
|
94
|
+
* @param error Caught error.
|
|
95
|
+
* @param code Error code like ENOENT.
|
|
96
|
+
* @return Whether the error has the code.
|
|
97
|
+
*/
|
|
98
|
+
export function isErrorCode(error, code) {
|
|
99
|
+
return error instanceof Error && 'code' in error && code === error.code;
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* List the files in a directory that the rules don't ignore, as sorted
|
|
103
|
+
* relative paths with forward slashes. Ignored directories aren't read, so
|
|
104
|
+
* like in git, a file can't be re-included from an ignored directory.
|
|
105
|
+
*
|
|
106
|
+
* @param sourceDir Directory to list.
|
|
107
|
+
* @param rules Matcher from loadFileRules().
|
|
108
|
+
* @return Relative file paths, sorted by code point.
|
|
109
|
+
* @throws {ValidationError} For selected symbolic links, which ZIP archives and WordPress.org SVN don't support.
|
|
110
|
+
*/
|
|
111
|
+
export async function selectFiles(sourceDir, rules) {
|
|
112
|
+
const files = await findFiles(sourceDir, '', rules);
|
|
113
|
+
return files.sort();
|
|
114
|
+
}
|
|
115
|
+
async function findFiles(sourceDir, dir, rules) {
|
|
116
|
+
const entries = await readdir(join(sourceDir, dir), {
|
|
117
|
+
withFileTypes: true,
|
|
118
|
+
});
|
|
119
|
+
const files = await Promise.all(entries.map((entry) => findEntryFiles(sourceDir, posix.join(dir, entry.name), entry, rules)));
|
|
120
|
+
return files.flat();
|
|
121
|
+
}
|
|
122
|
+
async function findEntryFiles(sourceDir, path, entry, rules) {
|
|
123
|
+
if (entry.isDirectory() && !rules.ignores(`${path}/`)) {
|
|
124
|
+
return findFiles(sourceDir, path, rules);
|
|
125
|
+
}
|
|
126
|
+
if (entry.isFile() && !rules.ignores(path)) {
|
|
127
|
+
return [path];
|
|
128
|
+
}
|
|
129
|
+
if (entry.isSymbolicLink() && !rules.ignores(path)) {
|
|
130
|
+
throw new ValidationError(`${path} is a symbolic link, which isn't supported. Exclude it or replace it with the file.`);
|
|
131
|
+
}
|
|
132
|
+
return [];
|
|
133
|
+
}
|