@wpelevator/wp-release 0.9.0 → 0.11.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 +101 -4
- package/dist/changelog.d.ts +45 -0
- package/dist/changelog.js +246 -0
- package/dist/changeset-config.d.ts +75 -0
- package/dist/changeset-config.js +48 -0
- package/dist/changeset-dependents.d.ts +132 -0
- package/dist/changeset-dependents.js +540 -0
- package/dist/changeset-files.d.ts +41 -0
- package/dist/changeset-files.js +100 -0
- package/dist/changeset-packages.d.ts +59 -0
- package/dist/changeset-packages.js +186 -0
- package/dist/changeset-release.d.ts +155 -18
- package/dist/changeset-release.js +366 -1090
- package/dist/changesets.d.ts +2 -0
- package/dist/commands/changeset-args.d.ts +5 -4
- package/dist/commands/changeset-args.js +2 -5
- package/dist/commands/changeset-status.d.ts +2 -0
- package/dist/commands/changeset-status.js +6 -2
- package/dist/commands/changeset-tag.d.ts +0 -3
- package/dist/commands/changeset-tag.js +8 -10
- package/dist/commands/changeset-version.d.ts +3 -0
- package/dist/commands/changeset-version.js +2 -1
- package/dist/file-journal.d.ts +22 -0
- package/dist/file-journal.js +56 -0
- package/dist/index.d.ts +3 -3
- package/dist/index.js +1 -1
- package/dist/json-files.d.ts +15 -0
- package/dist/json-files.js +35 -0
- package/dist/package-meta.js +1 -3
- package/dist/regexp.d.ts +7 -0
- package/dist/regexp.js +9 -0
- package/dist/release-repository.d.ts +102 -0
- package/dist/release-repository.js +159 -0
- package/dist/release-tags.d.ts +56 -0
- package/dist/release-tags.js +89 -0
- package/dist/tag-format.d.ts +20 -0
- package/dist/tag-format.js +51 -0
- package/dist/version-check.js +1 -3
- package/dist/version-files.d.ts +36 -0
- package/dist/version-files.js +195 -0
- package/dist/versions.d.ts +25 -0
- package/dist/versions.js +74 -0
- package/package.json +3 -1
|
@@ -1,501 +1,418 @@
|
|
|
1
|
-
import { randomUUID } from 'node:crypto';
|
|
2
|
-
import { execFile } from 'node:child_process';
|
|
3
|
-
import { access, mkdir, readdir, readFile, rm, writeFile, } from 'node:fs/promises';
|
|
4
1
|
import path from 'node:path';
|
|
5
|
-
import {
|
|
2
|
+
import { getVersionedReleases, prependChangelog } from "./changelog.js";
|
|
6
3
|
import { loadChangesetConfig } from "./changeset-config.js";
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
4
|
+
import { applyDependents, assertDependentsSourcesReleased, collectPinDriftWarnings, expandDependentsPlan, findBundledPinChanges, hasChangeset, selectReleasePlan, stripDependentsPlan, updateDependencyDeclaration, } from "./changeset-dependents.js";
|
|
5
|
+
import { consumeChangeset, readChangesets, readChangesetsSince, writeChangeset, } from "./changeset-files.js";
|
|
6
|
+
import { assertKnownPackages, discoverPackages, getChangedPackages, toChangesetPackage, } from "./changeset-packages.js";
|
|
7
|
+
import { getChangesetBump, getNextVersion } from "./changesets.js";
|
|
10
8
|
import { ValidationError } from "./errors.js";
|
|
11
|
-
|
|
9
|
+
import { FileJournal } from "./file-journal.js";
|
|
10
|
+
import { getPreviousTag, getReleaseNotes, getTagVersion, selectTagPackages, } from "./release-tags.js";
|
|
11
|
+
import { ReleaseRepository } from "./release-repository.js";
|
|
12
|
+
import { formatTag } from "./tag-format.js";
|
|
13
|
+
import { verifyVersionTargets, writeVersion } from "./version-files.js";
|
|
14
|
+
import { isPrerelease } from "./versions.js";
|
|
15
|
+
const MISSING_CONFIG = 'No wp-release config found. Add wp-release.config.js or pass --config <path>.';
|
|
12
16
|
/**
|
|
13
|
-
*
|
|
17
|
+
* Changeset releases of a repository: recording changesets, versioning the
|
|
18
|
+
* packages they name and tagging the releases.
|
|
14
19
|
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
20
|
+
* Each operation loads the configuration, discovers the packages and reads the
|
|
21
|
+
* pending changesets again, so that one instance can version and then tag.
|
|
22
|
+
* Git runs through the injected runner.
|
|
17
23
|
*/
|
|
18
|
-
export
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
/**
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
return packages.map(toChangesetPackage);
|
|
33
|
-
}
|
|
34
|
-
async function getReleaseContext(input) {
|
|
35
|
-
const loaded = await loadChangesetConfig(input);
|
|
36
|
-
if (!loaded) {
|
|
37
|
-
return undefined;
|
|
24
|
+
export class ChangesetRelease {
|
|
25
|
+
cwd;
|
|
26
|
+
configFile;
|
|
27
|
+
repository;
|
|
28
|
+
/**
|
|
29
|
+
* @param options Repository root, config file and git runner.
|
|
30
|
+
*/
|
|
31
|
+
constructor(options) {
|
|
32
|
+
this.cwd = options.cwd;
|
|
33
|
+
this.configFile = options.configFile;
|
|
34
|
+
this.repository = new ReleaseRepository({
|
|
35
|
+
cwd: options.cwd,
|
|
36
|
+
runner: options.git,
|
|
37
|
+
});
|
|
38
38
|
}
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
39
|
+
/**
|
|
40
|
+
* Discover configured packages, pending changesets, and their proposed
|
|
41
|
+
* versions.
|
|
42
|
+
*
|
|
43
|
+
* @return A release plan; `undefined` when no config is present.
|
|
44
|
+
*/
|
|
45
|
+
async getPlan() {
|
|
46
|
+
const context = await this.loadContext();
|
|
47
|
+
return context?.plan;
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Discover the packages of the repository configuration.
|
|
51
|
+
*
|
|
52
|
+
* @return Packages in the configured order with their current versions.
|
|
53
|
+
* @throws {ValidationError} Without a config, or for an invalid one.
|
|
54
|
+
*/
|
|
55
|
+
async getPackages() {
|
|
56
|
+
const config = await this.requireConfig();
|
|
57
|
+
const packages = await discoverPackages(this.repository, config);
|
|
58
|
+
return packages.map(toChangesetPackage);
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Create a Changesets-compatible Markdown file.
|
|
62
|
+
*
|
|
63
|
+
* @param options Packages, summary and file name.
|
|
64
|
+
* @return Created file path.
|
|
65
|
+
* @throws {ValidationError} For unknown packages or an existing file.
|
|
66
|
+
*/
|
|
67
|
+
async add(options) {
|
|
68
|
+
const config = await this.requireConfig();
|
|
69
|
+
const packages = await discoverPackages(this.repository, config);
|
|
70
|
+
assertKnownPackages(packages, Object.keys(options.packages));
|
|
71
|
+
return writeChangeset(this.getChangesetDirectory(config), options.packages, options.summary, options.name);
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Apply pending package versions and changelogs. A failed run restores
|
|
75
|
+
* every file it changed.
|
|
76
|
+
*
|
|
77
|
+
* @param options Versioning options.
|
|
78
|
+
* @return Planned releases and whether files were changed.
|
|
79
|
+
* @throws {ValidationError} Without a config, or for an invalid plan.
|
|
80
|
+
*/
|
|
81
|
+
async version(options = {}) {
|
|
82
|
+
const context = await this.requireContext();
|
|
83
|
+
const { config, packages } = context;
|
|
84
|
+
const withDependents = options.dependents ?? true;
|
|
85
|
+
let releasePlan = context.plan;
|
|
86
|
+
if (options.packages) {
|
|
87
|
+
releasePlan = expandDependentsPlan(await selectReleasePlan(context.plan, packages, options.packages, config, withDependents));
|
|
88
|
+
}
|
|
89
|
+
else if (!withDependents) {
|
|
90
|
+
releasePlan = stripDependentsPlan(context.plan);
|
|
91
|
+
}
|
|
92
|
+
// A dependent must not release while one of the packages it depends on has
|
|
93
|
+
// pending changesets that aren't released in the same run, or it would
|
|
94
|
+
// bundle unreleased code under a changelog that cannot credit it. The
|
|
95
|
+
// check only applies to repositories that adopted the dependents policy.
|
|
96
|
+
if (withDependents && config.dependents) {
|
|
97
|
+
await assertDependentsSourcesReleased(releasePlan, context.plan, packages, config);
|
|
98
|
+
}
|
|
99
|
+
const date = options.date ?? new Date().toISOString().slice(0, 10);
|
|
100
|
+
const releases = await getVersionedReleases(this.repository, config, releasePlan.releases, this.getChangesetDirectory(config));
|
|
101
|
+
if (!options.dryRun) {
|
|
102
|
+
await this.writeReleases(context, releasePlan, releases, date, options.packages);
|
|
103
|
+
}
|
|
104
|
+
return {
|
|
105
|
+
changesets: releasePlan.changesets,
|
|
106
|
+
releases,
|
|
107
|
+
date,
|
|
108
|
+
changed: !options.dryRun &&
|
|
109
|
+
(releasePlan.changesets.length > 0 ||
|
|
110
|
+
releasePlan.releases.length > 0),
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* Create missing release tags, optionally pushing them to the origin
|
|
115
|
+
* remote. Each tag comes with the title and notes of its release, for a
|
|
116
|
+
* workflow step to create the GitHub Release.
|
|
117
|
+
*
|
|
118
|
+
* @param options Tag options.
|
|
119
|
+
* @return Per-package tags and release notes.
|
|
120
|
+
* @throws {ValidationError} For conflicting options, unknown packages or version mismatches.
|
|
121
|
+
*/
|
|
122
|
+
async tag(options = {}) {
|
|
123
|
+
if (options.untagged && options.push) {
|
|
124
|
+
throw new ValidationError('Listing untagged releases cannot be combined with pushing tags.');
|
|
125
|
+
}
|
|
126
|
+
const context = await this.requireContext();
|
|
127
|
+
const selected = selectTagPackages(context.packages, options.packages ?? [], options.version);
|
|
128
|
+
const packages = options.since
|
|
129
|
+
? (await getChangedPackages(this.repository, selected, options.since)).packages
|
|
130
|
+
: selected;
|
|
131
|
+
const results = [];
|
|
132
|
+
const dryRun = options.dryRun || options.untagged;
|
|
133
|
+
const tagFormat = context.config.tagFormat ?? '{path}/{version}';
|
|
134
|
+
for (const pkg of packages) {
|
|
135
|
+
const version = await getTagVersion(pkg, options.version);
|
|
136
|
+
if (undefined !== options.version &&
|
|
137
|
+
options.version !== version) {
|
|
138
|
+
throw new ValidationError(`Package "${pkg.name}" is at version ${version}, not ${options.version}.`);
|
|
139
|
+
}
|
|
140
|
+
const tag = formatTag(tagFormat, pkg, version);
|
|
141
|
+
const exists = await this.repository.tagExists(tag);
|
|
142
|
+
if (!exists) {
|
|
143
|
+
await verifyVersionTargets(this.repository, pkg, version);
|
|
57
144
|
}
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
145
|
+
const previousTag = await getPreviousTag(this.repository, formatTag(tagFormat, pkg, ''), version);
|
|
146
|
+
const notes = await getReleaseNotes(this.repository, context.config, pkg, { version, tag, previousTag }, exists ? tag : 'HEAD');
|
|
147
|
+
if (!exists && !dryRun) {
|
|
148
|
+
await this.repository.createTag(tag);
|
|
149
|
+
}
|
|
150
|
+
results.push({
|
|
151
|
+
package: pkg.name,
|
|
152
|
+
version,
|
|
153
|
+
tag,
|
|
154
|
+
title: `${pkg.name}@${version}`,
|
|
155
|
+
...(previousTag ? { previousTag } : {}),
|
|
156
|
+
prerelease: isPrerelease(version),
|
|
157
|
+
exists,
|
|
158
|
+
created: !exists && !dryRun,
|
|
159
|
+
notes,
|
|
64
160
|
});
|
|
65
161
|
}
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
162
|
+
// Push one tag at a time because GitHub doesn't create push events for
|
|
163
|
+
// tags when more than three are pushed at once.
|
|
164
|
+
if (options.push && !dryRun) {
|
|
165
|
+
for (const item of results.filter((result) => result.created)) {
|
|
166
|
+
await this.repository.pushTag(item.tag);
|
|
71
167
|
}
|
|
72
|
-
});
|
|
73
|
-
});
|
|
74
|
-
return { config, packages, plan: { changesets, releases } };
|
|
75
|
-
}
|
|
76
|
-
/**
|
|
77
|
-
* Create a Changesets-compatible Markdown file.
|
|
78
|
-
*
|
|
79
|
-
* @param input Repository, packages and summary.
|
|
80
|
-
* @return Created file path.
|
|
81
|
-
*/
|
|
82
|
-
export async function addChangeset(input) {
|
|
83
|
-
const loaded = await requireConfig(input);
|
|
84
|
-
const packages = await discoverPackages(input.cwd, loaded.config);
|
|
85
|
-
Object.keys(input.packages).forEach((name) => {
|
|
86
|
-
if (!packages.some((pkg) => pkg.name === name)) {
|
|
87
|
-
throw new ValidationError(`Unknown package "${name}".`);
|
|
88
168
|
}
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
.
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
169
|
+
return {
|
|
170
|
+
tags: options.untagged
|
|
171
|
+
? results.filter((result) => !result.exists)
|
|
172
|
+
: results,
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
/**
|
|
176
|
+
* Check whether changed files belong to packages without a changeset.
|
|
177
|
+
*
|
|
178
|
+
* @param options Base git ref.
|
|
179
|
+
* @return Changed packages and missing changeset identifiers.
|
|
180
|
+
* @throws {ValidationError} Without a config.
|
|
181
|
+
*/
|
|
182
|
+
async getStatus(options = {}) {
|
|
183
|
+
const { config, packages, plan } = await this.requireContext();
|
|
184
|
+
const { changesets, releases } = plan;
|
|
185
|
+
const since = options.since ??
|
|
186
|
+
(config.baseBranch ? `origin/${config.baseBranch}` : undefined);
|
|
187
|
+
const changed = since
|
|
188
|
+
? await getChangedPackages(this.repository, packages, since)
|
|
189
|
+
: { packages: [], filesByPackage: {} };
|
|
190
|
+
const changedPackages = changed.packages.map((pkg) => pkg.name);
|
|
191
|
+
const changesetsInDiff = since
|
|
192
|
+
? await readChangesetsSince(this.repository, this.getChangesetDirectory(config), changesets, since)
|
|
193
|
+
: [];
|
|
194
|
+
const hasEmptyChangeset = changesetsInDiff.some((changeset) => 0 === Object.keys(changeset.packages).length);
|
|
195
|
+
const bundled = await findBundledPinChanges(changed, changesetsInDiff, packages, config);
|
|
196
|
+
const missingChangesets = hasEmptyChangeset
|
|
197
|
+
? []
|
|
198
|
+
: changedPackages.filter((name) => !hasChangeset(changesetsInDiff, name) &&
|
|
199
|
+
!bundled.has(name));
|
|
200
|
+
// A pin that doesn't resolve the local package selects a registry release
|
|
201
|
+
// instead, which the dependents policy won't re-release automatically.
|
|
202
|
+
const warnings = config.dependents
|
|
203
|
+
? await collectPinDriftWarnings(config, packages)
|
|
204
|
+
: [];
|
|
205
|
+
return {
|
|
206
|
+
changesets,
|
|
207
|
+
releases,
|
|
208
|
+
changedPackages,
|
|
209
|
+
changedPackageFiles: changed.filesByPackage,
|
|
210
|
+
missingChangesets,
|
|
211
|
+
warnings,
|
|
212
|
+
};
|
|
213
|
+
}
|
|
214
|
+
/**
|
|
215
|
+
* Write the versions, dependency specs and changelogs of the releases and
|
|
216
|
+
* consume the applied changesets, restoring every changed file when a
|
|
217
|
+
* step fails.
|
|
218
|
+
*
|
|
219
|
+
* @param context Release context.
|
|
220
|
+
* @param plan Selected release plan.
|
|
221
|
+
* @param releases Releases with their changelog entries.
|
|
222
|
+
* @param date Release date, as YYYY-MM-DD.
|
|
223
|
+
* @param applied Versioned packages, or undefined when all were versioned.
|
|
224
|
+
*/
|
|
225
|
+
async writeReleases(context, plan, releases, date, applied) {
|
|
226
|
+
const journal = new FileJournal();
|
|
227
|
+
const changesetDirectory = this.getChangesetDirectory(context.config);
|
|
134
228
|
try {
|
|
135
229
|
for (const release of releases) {
|
|
136
|
-
|
|
137
|
-
if (!configured) {
|
|
138
|
-
throw new ValidationError(`Unknown package "${release.package.name}".`);
|
|
139
|
-
}
|
|
140
|
-
for (const target of configured.versionConfig.to) {
|
|
141
|
-
await writeVersion(configured.directory, target, release.nextVersion, journal);
|
|
142
|
-
}
|
|
143
|
-
await verifyVersionTargets(input.cwd, configured, release.nextVersion);
|
|
144
|
-
await prependChangelog(configured.directory, release, date, config.readmeChangelog ?? false, config.changelogInsertAfter, journal);
|
|
230
|
+
await this.writeRelease(context, plan, release, date, journal);
|
|
145
231
|
}
|
|
146
|
-
for (const changeset of
|
|
147
|
-
await consumeChangeset(path.join(changesetDirectory, changeset.fileName), changeset,
|
|
232
|
+
for (const changeset of plan.changesets) {
|
|
233
|
+
await consumeChangeset(path.join(changesetDirectory, changeset.fileName), changeset, applied, journal);
|
|
148
234
|
}
|
|
149
235
|
}
|
|
150
236
|
catch (error) {
|
|
151
|
-
const rollbackErrors = await
|
|
237
|
+
const rollbackErrors = await journal.restore();
|
|
152
238
|
if (rollbackErrors.length) {
|
|
153
239
|
throw new AggregateError([error, ...rollbackErrors], 'Changeset versioning failed and one or more file changes could not be rolled back.');
|
|
154
240
|
}
|
|
155
241
|
throw error;
|
|
156
242
|
}
|
|
157
243
|
}
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
return { ...release, changelog: formatChangelogList(entries) };
|
|
190
|
-
}));
|
|
191
|
-
}
|
|
192
|
-
async function getChangelogEntry(config, pkg, changeset, getCommit) {
|
|
193
|
-
const summary = changeset.summary.trim();
|
|
194
|
-
if (!summary || !config.changelogEntry) {
|
|
195
|
-
return summary;
|
|
196
|
-
}
|
|
197
|
-
const commit = await getCommit(changeset);
|
|
198
|
-
const entry = await config.changelogEntry({
|
|
199
|
-
package: pkg,
|
|
200
|
-
changeset,
|
|
201
|
-
...(commit ? { commit } : {}),
|
|
202
|
-
});
|
|
203
|
-
if ('string' !== typeof entry) {
|
|
204
|
-
throw new ValidationError(`The changelogEntry formatter didn't return a string for changeset ${changeset.fileName}.`);
|
|
205
|
-
}
|
|
206
|
-
return entry.trim();
|
|
207
|
-
}
|
|
208
|
-
/**
|
|
209
|
-
* Find the commit that added a changeset file.
|
|
210
|
-
*
|
|
211
|
-
* @param cwd Repository root.
|
|
212
|
-
* @param file Changeset file.
|
|
213
|
-
* @return Commit, or undefined when the file isn't committed.
|
|
214
|
-
*/
|
|
215
|
-
async function getChangesetCommit(cwd, file) {
|
|
216
|
-
const output = await git(cwd, [
|
|
217
|
-
'log',
|
|
218
|
-
'--diff-filter=A',
|
|
219
|
-
'--format=%H%x1f%h%x1f%s',
|
|
220
|
-
'-1',
|
|
221
|
-
'--',
|
|
222
|
-
path.relative(cwd, file).split(path.sep).join('/'),
|
|
223
|
-
]);
|
|
224
|
-
const [hash, shortHash = '', subject = ''] = output
|
|
225
|
-
.trim()
|
|
226
|
-
.split('\x1f');
|
|
227
|
-
return hash ? { hash, shortHash, subject } : undefined;
|
|
228
|
-
}
|
|
229
|
-
/**
|
|
230
|
-
* Limit a release plan to some packages and the changesets that name them.
|
|
231
|
-
*
|
|
232
|
-
* @param plan Release plan of all pending changesets.
|
|
233
|
-
* @param packages Configured packages.
|
|
234
|
-
* @param names Packages to version.
|
|
235
|
-
* @return Release plan of the named packages.
|
|
236
|
-
* @throws {ValidationError} For unknown packages.
|
|
237
|
-
*/
|
|
238
|
-
function selectReleasePlan(plan, packages, names) {
|
|
239
|
-
names.forEach((name) => {
|
|
240
|
-
if (!packages.some((pkg) => pkg.name === name)) {
|
|
241
|
-
throw new ValidationError(`Unknown package "${name}".`);
|
|
242
|
-
}
|
|
243
|
-
});
|
|
244
|
-
return {
|
|
245
|
-
releases: plan.releases.filter((release) => names.includes(release.package.name)),
|
|
246
|
-
changesets: plan.changesets.filter((changeset) => names.some((name) => Object.hasOwn(changeset.packages, name))),
|
|
247
|
-
};
|
|
248
|
-
}
|
|
249
|
-
/**
|
|
250
|
-
* Remove an applied changeset, or only the applied packages from a changeset
|
|
251
|
-
* that names other packages too, so that their releases keep its summary.
|
|
252
|
-
*
|
|
253
|
-
* @param file Changeset file.
|
|
254
|
-
* @param changeset Parsed changeset.
|
|
255
|
-
* @param applied Versioned packages, or undefined when all were versioned.
|
|
256
|
-
* @param journal File journal for rolling back.
|
|
257
|
-
*/
|
|
258
|
-
async function consumeChangeset(file, changeset, applied, journal) {
|
|
259
|
-
const remaining = Object.entries(changeset.packages).filter(([name]) => undefined !== applied && !applied.includes(name));
|
|
260
|
-
await rememberFile(journal, file);
|
|
261
|
-
if (remaining.length) {
|
|
262
|
-
await writeFile(file, formatChangeset({
|
|
263
|
-
packages: Object.fromEntries(remaining),
|
|
264
|
-
summary: changeset.summary,
|
|
265
|
-
}));
|
|
266
|
-
}
|
|
267
|
-
else {
|
|
268
|
-
await rm(file);
|
|
269
|
-
}
|
|
270
|
-
}
|
|
271
|
-
/**
|
|
272
|
-
* Create missing release tags, optionally pushing them to the configured remote.
|
|
273
|
-
*
|
|
274
|
-
* @param input Repository and tag options.
|
|
275
|
-
* @return Per-package tags and release notes.
|
|
276
|
-
*/
|
|
277
|
-
export async function tagChangesets(input) {
|
|
278
|
-
if (input.githubRelease && !input.push) {
|
|
279
|
-
throw new ValidationError('GitHub Releases require pushing the tags.');
|
|
280
|
-
}
|
|
281
|
-
if (input.untagged && input.push) {
|
|
282
|
-
throw new ValidationError('Listing untagged releases cannot be combined with pushing tags.');
|
|
283
|
-
}
|
|
284
|
-
const context = await getReleaseContext(input);
|
|
285
|
-
if (!context) {
|
|
286
|
-
throw new ValidationError('No wp-release config found. Add wp-release.config.js or pass --config <path>.');
|
|
287
|
-
}
|
|
288
|
-
const selected = selectPackages(context.packages, input);
|
|
289
|
-
const packages = input.since
|
|
290
|
-
? (await changedPackagesSince(input.cwd, selected, input.since))
|
|
291
|
-
.packages
|
|
292
|
-
: selected;
|
|
293
|
-
const results = [];
|
|
294
|
-
const dryRun = input.dryRun || input.untagged;
|
|
295
|
-
const tagFormat = context.config.tagFormat ?? '{path}/{version}';
|
|
296
|
-
for (const pkg of packages) {
|
|
297
|
-
const version = await getTagVersion(pkg, input.version);
|
|
298
|
-
if (undefined !== input.version && input.version !== version) {
|
|
299
|
-
throw new ValidationError(`Package "${pkg.name}" is at version ${version}, not ${input.version}.`);
|
|
300
|
-
}
|
|
301
|
-
const tag = formatTag(tagFormat, pkg, version);
|
|
302
|
-
const exists = await gitTagExists(input.cwd, tag);
|
|
303
|
-
if (!exists) {
|
|
304
|
-
await verifyVersionTargets(input.cwd, pkg, version);
|
|
305
|
-
}
|
|
306
|
-
const previousTag = await getPreviousTag(input.cwd, formatTag(tagFormat, pkg, ''), version);
|
|
307
|
-
const notes = await getReleaseNotes(input.cwd, context.config, pkg, { version, tag, previousTag }, exists ? tag : 'HEAD');
|
|
308
|
-
if (!exists && !dryRun) {
|
|
309
|
-
await git(input.cwd, ['tag', tag]);
|
|
244
|
+
/**
|
|
245
|
+
* Write the version, the dependency specs of bundled sources and the
|
|
246
|
+
* changelog of a release.
|
|
247
|
+
*
|
|
248
|
+
* @param context Release context.
|
|
249
|
+
* @param plan Selected release plan.
|
|
250
|
+
* @param release Release with its changelog entries.
|
|
251
|
+
* @param date Release date, as YYYY-MM-DD.
|
|
252
|
+
* @param journal File journal for rolling back.
|
|
253
|
+
*/
|
|
254
|
+
async writeRelease(context, plan, release, date, journal) {
|
|
255
|
+
const { config } = context;
|
|
256
|
+
const configured = context.packages.find((pkg) => pkg.name === release.package.name);
|
|
257
|
+
if (!configured) {
|
|
258
|
+
throw new ValidationError(`Unknown package "${release.package.name}".`);
|
|
259
|
+
}
|
|
260
|
+
for (const target of configured.versionConfig.to) {
|
|
261
|
+
await writeVersion(configured.directory, target, release.nextVersion, journal);
|
|
262
|
+
}
|
|
263
|
+
for (const sourceName of release.sources ?? []) {
|
|
264
|
+
const source = plan.releases.find((item) => item.package.name === sourceName);
|
|
265
|
+
if (!source) {
|
|
266
|
+
throw new ValidationError(`The dependent package "${release.package.name}" bundles unreleased package "${sourceName}".`);
|
|
267
|
+
}
|
|
268
|
+
await updateDependencyDeclaration({
|
|
269
|
+
dependent: release.package,
|
|
270
|
+
source: source.package,
|
|
271
|
+
version: source.nextVersion,
|
|
272
|
+
rule: config.dependents,
|
|
273
|
+
journal,
|
|
274
|
+
});
|
|
310
275
|
}
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
276
|
+
await verifyVersionTargets(this.repository, configured, release.nextVersion);
|
|
277
|
+
await prependChangelog(configured.directory, release, date, config.readmeChangelog ?? false, config.changelogInsertAfter, journal);
|
|
278
|
+
}
|
|
279
|
+
/**
|
|
280
|
+
* Load the configuration, discover the packages and plan the releases of
|
|
281
|
+
* the pending changesets, including the releases of their dependents.
|
|
282
|
+
*
|
|
283
|
+
* @return Release context, undefined without a config.
|
|
284
|
+
* @throws {ValidationError} For an invalid config, plan or changeset.
|
|
285
|
+
*/
|
|
286
|
+
async loadContext() {
|
|
287
|
+
const loaded = await loadChangesetConfig({
|
|
288
|
+
cwd: this.cwd,
|
|
289
|
+
...(this.configFile ? { configFile: this.configFile } : {}),
|
|
320
290
|
});
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
291
|
+
if (!loaded) {
|
|
292
|
+
return undefined;
|
|
293
|
+
}
|
|
294
|
+
const config = loaded.config;
|
|
295
|
+
const packages = await discoverPackages(this.repository, config);
|
|
296
|
+
const changesets = await readChangesets(this.getChangesetDirectory(config));
|
|
297
|
+
const releases = [];
|
|
298
|
+
packages.forEach((pkg) => {
|
|
299
|
+
const bump = getChangesetBump(changesets, pkg.name);
|
|
300
|
+
if (bump) {
|
|
301
|
+
const packageChangesets = changesets.filter((changeset) => Object.hasOwn(changeset.packages, pkg.name));
|
|
302
|
+
const nextVersion = config.nextVersion
|
|
303
|
+
? config.nextVersion({
|
|
304
|
+
package: pkg,
|
|
305
|
+
bump,
|
|
306
|
+
currentVersion: pkg.currentVersion,
|
|
307
|
+
changesets: packageChangesets,
|
|
308
|
+
})
|
|
309
|
+
: getNextVersion(pkg.currentVersion, bump);
|
|
310
|
+
if ('string' !== typeof nextVersion || !nextVersion.trim()) {
|
|
311
|
+
throw new ValidationError(`The nextVersion strategy returned an empty version for package "${pkg.name}".`);
|
|
312
|
+
}
|
|
313
|
+
releases.push({
|
|
314
|
+
package: pkg,
|
|
315
|
+
bump,
|
|
316
|
+
currentVersion: pkg.currentVersion,
|
|
317
|
+
nextVersion,
|
|
318
|
+
changesets: packageChangesets,
|
|
319
|
+
});
|
|
320
|
+
}
|
|
344
321
|
});
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
322
|
+
changesets.forEach((changeset) => {
|
|
323
|
+
Object.keys(changeset.packages).forEach((name) => {
|
|
324
|
+
if (!packages.some((pkg) => pkg.name === name)) {
|
|
325
|
+
throw new ValidationError(`Changeset ${changeset.fileName} refers to unknown package "${name}".`);
|
|
326
|
+
}
|
|
327
|
+
});
|
|
328
|
+
});
|
|
329
|
+
await applyDependents(releases, packages, config);
|
|
330
|
+
return { config, packages, plan: { changesets, releases } };
|
|
331
|
+
}
|
|
332
|
+
/**
|
|
333
|
+
* Load the release context of a repository that has to be configured.
|
|
334
|
+
*
|
|
335
|
+
* @return Release context.
|
|
336
|
+
* @throws {ValidationError} Without a config.
|
|
337
|
+
*/
|
|
338
|
+
async requireContext() {
|
|
339
|
+
const context = await this.loadContext();
|
|
340
|
+
if (!context) {
|
|
341
|
+
throw new ValidationError(MISSING_CONFIG);
|
|
342
|
+
}
|
|
343
|
+
return context;
|
|
344
|
+
}
|
|
345
|
+
/**
|
|
346
|
+
* Load the configuration of a repository that has to be configured.
|
|
347
|
+
*
|
|
348
|
+
* @return Repository release configuration.
|
|
349
|
+
* @throws {ValidationError} Without a config.
|
|
350
|
+
*/
|
|
351
|
+
async requireConfig() {
|
|
352
|
+
const loaded = await loadChangesetConfig({
|
|
353
|
+
cwd: this.cwd,
|
|
354
|
+
...(this.configFile ? { configFile: this.configFile } : {}),
|
|
355
|
+
});
|
|
356
|
+
if (!loaded) {
|
|
357
|
+
throw new ValidationError(MISSING_CONFIG);
|
|
362
358
|
}
|
|
359
|
+
return loaded.config;
|
|
363
360
|
}
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
* @return Version to tag.
|
|
373
|
-
*/
|
|
374
|
-
async function getTagVersion(pkg, expected) {
|
|
375
|
-
if ('git-tag' !== pkg.versionConfig.from.kind) {
|
|
376
|
-
return pkg.currentVersion;
|
|
361
|
+
/**
|
|
362
|
+
* Get the directory of the changeset files.
|
|
363
|
+
*
|
|
364
|
+
* @param config Repository release configuration.
|
|
365
|
+
* @return Absolute directory path.
|
|
366
|
+
*/
|
|
367
|
+
getChangesetDirectory(config) {
|
|
368
|
+
return path.resolve(this.cwd, config.changesets ?? '.changeset');
|
|
377
369
|
}
|
|
378
|
-
return (expected ??
|
|
379
|
-
(await getLatestChangelogVersion(pkg.directory, pkg.currentVersion)));
|
|
380
370
|
}
|
|
381
371
|
/**
|
|
382
|
-
*
|
|
372
|
+
* Discover configured packages, pending changesets, and their proposed versions.
|
|
383
373
|
*
|
|
384
|
-
* @param
|
|
385
|
-
* @
|
|
386
|
-
* @return Selected packages in the configured order.
|
|
387
|
-
* @throws {ValidationError} For unknown packages, or --version without a single package.
|
|
374
|
+
* @param input Repository root and optional explicit config file.
|
|
375
|
+
* @return A release plan; `undefined` when no config is present.
|
|
388
376
|
*/
|
|
389
|
-
function
|
|
390
|
-
|
|
391
|
-
if (undefined !== input.version && 1 !== names.length) {
|
|
392
|
-
throw new ValidationError('An expected version requires exactly one package.');
|
|
393
|
-
}
|
|
394
|
-
names.forEach((name) => {
|
|
395
|
-
if (!packages.some((pkg) => pkg.name === name)) {
|
|
396
|
-
throw new ValidationError(`Unknown package "${name}".`);
|
|
397
|
-
}
|
|
398
|
-
});
|
|
399
|
-
return names.length
|
|
400
|
-
? packages.filter((pkg) => names.includes(pkg.name))
|
|
401
|
-
: packages;
|
|
377
|
+
export async function getChangesetReleasePlan(input) {
|
|
378
|
+
return new ChangesetRelease(input).getPlan();
|
|
402
379
|
}
|
|
403
380
|
/**
|
|
404
|
-
*
|
|
381
|
+
* Discover the packages of the repository configuration.
|
|
405
382
|
*
|
|
406
|
-
* @param
|
|
407
|
-
* @
|
|
408
|
-
* @
|
|
409
|
-
* @return Previous tag, or undefined for the first release.
|
|
383
|
+
* @param input Repository root and optional explicit config file.
|
|
384
|
+
* @return Packages in the configured order with their current versions.
|
|
385
|
+
* @throws {ValidationError} Without a config, or for an invalid one.
|
|
410
386
|
*/
|
|
411
|
-
async function
|
|
412
|
-
|
|
413
|
-
return output
|
|
414
|
-
.trim()
|
|
415
|
-
.split(/\r?\n/)
|
|
416
|
-
.filter((tag) => tag.startsWith(prefix))
|
|
417
|
-
.map((tag) => ({ tag, version: tag.slice(prefix.length) }))
|
|
418
|
-
.filter((item) => isVersion(item.version) &&
|
|
419
|
-
compareVersions(item.version, version) < 0)
|
|
420
|
-
.sort((a, b) => compareVersions(b.version, a.version))[0]?.tag;
|
|
387
|
+
export async function getChangesetPackages(input) {
|
|
388
|
+
return new ChangesetRelease(input).getPackages();
|
|
421
389
|
}
|
|
422
390
|
/**
|
|
423
|
-
*
|
|
391
|
+
* Create a Changesets-compatible Markdown file.
|
|
424
392
|
*
|
|
425
|
-
* @param
|
|
426
|
-
* @
|
|
427
|
-
* @param pkg Released package.
|
|
428
|
-
* @param release Version, tag and previous tag of the release.
|
|
429
|
-
* @param release.version Released version.
|
|
430
|
-
* @param release.tag Release tag.
|
|
431
|
-
* @param release.previousTag Tag of the previous release, if any.
|
|
432
|
-
* @param end Commit of the release: the tag, or HEAD before tagging.
|
|
433
|
-
* @return Release notes.
|
|
393
|
+
* @param input Repository, packages and summary.
|
|
394
|
+
* @return Created file path.
|
|
434
395
|
*/
|
|
435
|
-
async function
|
|
436
|
-
|
|
437
|
-
const commits = await getReleaseCommits(cwd, pkg, end, release.previousTag);
|
|
438
|
-
const notes = changelog ||
|
|
439
|
-
commits
|
|
440
|
-
.map((commit) => `- ${commit.subject} (${commit.shortHash})`)
|
|
441
|
-
.join('\n');
|
|
442
|
-
if (!config.releaseNotes) {
|
|
443
|
-
return notes;
|
|
444
|
-
}
|
|
445
|
-
const formatted = await config.releaseNotes({
|
|
446
|
-
package: toChangesetPackage(pkg),
|
|
447
|
-
...release,
|
|
448
|
-
changelog,
|
|
449
|
-
commits,
|
|
450
|
-
notes,
|
|
451
|
-
});
|
|
452
|
-
if ('string' !== typeof formatted) {
|
|
453
|
-
throw new ValidationError(`The releaseNotes formatter didn't return a string for package "${pkg.name}".`);
|
|
454
|
-
}
|
|
455
|
-
return formatted.trim();
|
|
396
|
+
export async function addChangeset(input) {
|
|
397
|
+
return new ChangesetRelease(input).add(input);
|
|
456
398
|
}
|
|
457
399
|
/**
|
|
458
|
-
*
|
|
400
|
+
* Apply pending package versions and changelogs.
|
|
459
401
|
*
|
|
460
|
-
* @param
|
|
461
|
-
* @
|
|
462
|
-
* @param end Commit of the release.
|
|
463
|
-
* @param previousTag Tag of the previous release, if any.
|
|
464
|
-
* @return Commits, newest first.
|
|
402
|
+
* @param input Repository and versioning options.
|
|
403
|
+
* @return Planned releases and whether files were changed.
|
|
465
404
|
*/
|
|
466
|
-
async function
|
|
467
|
-
|
|
468
|
-
'.';
|
|
469
|
-
const output = await git(cwd, [
|
|
470
|
-
'log',
|
|
471
|
-
'--format=%H%x1f%h%x1f%s',
|
|
472
|
-
previousTag ? `${previousTag}..${end}` : end,
|
|
473
|
-
'--',
|
|
474
|
-
packageFromRepo,
|
|
475
|
-
]);
|
|
476
|
-
return output
|
|
477
|
-
.trim()
|
|
478
|
-
.split(/\r?\n/)
|
|
479
|
-
.filter(Boolean)
|
|
480
|
-
.map((line) => {
|
|
481
|
-
const [hash = '', shortHash = '', subject = ''] = line.split('\x1f');
|
|
482
|
-
return { hash, shortHash, subject };
|
|
483
|
-
});
|
|
405
|
+
export async function versionChangesets(input) {
|
|
406
|
+
return new ChangesetRelease(input).version(input);
|
|
484
407
|
}
|
|
485
408
|
/**
|
|
486
|
-
*
|
|
409
|
+
* Create missing release tags, optionally pushing them to the configured remote.
|
|
487
410
|
*
|
|
488
|
-
* @param
|
|
489
|
-
* @return
|
|
411
|
+
* @param input Repository and tag options.
|
|
412
|
+
* @return Per-package tags and release notes.
|
|
490
413
|
*/
|
|
491
|
-
function
|
|
492
|
-
return
|
|
493
|
-
name: pkg.name,
|
|
494
|
-
directory: pkg.directory,
|
|
495
|
-
path: pkg.path,
|
|
496
|
-
type: pkg.type,
|
|
497
|
-
currentVersion: pkg.currentVersion,
|
|
498
|
-
};
|
|
414
|
+
export async function tagChangesets(input) {
|
|
415
|
+
return new ChangesetRelease(input).tag(input);
|
|
499
416
|
}
|
|
500
417
|
/**
|
|
501
418
|
* Check whether changed files belong to packages without a changeset.
|
|
@@ -504,646 +421,5 @@ function toChangesetPackage(pkg) {
|
|
|
504
421
|
* @return Changed packages and missing changeset identifiers.
|
|
505
422
|
*/
|
|
506
423
|
export async function getChangesetStatus(input) {
|
|
507
|
-
|
|
508
|
-
if (!context) {
|
|
509
|
-
throw new ValidationError('No wp-release config found. Add wp-release.config.js or pass --config <path>.');
|
|
510
|
-
}
|
|
511
|
-
const { plan } = context;
|
|
512
|
-
const { changesets, releases } = plan;
|
|
513
|
-
const since = input.since ??
|
|
514
|
-
(context.config.baseBranch
|
|
515
|
-
? `origin/${context.config.baseBranch}`
|
|
516
|
-
: undefined);
|
|
517
|
-
const changed = since
|
|
518
|
-
? await changedPackagesSince(input.cwd, context.packages, since)
|
|
519
|
-
: { packages: [], filesByPackage: {} };
|
|
520
|
-
const changedPackages = changed.packages.map((pkg) => pkg.name);
|
|
521
|
-
const changesetsInDiff = since
|
|
522
|
-
? await readChangesetsSince(input.cwd, context.config, changesets, since)
|
|
523
|
-
: [];
|
|
524
|
-
const hasEmptyChangeset = changesetsInDiff.some((changeset) => 0 === Object.keys(changeset.packages).length);
|
|
525
|
-
const missingChangesets = hasEmptyChangeset
|
|
526
|
-
? []
|
|
527
|
-
: changedPackages.filter((name) => !changesetsInDiff.some((item) => Object.hasOwn(item.packages, name)));
|
|
528
|
-
return {
|
|
529
|
-
changesets,
|
|
530
|
-
releases,
|
|
531
|
-
changedPackages,
|
|
532
|
-
changedPackageFiles: changed.filesByPackage,
|
|
533
|
-
missingChangesets,
|
|
534
|
-
};
|
|
535
|
-
}
|
|
536
|
-
async function requireConfig(input) {
|
|
537
|
-
const loaded = await loadChangesetConfig(input);
|
|
538
|
-
if (!loaded) {
|
|
539
|
-
throw new ValidationError('No wp-release config found. Add wp-release.config.js or pass --config <path>.');
|
|
540
|
-
}
|
|
541
|
-
return loaded;
|
|
542
|
-
}
|
|
543
|
-
async function readChangesets(cwd, config) {
|
|
544
|
-
const directory = path.resolve(cwd, config.changesets ?? '.changeset');
|
|
545
|
-
let entries;
|
|
546
|
-
try {
|
|
547
|
-
entries = await readdir(directory);
|
|
548
|
-
}
|
|
549
|
-
catch (error) {
|
|
550
|
-
if ('ENOENT' === error.code) {
|
|
551
|
-
return [];
|
|
552
|
-
}
|
|
553
|
-
throw error;
|
|
554
|
-
}
|
|
555
|
-
return Promise.all(entries
|
|
556
|
-
.filter((name) => name.endsWith('.md') && !name.startsWith('README'))
|
|
557
|
-
.sort()
|
|
558
|
-
.map(async (name) => parseChangeset(await readFile(path.join(directory, name), 'utf8'), name)));
|
|
559
|
-
}
|
|
560
|
-
async function readChangesetsSince(cwd, config, changesets, since) {
|
|
561
|
-
const directory = path.resolve(cwd, config.changesets ?? '.changeset');
|
|
562
|
-
const relativeDirectory = path
|
|
563
|
-
.relative(cwd, directory)
|
|
564
|
-
.split(path.sep)
|
|
565
|
-
.join('/');
|
|
566
|
-
const output = await git(cwd, [
|
|
567
|
-
'diff',
|
|
568
|
-
'--name-only',
|
|
569
|
-
'--diff-filter=ACMR',
|
|
570
|
-
`${since}...HEAD`,
|
|
571
|
-
'--',
|
|
572
|
-
relativeDirectory,
|
|
573
|
-
]);
|
|
574
|
-
const changedFiles = new Set(output.trim().split(/\r?\n/).filter(Boolean));
|
|
575
|
-
return changesets.filter((changeset) => changedFiles.has(path.posix.join(relativeDirectory, changeset.fileName)));
|
|
576
|
-
}
|
|
577
|
-
async function discoverPackages(cwd, config) {
|
|
578
|
-
const root = path.resolve(cwd, config.root ?? '.');
|
|
579
|
-
const packages = [];
|
|
580
|
-
const hasRecursiveGlob = config.packages.some((item) => item.glob.split('/').includes('**'));
|
|
581
|
-
const directories = await findDirectories(root, '', hasRecursiveGlob
|
|
582
|
-
? Infinity
|
|
583
|
-
: Math.max(1, ...config.packages.map((item) => item.glob.split('/').length + 2)));
|
|
584
|
-
for (const group of config.packages) {
|
|
585
|
-
const matcher = globRegExp(group.glob);
|
|
586
|
-
const candidates = matcher.test('.')
|
|
587
|
-
? ['.', ...directories]
|
|
588
|
-
: directories;
|
|
589
|
-
const matches = candidates
|
|
590
|
-
.filter((item) => matcher.test(item) &&
|
|
591
|
-
!(group.exclude ?? []).some((excluded) => globRegExp(excluded).test(item)))
|
|
592
|
-
.sort();
|
|
593
|
-
if (!matches.length) {
|
|
594
|
-
throw new ValidationError(`Package glob "${group.glob}" did not match any directories under ${root}.`);
|
|
595
|
-
}
|
|
596
|
-
for (const relativePath of matches) {
|
|
597
|
-
const directory = path.join(root, relativePath);
|
|
598
|
-
const name = await packageName(directory, relativePath, group.idFrom ?? 'path');
|
|
599
|
-
const currentVersion = await readVersion(cwd, directory, group.version.from, relativePath, group.tagType);
|
|
600
|
-
if (packages.some((pkg) => pkg.name === name)) {
|
|
601
|
-
throw new ValidationError(`Package identifier "${name}" is configured more than once.`);
|
|
602
|
-
}
|
|
603
|
-
packages.push({
|
|
604
|
-
name,
|
|
605
|
-
directory,
|
|
606
|
-
path: relativePath,
|
|
607
|
-
type: packageType(group),
|
|
608
|
-
currentVersion,
|
|
609
|
-
group,
|
|
610
|
-
versionConfig: group.version,
|
|
611
|
-
});
|
|
612
|
-
}
|
|
613
|
-
}
|
|
614
|
-
return packages;
|
|
615
|
-
}
|
|
616
|
-
async function findDirectories(root, prefix = '', maxDepth = 5) {
|
|
617
|
-
let entries;
|
|
618
|
-
try {
|
|
619
|
-
entries = await readdir(path.join(root, prefix), {
|
|
620
|
-
withFileTypes: true,
|
|
621
|
-
});
|
|
622
|
-
}
|
|
623
|
-
catch (error) {
|
|
624
|
-
if ('ENOENT' === error.code) {
|
|
625
|
-
return [];
|
|
626
|
-
}
|
|
627
|
-
throw error;
|
|
628
|
-
}
|
|
629
|
-
const directories = entries.filter((item) => item.isDirectory() &&
|
|
630
|
-
!['.git', 'node_modules', 'vendor', 'dist'].includes(item.name));
|
|
631
|
-
if (prefix.split(path.sep).filter(Boolean).length >= maxDepth) {
|
|
632
|
-
return directories.map((item) => path.join(prefix, item.name));
|
|
633
|
-
}
|
|
634
|
-
const children = await Promise.all(directories.map((item) => findDirectories(root, path.join(prefix, item.name), maxDepth)));
|
|
635
|
-
return [
|
|
636
|
-
...directories.map((item) => path.join(prefix, item.name)),
|
|
637
|
-
...children.flat(),
|
|
638
|
-
];
|
|
639
|
-
}
|
|
640
|
-
function globRegExp(glob) {
|
|
641
|
-
const escaped = glob
|
|
642
|
-
.split('/')
|
|
643
|
-
.map((part) => {
|
|
644
|
-
if ('**' === part) {
|
|
645
|
-
return '.*';
|
|
646
|
-
}
|
|
647
|
-
return part
|
|
648
|
-
.replaceAll(/[.+^${}()|[\]\\]/g, '\\$&')
|
|
649
|
-
.replaceAll('*', '[^/]*')
|
|
650
|
-
.replaceAll('?', '[^/]');
|
|
651
|
-
})
|
|
652
|
-
.join('/');
|
|
653
|
-
return new RegExp(`^${escaped}$`);
|
|
654
|
-
}
|
|
655
|
-
async function packageName(directory, relativePath, idFrom) {
|
|
656
|
-
if ('path' === idFrom) {
|
|
657
|
-
if ('.' === relativePath) {
|
|
658
|
-
return path.basename(directory);
|
|
659
|
-
}
|
|
660
|
-
return relativePath.split(path.sep).join('/');
|
|
661
|
-
}
|
|
662
|
-
const [file, field] = idFrom.split('#');
|
|
663
|
-
if (!file || !field) {
|
|
664
|
-
throw new ValidationError(`Invalid package identifier source "${idFrom}".`);
|
|
665
|
-
}
|
|
666
|
-
const manifest = await readJson(path.join(directory, file));
|
|
667
|
-
const name = manifest[field];
|
|
668
|
-
if ('string' !== typeof name || !name.trim()) {
|
|
669
|
-
throw new ValidationError(`Expected ${file}#${field} in ${directory}.`);
|
|
670
|
-
}
|
|
671
|
-
return name;
|
|
672
|
-
}
|
|
673
|
-
async function readVersion(cwd, directory, descriptor, relativePath, tagType) {
|
|
674
|
-
if ('package-json' === descriptor.kind ||
|
|
675
|
-
'composer-json' === descriptor.kind) {
|
|
676
|
-
const manifest = await readJson(path.join(directory, descriptor.file));
|
|
677
|
-
const version = manifest[descriptor.field];
|
|
678
|
-
if ('string' === typeof version && version.trim()) {
|
|
679
|
-
return version;
|
|
680
|
-
}
|
|
681
|
-
throw new ValidationError(`Missing ${descriptor.file}#${descriptor.field} in ${directory}.`);
|
|
682
|
-
}
|
|
683
|
-
if ('git-tag' === descriptor.kind) {
|
|
684
|
-
const pattern = descriptor.pattern ?? '{path}/{version}';
|
|
685
|
-
const tagsOutput = await git(cwd, [
|
|
686
|
-
'tag',
|
|
687
|
-
'--list',
|
|
688
|
-
`${formatTemplatePrefix(pattern, relativePath, tagType)}*`,
|
|
689
|
-
]);
|
|
690
|
-
const tags = tagsOutput.trim().split(/\r?\n/).filter(Boolean);
|
|
691
|
-
const versions = tags
|
|
692
|
-
.map((tag) => tag.match(/(?:^|\/|v)(\d+\.\d+\.\d+)$/)?.[1])
|
|
693
|
-
.filter((item) => Boolean(item));
|
|
694
|
-
versions.sort((a, b) => compareVersions(b, a));
|
|
695
|
-
if (versions[0]) {
|
|
696
|
-
return versions[0];
|
|
697
|
-
}
|
|
698
|
-
throw new ValidationError(`No git tag matches ${pattern} for ${relativePath}.`);
|
|
699
|
-
}
|
|
700
|
-
if ('php-constant' === descriptor.kind) {
|
|
701
|
-
const file = path.join(directory, descriptor.file);
|
|
702
|
-
const contents = await readFile(file, 'utf8');
|
|
703
|
-
const versions = findConstantValues(contents, descriptor.constant);
|
|
704
|
-
if (versions[0]) {
|
|
705
|
-
return versions[0];
|
|
706
|
-
}
|
|
707
|
-
throw new ValidationError(`Missing PHP constant ${descriptor.constant} in ${file}.`);
|
|
708
|
-
}
|
|
709
|
-
const file = path.join(directory, await getVersionFile(directory, descriptor.kind, descriptor.file));
|
|
710
|
-
const contents = await readFile(file, 'utf8');
|
|
711
|
-
const label = 'wordpress-stable-tag' === descriptor.kind ? 'Stable tag' : 'Version';
|
|
712
|
-
const match = new RegExp(`^\\s*\\*?\\s*${label}:\\s*([^\\r\\n]+)`, 'im').exec(contents);
|
|
713
|
-
if (!match) {
|
|
714
|
-
throw new ValidationError(`Missing ${label} header in ${file}.`);
|
|
715
|
-
}
|
|
716
|
-
return match[1]?.trim() ?? '';
|
|
717
|
-
}
|
|
718
|
-
async function writeVersion(directory, descriptor, version, journal) {
|
|
719
|
-
if ('package-json' === descriptor.kind ||
|
|
720
|
-
'composer-json' === descriptor.kind) {
|
|
721
|
-
const file = path.join(directory, descriptor.file);
|
|
722
|
-
const contents = await readFile(file, 'utf8');
|
|
723
|
-
const manifest = JSON.parse(contents);
|
|
724
|
-
if ('composer-json' === descriptor.kind &&
|
|
725
|
-
descriptor.ifPresent &&
|
|
726
|
-
!Object.hasOwn(manifest, descriptor.field)) {
|
|
727
|
-
return;
|
|
728
|
-
}
|
|
729
|
-
manifest[descriptor.field] = version;
|
|
730
|
-
await rememberFile(journal, file);
|
|
731
|
-
await writeFile(file, `${JSON.stringify(manifest, null, '\t')}\n`);
|
|
732
|
-
return;
|
|
733
|
-
}
|
|
734
|
-
if ('php-constant' === descriptor.kind) {
|
|
735
|
-
const file = path.join(directory, descriptor.file);
|
|
736
|
-
const contents = await readFile(file, 'utf8');
|
|
737
|
-
const constant = escapeRegExp(descriptor.constant);
|
|
738
|
-
const definePattern = new RegExp(`(\\bdefine\\s*\\(\\s*(['"])${constant}\\2\\s*,\\s*)(['"])(.*?)\\3`, 'g');
|
|
739
|
-
const declarationPattern = new RegExp(`(\\bconst\\s+${constant}\\s*=\\s*)(['"])(.*?)\\2`, 'g');
|
|
740
|
-
const updated = contents
|
|
741
|
-
.replace(definePattern, (_match, prefix, _nameQuote, quote) => `${prefix}${quote}${version}${quote}`)
|
|
742
|
-
.replace(declarationPattern, (_match, prefix, quote) => `${prefix}${quote}${version}${quote}`);
|
|
743
|
-
if (updated === contents) {
|
|
744
|
-
throw new ValidationError(`Missing PHP constant ${descriptor.constant} in ${file}.`);
|
|
745
|
-
}
|
|
746
|
-
await rememberFile(journal, file);
|
|
747
|
-
await writeFile(file, updated);
|
|
748
|
-
return;
|
|
749
|
-
}
|
|
750
|
-
if ('wordpress-stable-tag' === descriptor.kind &&
|
|
751
|
-
descriptor.ifPresent &&
|
|
752
|
-
!(await versionFileExists(directory, descriptor.kind, descriptor.file))) {
|
|
753
|
-
return;
|
|
754
|
-
}
|
|
755
|
-
const file = path.join(directory, await getVersionFile(directory, descriptor.kind, descriptor.file));
|
|
756
|
-
const contents = await readFile(file, 'utf8');
|
|
757
|
-
const label = 'wordpress-stable-tag' === descriptor.kind ? 'Stable tag' : 'Version';
|
|
758
|
-
const expression = new RegExp(`(^\\s*\\*?\\s*${label}:\\s*)[^\\r\\n]+`, 'im');
|
|
759
|
-
if (!expression.test(contents)) {
|
|
760
|
-
throw new ValidationError(`Missing ${label} header in ${file}.`);
|
|
761
|
-
}
|
|
762
|
-
await rememberFile(journal, file);
|
|
763
|
-
await writeFile(file, contents.replace(expression, `$1${version}`));
|
|
764
|
-
}
|
|
765
|
-
async function getVersionFile(directory, kind, configuredFile) {
|
|
766
|
-
if (configuredFile) {
|
|
767
|
-
return configuredFile;
|
|
768
|
-
}
|
|
769
|
-
if ('wordpress-plugin-header' === kind) {
|
|
770
|
-
const file = await findMainPluginFile({ dir: directory });
|
|
771
|
-
if (file) {
|
|
772
|
-
return file;
|
|
773
|
-
}
|
|
774
|
-
throw new ValidationError(`Could not find a plugin header file in ${directory}.`);
|
|
775
|
-
}
|
|
776
|
-
if ('wordpress-stable-tag' === kind) {
|
|
777
|
-
for (const file of ['readme.txt', 'readme.md']) {
|
|
778
|
-
try {
|
|
779
|
-
await access(path.join(directory, file));
|
|
780
|
-
return file;
|
|
781
|
-
}
|
|
782
|
-
catch {
|
|
783
|
-
// Try the next WordPress readme filename.
|
|
784
|
-
}
|
|
785
|
-
}
|
|
786
|
-
throw new ValidationError(`Could not find readme.txt or readme.md in ${directory}.`);
|
|
787
|
-
}
|
|
788
|
-
return 'style.css';
|
|
789
|
-
}
|
|
790
|
-
async function versionFileExists(directory, kind, configuredFile) {
|
|
791
|
-
if (configuredFile) {
|
|
792
|
-
try {
|
|
793
|
-
await access(path.join(directory, configuredFile));
|
|
794
|
-
return true;
|
|
795
|
-
}
|
|
796
|
-
catch {
|
|
797
|
-
return false;
|
|
798
|
-
}
|
|
799
|
-
}
|
|
800
|
-
if ('wordpress-stable-tag' === kind) {
|
|
801
|
-
const files = await Promise.all(['readme.txt', 'readme.md'].map(async (file) => {
|
|
802
|
-
try {
|
|
803
|
-
await access(path.join(directory, file));
|
|
804
|
-
return true;
|
|
805
|
-
}
|
|
806
|
-
catch {
|
|
807
|
-
return false;
|
|
808
|
-
}
|
|
809
|
-
}));
|
|
810
|
-
return files.some(Boolean);
|
|
811
|
-
}
|
|
812
|
-
return true;
|
|
813
|
-
}
|
|
814
|
-
async function prependChangelog(directory, release, date, readmeChangelog, changelogInsertAfter, journal) {
|
|
815
|
-
const summaries = release.changesets
|
|
816
|
-
.map((item) => item.summary.trim())
|
|
817
|
-
.filter(Boolean);
|
|
818
|
-
const section = `## ${release.nextVersion} (${date})\n\n${release.changelog}\n\n`;
|
|
819
|
-
const changelog = path.join(directory, 'CHANGELOG.md');
|
|
820
|
-
let previous = '';
|
|
821
|
-
try {
|
|
822
|
-
previous = await readFile(changelog, 'utf8');
|
|
823
|
-
}
|
|
824
|
-
catch (error) {
|
|
825
|
-
if ('ENOENT' !== error.code) {
|
|
826
|
-
throw error;
|
|
827
|
-
}
|
|
828
|
-
}
|
|
829
|
-
const releaseHeading = new RegExp(`^## ${escapeRegExp(release.nextVersion)} \\(`, 'm');
|
|
830
|
-
if (!releaseHeading.test(previous)) {
|
|
831
|
-
await rememberFile(journal, changelog);
|
|
832
|
-
if (undefined !== changelogInsertAfter) {
|
|
833
|
-
await writeFile(changelog, insertAfterMarker(previous, changelogInsertAfter, section, changelog));
|
|
834
|
-
}
|
|
835
|
-
else {
|
|
836
|
-
const versionHeading = /^##[ \t]+v?\d+(?:\.\d+){1,2}(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?(?:[ \t]+.*)?$/m.exec(previous);
|
|
837
|
-
const heading = /^#[ \t]+[^\r\n]+[ \t]*$/m.exec(previous);
|
|
838
|
-
const insertion = versionHeading ?? heading;
|
|
839
|
-
if (insertion) {
|
|
840
|
-
const before = previous
|
|
841
|
-
.slice(0, insertion.index)
|
|
842
|
-
.replace(/(?:\r?\n)*$/, '');
|
|
843
|
-
const after = previous.slice(insertion.index);
|
|
844
|
-
const prefix = before ? `${before}\n\n` : '';
|
|
845
|
-
await writeFile(changelog, `${prefix}${section}${after}`);
|
|
846
|
-
}
|
|
847
|
-
else {
|
|
848
|
-
await writeFile(changelog, `${section}${previous}`);
|
|
849
|
-
}
|
|
850
|
-
}
|
|
851
|
-
}
|
|
852
|
-
if (readmeChangelog) {
|
|
853
|
-
const readme = ['readme.txt', 'readme.md'].map((name) => path.join(directory, name));
|
|
854
|
-
let readmeUpdated = false;
|
|
855
|
-
let readmeFound = false;
|
|
856
|
-
let readmeMarkerFound = false;
|
|
857
|
-
let readmeAlreadyVersioned = false;
|
|
858
|
-
for (const file of readme) {
|
|
859
|
-
try {
|
|
860
|
-
const contents = await readFile(file, 'utf8');
|
|
861
|
-
readmeFound = true;
|
|
862
|
-
const defaultHeading = undefined === changelogInsertAfter
|
|
863
|
-
? /^==\s*Changelog\s*==\s*$/im.exec(contents)?.[0]
|
|
864
|
-
: undefined;
|
|
865
|
-
const insertionMarker = changelogInsertAfter ?? defaultHeading;
|
|
866
|
-
readmeMarkerFound ||=
|
|
867
|
-
undefined !== insertionMarker &&
|
|
868
|
-
contents.includes(insertionMarker);
|
|
869
|
-
const alreadyVersioned = new RegExp(`^=\\s*${escapeRegExp(release.nextVersion)}\\s*=$`, 'm').test(contents);
|
|
870
|
-
readmeAlreadyVersioned ||= alreadyVersioned;
|
|
871
|
-
if (insertionMarker &&
|
|
872
|
-
!alreadyVersioned &&
|
|
873
|
-
!readmeUpdated &&
|
|
874
|
-
contents.includes(insertionMarker)) {
|
|
875
|
-
const readmeSection = `\n\n= ${release.nextVersion} =\n\n${summaries
|
|
876
|
-
.map((item) => `* ${item.replaceAll(/\r?\n/g, '\n ')}`)
|
|
877
|
-
.join('\n')}\n\n`;
|
|
878
|
-
await rememberFile(journal, file);
|
|
879
|
-
await writeFile(file, insertAfterMarker(contents, insertionMarker, readmeSection.trimStart(), file));
|
|
880
|
-
readmeUpdated = true;
|
|
881
|
-
}
|
|
882
|
-
}
|
|
883
|
-
catch (error) {
|
|
884
|
-
if ('ENOENT' !== error.code) {
|
|
885
|
-
throw error;
|
|
886
|
-
}
|
|
887
|
-
}
|
|
888
|
-
}
|
|
889
|
-
if (changelogInsertAfter &&
|
|
890
|
-
readmeFound &&
|
|
891
|
-
!readmeMarkerFound &&
|
|
892
|
-
!readmeAlreadyVersioned) {
|
|
893
|
-
throw new ValidationError(`Changelog insertion marker "${changelogInsertAfter}" was not found in a plugin readme in ${directory}.`);
|
|
894
|
-
}
|
|
895
|
-
}
|
|
896
|
-
}
|
|
897
|
-
function insertAfterMarker(contents, marker, section, file) {
|
|
898
|
-
const index = contents.indexOf(marker);
|
|
899
|
-
if (-1 === index) {
|
|
900
|
-
throw new ValidationError(`Changelog insertion marker "${marker}" was not found in ${file}.`);
|
|
901
|
-
}
|
|
902
|
-
const markerEnd = index + marker.length;
|
|
903
|
-
const after = contents
|
|
904
|
-
.slice(markerEnd)
|
|
905
|
-
.replace(/^(?:[ \t]*\r?\n)+/, '');
|
|
906
|
-
const lineEnding = contents.includes('\r\n') ? '\r\n' : '\n';
|
|
907
|
-
return `${contents.slice(0, markerEnd)}${lineEnding}${lineEnding}${section.replaceAll('\n', lineEnding)}${after}`;
|
|
908
|
-
}
|
|
909
|
-
async function changedPackagesSince(cwd, packages, since) {
|
|
910
|
-
const output = await git(cwd, [
|
|
911
|
-
'diff',
|
|
912
|
-
'--name-only',
|
|
913
|
-
`${since}...HEAD`,
|
|
914
|
-
]);
|
|
915
|
-
const changedFiles = output.trim().split(/\r?\n/).filter(Boolean);
|
|
916
|
-
const filesByPackage = {};
|
|
917
|
-
const changed = packages.filter((pkg) => {
|
|
918
|
-
const packageFromRepo = path
|
|
919
|
-
.relative(cwd, pkg.directory)
|
|
920
|
-
.split(path.sep)
|
|
921
|
-
.join('/');
|
|
922
|
-
const matchingFiles = '.' === packageFromRepo
|
|
923
|
-
? changedFiles
|
|
924
|
-
: changedFiles.filter((file) => file === packageFromRepo ||
|
|
925
|
-
file.startsWith(`${packageFromRepo}/`));
|
|
926
|
-
if (matchingFiles.length) {
|
|
927
|
-
filesByPackage[pkg.name] = matchingFiles;
|
|
928
|
-
}
|
|
929
|
-
return matchingFiles.length > 0;
|
|
930
|
-
});
|
|
931
|
-
return { packages: changed, filesByPackage };
|
|
932
|
-
}
|
|
933
|
-
function formatTag(format, pkg, version) {
|
|
934
|
-
const type = pkg.group.tagType ?? defaultTagType(pkg.path, pkg.type);
|
|
935
|
-
return format
|
|
936
|
-
.replaceAll('{name}', pkg.name)
|
|
937
|
-
.replaceAll('{slug}', '.' === pkg.path
|
|
938
|
-
? path.basename(pkg.directory)
|
|
939
|
-
: path.basename(pkg.path))
|
|
940
|
-
.replaceAll('{path}', pkg.path)
|
|
941
|
-
.replaceAll('{type}', type)
|
|
942
|
-
.replaceAll('{version}', version);
|
|
943
|
-
}
|
|
944
|
-
function defaultTagType(packagePath, type) {
|
|
945
|
-
if ('.' === packagePath && type) {
|
|
946
|
-
return type;
|
|
947
|
-
}
|
|
948
|
-
const first = packagePath.split('/')[0] ?? 'package';
|
|
949
|
-
const irregular = {
|
|
950
|
-
actions: 'action',
|
|
951
|
-
plugins: 'plugin',
|
|
952
|
-
themes: 'theme',
|
|
953
|
-
php: 'php',
|
|
954
|
-
js: 'js',
|
|
955
|
-
};
|
|
956
|
-
return irregular[first] ?? first.replace(/s$/, '');
|
|
957
|
-
}
|
|
958
|
-
function formatTemplatePrefix(pattern, packagePath, tagType) {
|
|
959
|
-
return pattern
|
|
960
|
-
.replaceAll('{path}', packagePath)
|
|
961
|
-
.replaceAll('{type}', tagType ?? defaultTagType(packagePath))
|
|
962
|
-
.replaceAll('{name}', packagePath.split('/').at(-1) ?? packagePath)
|
|
963
|
-
.replaceAll('{slug}', packagePath.split('/').at(-1) ?? packagePath)
|
|
964
|
-
.replaceAll('{version}', '');
|
|
965
|
-
}
|
|
966
|
-
async function verifyVersionTargets(cwd, pkg, expected) {
|
|
967
|
-
for (const target of pkg.versionConfig.to) {
|
|
968
|
-
let actual;
|
|
969
|
-
if ('composer-json' === target.kind &&
|
|
970
|
-
target.ifPresent &&
|
|
971
|
-
!Object.hasOwn(await readJson(path.join(pkg.directory, target.file)), target.field)) {
|
|
972
|
-
actual = undefined;
|
|
973
|
-
}
|
|
974
|
-
else if ('wordpress-stable-tag' === target.kind &&
|
|
975
|
-
target.ifPresent &&
|
|
976
|
-
!(await versionFileExists(pkg.directory, target.kind, target.file))) {
|
|
977
|
-
actual = undefined;
|
|
978
|
-
}
|
|
979
|
-
else {
|
|
980
|
-
actual = await readVersion(cwd, pkg.directory, target, pkg.path, pkg.group.tagType);
|
|
981
|
-
}
|
|
982
|
-
if (undefined !== actual && actual !== expected) {
|
|
983
|
-
throw new ValidationError(`Version mismatch for ${pkg.name}: expected ${expected}, found ${actual}.`);
|
|
984
|
-
}
|
|
985
|
-
}
|
|
986
|
-
}
|
|
987
|
-
async function gitTagExists(cwd, tag) {
|
|
988
|
-
const result = await git(cwd, ['tag', '--list', tag]);
|
|
989
|
-
return result.trim() === tag;
|
|
990
|
-
}
|
|
991
|
-
async function git(cwd, args) {
|
|
992
|
-
const directory = path.resolve(cwd);
|
|
993
|
-
try {
|
|
994
|
-
const { stdout } = await execFileAsync('git', ['-c', `safe.directory=${directory}`, ...args], { cwd: directory });
|
|
995
|
-
return stdout;
|
|
996
|
-
}
|
|
997
|
-
catch (error) {
|
|
998
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
999
|
-
throw new ValidationError(`Git command failed (${args.join(' ')}): ${message}`);
|
|
1000
|
-
}
|
|
1001
|
-
}
|
|
1002
|
-
async function readReleaseNotes(directory, version) {
|
|
1003
|
-
let contents = '';
|
|
1004
|
-
try {
|
|
1005
|
-
contents = await readFile(path.join(directory, 'CHANGELOG.md'), 'utf8');
|
|
1006
|
-
}
|
|
1007
|
-
catch (error) {
|
|
1008
|
-
if ('ENOENT' !== error.code) {
|
|
1009
|
-
throw error;
|
|
1010
|
-
}
|
|
1011
|
-
}
|
|
1012
|
-
const lines = contents.split(/\r?\n/);
|
|
1013
|
-
const headingIndex = lines.findIndex((line) => new RegExp(`^##\\s+${escapeRegExp(version)}(?:\\s|$)`).test(line));
|
|
1014
|
-
if (-1 === headingIndex) {
|
|
1015
|
-
return '';
|
|
1016
|
-
}
|
|
1017
|
-
const followingHeading = lines.findIndex((line, index) => index > headingIndex && /^##\s/.test(line));
|
|
1018
|
-
const end = -1 === followingHeading ? lines.length : followingHeading;
|
|
1019
|
-
return lines
|
|
1020
|
-
.slice(headingIndex + 1, end)
|
|
1021
|
-
.join('\n')
|
|
1022
|
-
.trim();
|
|
1023
|
-
}
|
|
1024
|
-
async function getLatestChangelogVersion(directory, fallback) {
|
|
1025
|
-
try {
|
|
1026
|
-
const contents = await readFile(path.join(directory, 'CHANGELOG.md'), 'utf8');
|
|
1027
|
-
const match = /^##\s+(\d+\.\d+\.\d+)(?:\s|$)/m.exec(contents);
|
|
1028
|
-
return match?.[1] ?? fallback;
|
|
1029
|
-
}
|
|
1030
|
-
catch (error) {
|
|
1031
|
-
if ('ENOENT' === error.code) {
|
|
1032
|
-
return fallback;
|
|
1033
|
-
}
|
|
1034
|
-
throw error;
|
|
1035
|
-
}
|
|
1036
|
-
}
|
|
1037
|
-
function escapeRegExp(value) {
|
|
1038
|
-
return value.replaceAll(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
1039
|
-
}
|
|
1040
|
-
async function rememberFile(journal, file) {
|
|
1041
|
-
if (journal.has(file)) {
|
|
1042
|
-
return;
|
|
1043
|
-
}
|
|
1044
|
-
try {
|
|
1045
|
-
journal.set(file, await readFile(file, 'utf8'));
|
|
1046
|
-
}
|
|
1047
|
-
catch (error) {
|
|
1048
|
-
if ('ENOENT' !== error.code) {
|
|
1049
|
-
throw error;
|
|
1050
|
-
}
|
|
1051
|
-
journal.set(file, undefined);
|
|
1052
|
-
}
|
|
1053
|
-
}
|
|
1054
|
-
async function restoreFileJournal(journal) {
|
|
1055
|
-
const errors = [];
|
|
1056
|
-
for (const [file, contents] of [...journal.entries()].reverse()) {
|
|
1057
|
-
try {
|
|
1058
|
-
if (undefined === contents) {
|
|
1059
|
-
await rm(file, { force: true });
|
|
1060
|
-
}
|
|
1061
|
-
else {
|
|
1062
|
-
await writeFile(file, contents);
|
|
1063
|
-
}
|
|
1064
|
-
}
|
|
1065
|
-
catch (error) {
|
|
1066
|
-
errors.push(error instanceof Error ? error : new Error(String(error)));
|
|
1067
|
-
}
|
|
1068
|
-
}
|
|
1069
|
-
return errors;
|
|
1070
|
-
}
|
|
1071
|
-
async function readJson(file) {
|
|
1072
|
-
try {
|
|
1073
|
-
return JSON.parse(await readFile(file, 'utf8'));
|
|
1074
|
-
}
|
|
1075
|
-
catch (error) {
|
|
1076
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
1077
|
-
throw new ValidationError(`Could not read JSON manifest ${file}: ${message}`);
|
|
1078
|
-
}
|
|
1079
|
-
}
|
|
1080
|
-
function packageType(group) {
|
|
1081
|
-
const kind = group.version.from.kind;
|
|
1082
|
-
if ('wordpress-plugin-header' === kind) {
|
|
1083
|
-
return 'plugin';
|
|
1084
|
-
}
|
|
1085
|
-
if ('wordpress-theme-header' === kind) {
|
|
1086
|
-
return 'theme';
|
|
1087
|
-
}
|
|
1088
|
-
if ('package-json' === kind) {
|
|
1089
|
-
return 'npm';
|
|
1090
|
-
}
|
|
1091
|
-
return 'other';
|
|
1092
|
-
}
|
|
1093
|
-
const VERSION_PATTERN = /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-([0-9A-Za-z.-]+))?$/;
|
|
1094
|
-
function isVersion(version) {
|
|
1095
|
-
return VERSION_PATTERN.test(version);
|
|
1096
|
-
}
|
|
1097
|
-
function isPrerelease(version) {
|
|
1098
|
-
return Boolean(VERSION_PATTERN.exec(version)?.[4]);
|
|
1099
|
-
}
|
|
1100
|
-
/**
|
|
1101
|
-
* Compare two versions with SemVer precedence, where a pre-release like
|
|
1102
|
-
* 1.0.0-rc.1 comes before 1.0.0.
|
|
1103
|
-
*
|
|
1104
|
-
* @param left Version.
|
|
1105
|
-
* @param right Version to compare with.
|
|
1106
|
-
* @return Negative when left is lower, positive when higher, otherwise 0.
|
|
1107
|
-
*/
|
|
1108
|
-
function compareVersions(left, right) {
|
|
1109
|
-
const [leftCore = '', leftPre] = splitPrerelease(left);
|
|
1110
|
-
const [rightCore = '', rightPre] = splitPrerelease(right);
|
|
1111
|
-
const a = leftCore.split('.').map(Number);
|
|
1112
|
-
const b = rightCore.split('.').map(Number);
|
|
1113
|
-
const core = (a[0] ?? 0) - (b[0] ?? 0) ||
|
|
1114
|
-
(a[1] ?? 0) - (b[1] ?? 0) ||
|
|
1115
|
-
(a[2] ?? 0) - (b[2] ?? 0);
|
|
1116
|
-
if (core || leftPre === rightPre) {
|
|
1117
|
-
return core;
|
|
1118
|
-
}
|
|
1119
|
-
if (undefined === leftPre || undefined === rightPre) {
|
|
1120
|
-
return undefined === leftPre ? 1 : -1;
|
|
1121
|
-
}
|
|
1122
|
-
return comparePrerelease(leftPre.split('.'), rightPre.split('.'));
|
|
1123
|
-
}
|
|
1124
|
-
function splitPrerelease(version) {
|
|
1125
|
-
const index = version.indexOf('-');
|
|
1126
|
-
return -1 === index
|
|
1127
|
-
? [version]
|
|
1128
|
-
: [version.slice(0, index), version.slice(index + 1)];
|
|
1129
|
-
}
|
|
1130
|
-
function comparePrerelease(left, right) {
|
|
1131
|
-
const [a, ...leftRest] = left;
|
|
1132
|
-
const [b, ...rightRest] = right;
|
|
1133
|
-
if (undefined === a || undefined === b) {
|
|
1134
|
-
return (undefined === a ? 0 : 1) - (undefined === b ? 0 : 1);
|
|
1135
|
-
}
|
|
1136
|
-
const order = compareIdentifier(a, b);
|
|
1137
|
-
return order || comparePrerelease(leftRest, rightRest);
|
|
1138
|
-
}
|
|
1139
|
-
function compareIdentifier(a, b) {
|
|
1140
|
-
const aNumeric = /^\d+$/.test(a);
|
|
1141
|
-
const bNumeric = /^\d+$/.test(b);
|
|
1142
|
-
if (aNumeric && bNumeric) {
|
|
1143
|
-
return Number(a) - Number(b);
|
|
1144
|
-
}
|
|
1145
|
-
if (aNumeric || bNumeric) {
|
|
1146
|
-
return aNumeric ? -1 : 1;
|
|
1147
|
-
}
|
|
1148
|
-
return Number(a > b) - Number(a < b);
|
|
424
|
+
return new ChangesetRelease(input).getStatus(input);
|
|
1149
425
|
}
|