@wpelevator/wp-release 0.12.0 → 0.14.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (78) hide show
  1. package/README.md +150 -19
  2. package/dist/changeset-config.d.ts +91 -5
  3. package/dist/changeset-config.js +108 -11
  4. package/dist/changeset-dependents.d.ts +19 -8
  5. package/dist/changeset-dependents.js +96 -34
  6. package/dist/changeset-files.d.ts +6 -5
  7. package/dist/changeset-files.js +7 -5
  8. package/dist/changeset-packages.d.ts +55 -8
  9. package/dist/changeset-packages.js +85 -14
  10. package/dist/changeset-prompt.d.ts +119 -0
  11. package/dist/changeset-prompt.js +203 -0
  12. package/dist/changeset-release.d.ts +43 -5
  13. package/dist/changeset-release.js +99 -16
  14. package/dist/changesets.d.ts +6 -2
  15. package/dist/changesets.js +18 -12
  16. package/dist/cli.d.ts +7 -1
  17. package/dist/cli.js +51 -14
  18. package/dist/commands/args.d.ts +0 -21
  19. package/dist/commands/args.js +1 -35
  20. package/dist/commands/changeset-add.d.ts +5 -2
  21. package/dist/commands/changeset-add.js +109 -9
  22. package/dist/commands/changeset-args.d.ts +24 -5
  23. package/dist/commands/changeset-args.js +57 -14
  24. package/dist/commands/changeset-status.d.ts +3 -1
  25. package/dist/commands/changeset-status.js +2 -1
  26. package/dist/commands/changeset-tag.d.ts +1 -1
  27. package/dist/commands/changeset-tag.js +6 -3
  28. package/dist/commands/changeset-version.d.ts +7 -1
  29. package/dist/commands/changeset-version.js +17 -4
  30. package/dist/commands/checksums.js +2 -2
  31. package/dist/commands/command.d.ts +5 -0
  32. package/dist/commands/deploy-git.d.ts +24 -4
  33. package/dist/commands/deploy-git.js +57 -3
  34. package/dist/commands/deploy-svn.d.ts +24 -4
  35. package/dist/commands/deploy-svn.js +57 -2
  36. package/dist/commands/keygen.d.ts +3 -0
  37. package/dist/commands/keygen.js +6 -1
  38. package/dist/commands/package-deploys.d.ts +86 -0
  39. package/dist/commands/package-deploys.js +113 -0
  40. package/dist/commands/sign.d.ts +19 -1
  41. package/dist/commands/sign.js +47 -12
  42. package/dist/commands/verify.d.ts +3 -0
  43. package/dist/commands/verify.js +43 -24
  44. package/dist/commands/zip.d.ts +12 -8
  45. package/dist/commands/zip.js +135 -78
  46. package/dist/ed25519.d.ts +25 -0
  47. package/dist/ed25519.js +44 -0
  48. package/dist/file-selection.d.ts +21 -0
  49. package/dist/file-selection.js +35 -0
  50. package/dist/git-deploy.d.ts +32 -0
  51. package/dist/git-deploy.js +57 -1
  52. package/dist/git.d.ts +17 -0
  53. package/dist/git.js +23 -0
  54. package/dist/index.d.ts +13 -5
  55. package/dist/index.js +7 -3
  56. package/dist/minisign.d.ts +41 -0
  57. package/dist/minisign.js +128 -0
  58. package/dist/package-deploy.d.ts +133 -0
  59. package/dist/package-deploy.js +323 -0
  60. package/dist/package-zip.d.ts +52 -0
  61. package/dist/package-zip.js +78 -0
  62. package/dist/release-repository.d.ts +18 -5
  63. package/dist/release-repository.js +51 -11
  64. package/dist/release-tags.d.ts +3 -1
  65. package/dist/release-tags.js +5 -2
  66. package/dist/signing.d.ts +64 -9
  67. package/dist/signing.js +113 -40
  68. package/dist/ssh-signature.d.ts +43 -0
  69. package/dist/ssh-signature.js +156 -0
  70. package/dist/summary.d.ts +3 -3
  71. package/dist/summary.js +2 -2
  72. package/dist/tag-format.d.ts +23 -14
  73. package/dist/tag-format.js +26 -40
  74. package/dist/version-files.d.ts +8 -8
  75. package/dist/version-files.js +22 -18
  76. package/dist/versions.d.ts +28 -3
  77. package/dist/versions.js +84 -9
  78. package/package.json +2 -2
@@ -0,0 +1,323 @@
1
+ import { realpath } from 'node:fs/promises';
2
+ import path from 'node:path';
3
+ import { loadChangesetConfig, toTargetList } from "./changeset-config.js";
4
+ import { discoverDeployPackages, discoverConfiguredPackages, findPackage, } from "./changeset-packages.js";
5
+ import { ValidationError } from "./errors.js";
6
+ import { parseRepositoryUrl, redactUrls } from "./git.js";
7
+ import { ReleaseRepository } from "./release-repository.js";
8
+ import { DEFAULT_TAG_FORMAT, formatTag, formatTemplate } from "./tag-format.js";
9
+ import { isPrerelease, isVersion } from "./versions.js";
10
+ /**
11
+ * Get the Git deploys of the configured packages that a ref releases: every
12
+ * Git target of the package of a release tag that releases tags, or of every
13
+ * package with a Git target that releases the branch.
14
+ *
15
+ * @param options Repository, ref and package selection.
16
+ * @return Deploys in the configured package and target order.
17
+ * @throws {ValidationError} Without a config, or for an invalid one or an unknown package.
18
+ */
19
+ export async function getGitDeploys(options) {
20
+ const { config, releases, root } = await loadDeployContext(options);
21
+ const deploys = await Promise.all(releases.map(async (release) => {
22
+ const targets = toTargetList(release.package.group.deploy?.git);
23
+ const resolved = await Promise.all(targets
24
+ .filter((target) => isGitTargetReleased(target, release))
25
+ .map((target) => getGitDeploy(target, release, config, root, options)));
26
+ return resolved.filter((deploy) => undefined !== deploy);
27
+ }));
28
+ return deploys.flat();
29
+ }
30
+ /**
31
+ * Get the WordPress.org SVN deploys of the configured package that a release
32
+ * tag releases. A stable release syncs trunk/, the tag and the assets, and a
33
+ * pre-release only creates its tag. Branches aren't deployed to SVN.
34
+ *
35
+ * @param options Repository, ref and package selection.
36
+ * @return Deploys in the configured target order.
37
+ * @throws {ValidationError} Without a config, or for an invalid one or an unknown package.
38
+ */
39
+ export async function getSvnDeploys(options) {
40
+ const { config, releases } = await loadDeployContext(options);
41
+ const deploys = await Promise.all(releases
42
+ .filter((release) => release.version)
43
+ .map(async (release) => {
44
+ const targets = toTargetList(release.package.group.deploy?.svn);
45
+ const resolved = await Promise.all(targets.map((target) => getSvnDeploy(target, release, config, options)));
46
+ return resolved.filter((deploy) => undefined !== deploy);
47
+ }));
48
+ return deploys.flat();
49
+ }
50
+ /**
51
+ * Get the packages with deploy targets and the files that their deploys
52
+ * ship, for example to build a ZIP with the same files.
53
+ *
54
+ * @param options Repository root and config file.
55
+ * @param options.cwd Repository root with the wp-release config.
56
+ * @param options.configFile Config file, instead of the conventional one at the root.
57
+ * @return Packages in the configured order.
58
+ * @throws {ValidationError} Without a config, or for an invalid one.
59
+ */
60
+ export async function getDeployPackages(options) {
61
+ const config = await requireConfig(options);
62
+ const packages = await discoverDeployPackages(new ReleaseRepository({ cwd: options.cwd }), config);
63
+ return packages.map((pkg) => ({
64
+ ...toDeployPackage(pkg),
65
+ files: getFileSelection(pkg, config),
66
+ }));
67
+ }
68
+ /**
69
+ * Get a configured package with the files that its releases ship, from the
70
+ * files config of its package group, which its deploys use too, for example
71
+ * to build its ZIP with the same files. Any package of the config works, with
72
+ * or without a version or deploy targets.
73
+ *
74
+ * @param options Repository root, config file and package.
75
+ * @param options.cwd Repository root with the wp-release config.
76
+ * @param options.configFile Config file, instead of the conventional one at the root.
77
+ * @param options.package Package identifier or path under the configured root.
78
+ * @return The package and its file selection.
79
+ * @throws {ValidationError} Without a config, for an invalid one or an unknown package.
80
+ */
81
+ export async function getPackageFiles(options) {
82
+ const config = await requireConfig(options);
83
+ const pkg = findPackage(await discoverConfiguredPackages(new ReleaseRepository({ cwd: options.cwd }), config), options.package);
84
+ if (!pkg) {
85
+ throw new ValidationError(`Unknown package "${options.package}".`);
86
+ }
87
+ return {
88
+ ...toDeployPackage(pkg),
89
+ files: getFileSelection(pkg, config),
90
+ };
91
+ }
92
+ async function requireConfig(options) {
93
+ const loaded = await loadChangesetConfig({
94
+ cwd: options.cwd,
95
+ ...(options.configFile ? { configFile: options.configFile } : {}),
96
+ ...(options.tagFormat ? { tagFormat: options.tagFormat } : {}),
97
+ });
98
+ if (!loaded) {
99
+ throw new ValidationError('No wp-release config found. Add wp-release.config.js or pass --config <path>.');
100
+ }
101
+ return loaded.config;
102
+ }
103
+ async function loadDeployContext(options) {
104
+ const config = await requireConfig(options);
105
+ const repository = new ReleaseRepository({
106
+ cwd: options.cwd,
107
+ runner: options.git,
108
+ });
109
+ const packages = selectPackages(await discoverDeployPackages(repository, config), options.packages ?? []);
110
+ const root = await repository.run(['rev-parse', '--show-toplevel']);
111
+ const revision = await repository.run(['rev-parse', 'HEAD^{commit}']);
112
+ return {
113
+ config,
114
+ releases: getReleases(packages, config, options.ref).map((release) => ({ ...release, revision: revision.trim() })),
115
+ root: root.trim(),
116
+ };
117
+ }
118
+ function selectPackages(packages, identifiers) {
119
+ const selected = identifiers.map((identifier) => {
120
+ const pkg = findPackage(packages, identifier);
121
+ if (!pkg) {
122
+ throw new ValidationError(`Unknown package "${identifier}", or it has no deploy targets.`);
123
+ }
124
+ return pkg;
125
+ });
126
+ return identifiers.length
127
+ ? packages.filter((pkg) => selected.includes(pkg))
128
+ : packages;
129
+ }
130
+ /**
131
+ * Resolve the releases of a ref: the package of a release tag, or every
132
+ * package for a branch.
133
+ *
134
+ * @param packages Packages with deploy targets.
135
+ * @param config Repository release configuration.
136
+ * @param ref Ref to deploy.
137
+ * @return Releases.
138
+ */
139
+ function getReleases(packages, config, ref) {
140
+ const tagFormat = config.tagFormat ?? DEFAULT_TAG_FORMAT;
141
+ const tag = ref.startsWith('refs/tags/')
142
+ ? ref.slice('refs/tags/'.length)
143
+ : ref;
144
+ const tagged = ref.startsWith('refs/heads/')
145
+ ? []
146
+ : packages.flatMap((pkg) => getTaggedVersion(pkg, tagFormat, tag));
147
+ const branch = ref.replace(/^refs\/heads\//, '');
148
+ return ref.startsWith('refs/tags/') || tagged.length
149
+ ? tagged
150
+ : packages.map((pkg) => ({ package: pkg, branch }));
151
+ }
152
+ function getTaggedVersion(pkg, tagFormat, tag) {
153
+ const prefix = formatTag(tagFormat, pkg, '');
154
+ const version = tag.startsWith(prefix) ? tag.slice(prefix.length) : '';
155
+ return pkg.group.version && isVersion(version)
156
+ ? [{ package: pkg, version }]
157
+ : [];
158
+ }
159
+ function isGitTargetReleased(target, release) {
160
+ const branches = target.branches ?? true;
161
+ const branchReleased = Array.isArray(branches)
162
+ ? branches.includes(release.branch ?? '')
163
+ : branches;
164
+ return release.version ? false !== target.tag : branchReleased;
165
+ }
166
+ async function getGitDeploy(target, release, config, root, options) {
167
+ const pkg = release.package;
168
+ const tag = release.version
169
+ ? formatTag(target.tag || '{version}', pkg, release.version)
170
+ : undefined;
171
+ const ref = tag ?? release.branch ?? '';
172
+ const repository = await resolveTemplate(target.repository, release, ref);
173
+ const message = target.message
174
+ ? await resolveTemplate(target.message, release, ref)
175
+ : '';
176
+ // Git reports the real path of the root, so compare it with the real path
177
+ // of the package, like on macOS where the temporary directory is a link.
178
+ const splitPrefix = false === target.history
179
+ ? ''
180
+ : path
181
+ .relative(root, await realpath(pkg.directory))
182
+ .split(path.sep)
183
+ .join('/');
184
+ const location = repository ? parseRepositoryUrl(repository) : undefined;
185
+ return repository
186
+ ? {
187
+ package: toDeployPackage(pkg),
188
+ release: toRelease(release),
189
+ target: redactUrls(repository),
190
+ targetRef: ref,
191
+ ...(location
192
+ ? { targetHost: location.host, targetPath: location.path }
193
+ : {}),
194
+ label: `${pkg.name} to ${redactUrls(repository)} ${ref}`,
195
+ input: {
196
+ cwd: options.cwd,
197
+ sourceDir: pkg.directory,
198
+ gitUrl: repository,
199
+ ...(release.branch ? { branch: release.branch } : {}),
200
+ ...(tag ? { tag } : {}),
201
+ ...(splitPrefix ? { splitPrefix } : {}),
202
+ ...(message ? { message } : {}),
203
+ ...getFileSelection(pkg, config),
204
+ dryRun: Boolean(options.dryRun),
205
+ },
206
+ }
207
+ : undefined;
208
+ }
209
+ async function getSvnDeploy(target, release, config, options) {
210
+ const pkg = release.package;
211
+ const version = release.version ?? '';
212
+ const stable = !isPrerelease(version);
213
+ // The WordPress.org slug is the name of the package directory by default.
214
+ const slug = target.slug
215
+ ? await resolveTemplate(target.slug, release, version)
216
+ : path.basename(pkg.directory);
217
+ const message = target.message
218
+ ? await resolveTemplate(target.message, release, version)
219
+ : '';
220
+ return slug
221
+ ? {
222
+ package: toDeployPackage(pkg),
223
+ release: toRelease(release),
224
+ target: `WordPress.org ${slug}`,
225
+ targetRef: version,
226
+ label: `${pkg.name} to WordPress.org ${slug} ${version}`,
227
+ input: {
228
+ cwd: options.cwd,
229
+ sourceDir: pkg.directory,
230
+ slug,
231
+ version,
232
+ svnTag: version,
233
+ trunk: stable,
234
+ ...(stable && target.assetsDir
235
+ ? {
236
+ assetsDir: path.join(pkg.directory, target.assetsDir),
237
+ }
238
+ : {}),
239
+ ...(target.readme ? { readme: target.readme } : {}),
240
+ ...(target.url ? { svnUrl: target.url } : {}),
241
+ ...(message ? { message } : {}),
242
+ ...getFileSelection(pkg, config),
243
+ dryRun: Boolean(options.dryRun),
244
+ },
245
+ }
246
+ : undefined;
247
+ }
248
+ /**
249
+ * Resolve a template with the package and release placeholders, or call the
250
+ * callback with the package and its release.
251
+ *
252
+ * @param template Template or callback.
253
+ * @param release Release of the package.
254
+ * @param ref Tag or branch that the deploy writes in the target.
255
+ * @return Resolved value, empty for a skipped target or the default message.
256
+ */
257
+ async function resolveTemplate(template, release, ref) {
258
+ const value = 'function' === typeof template
259
+ ? await template({
260
+ package: toDeployPackage(release.package),
261
+ ...toRelease(release),
262
+ ref,
263
+ revision: release.revision,
264
+ })
265
+ : formatTemplate(template, release.package, release.version ?? '')
266
+ .replaceAll('{branch}', release.branch ?? '')
267
+ .replaceAll('{ref}', ref)
268
+ .replaceAll('{revision}', release.revision);
269
+ return value?.trim() ?? '';
270
+ }
271
+ function getFileSelection(pkg, config) {
272
+ const shared = config.files ?? {};
273
+ const files = pkg.group.files ?? {};
274
+ const setting = undefined === files.distignore ? shared.distignore : files.distignore;
275
+ const distignore = 'string' === typeof setting
276
+ ? path.join(pkg.directory, setting)
277
+ : setting;
278
+ const exclude = [...(shared.exclude ?? []), ...(files.exclude ?? [])];
279
+ return {
280
+ ...(undefined === distignore ? {} : { distignore }),
281
+ ...(exclude.length ? { exclude } : {}),
282
+ };
283
+ }
284
+ function toDeployPackage(pkg) {
285
+ return {
286
+ name: pkg.name,
287
+ directory: pkg.directory,
288
+ path: pkg.path,
289
+ type: pkg.type,
290
+ };
291
+ }
292
+ function toRelease(release) {
293
+ return {
294
+ ...(release.version ? { version: release.version } : {}),
295
+ ...(release.branch ? { branch: release.branch } : {}),
296
+ };
297
+ }
298
+ /**
299
+ * Group deploys by package, for a preview of what a ref deploys or a CI
300
+ * matrix with a job for each package.
301
+ *
302
+ * @param deploys Deploys from getGitDeploys() or getSvnDeploys().
303
+ * @return One item for each package, in the deploy order.
304
+ */
305
+ export function getDeployPlan(deploys) {
306
+ const names = [
307
+ ...new Set(deploys.map((item) => item.package.name)),
308
+ ];
309
+ return names.map((name) => {
310
+ const items = deploys.filter((item) => item.package.name === name);
311
+ return {
312
+ package: name,
313
+ ...items[0]?.release,
314
+ targets: items.map((item) => ({
315
+ target: item.target,
316
+ ref: item.targetRef,
317
+ ...(item.targetHost
318
+ ? { host: item.targetHost, path: item.targetPath }
319
+ : {}),
320
+ })),
321
+ };
322
+ });
323
+ }
@@ -0,0 +1,52 @@
1
+ import type { PackageType } from './checksums.ts';
2
+ import type { FileSelection } from './file-selection.ts';
3
+ export interface ZipPackageInput {
4
+ /** Directory to archive. */
5
+ sourceDir: string;
6
+ /** File selection from resolveFileSelection(). */
7
+ selection: FileSelection;
8
+ /** Archive path. */
9
+ zipPath: string;
10
+ /** Folder inside the archive, or null for the root. */
11
+ rootFolder: string | null;
12
+ /** Plugin or theme slug, for the checksums manifest. */
13
+ slug: string;
14
+ /** Version for the checksums manifest. */
15
+ version?: string | null;
16
+ /** Package type for the checksums manifest. */
17
+ type?: PackageType;
18
+ /** Also write the checksums manifest next to the archive. */
19
+ checksums?: boolean;
20
+ /** Overwrite existing output files. */
21
+ force?: boolean;
22
+ }
23
+ /** Result of zipPackage(). */
24
+ export interface ZipPackageResult {
25
+ /** Absolute path of the ZIP. */
26
+ zipPath: string;
27
+ /** Absolute path of the checksums manifest, set with checksums. */
28
+ checksumsPath?: string;
29
+ /** Number of archived files. */
30
+ files: number;
31
+ }
32
+ /**
33
+ * Get the checksums manifest path next to an archive, with .checksums.json
34
+ * instead of .zip.
35
+ *
36
+ * @param zipPath Archive path.
37
+ * @return Manifest path.
38
+ */
39
+ export declare function getChecksumsPath(zipPath: string): string;
40
+ /**
41
+ * Write a ZIP of the selected files of a plugin, theme or package, and
42
+ * optionally the checksums manifest of the same files next to it. The zip
43
+ * command, with a directory or a configured package release, and the
44
+ * snapshot builds of a monorepo all use it, so that every ZIP of a package
45
+ * selects its files the same way. Output files inside the source directory,
46
+ * and their signatures, are never archived.
47
+ *
48
+ * @param input Files, archive path and manifest options.
49
+ * @return Paths of the written files and the number of archived files.
50
+ * @throws {ValidationError} Without selected files, or when an output file exists without force.
51
+ */
52
+ export declare function zipPackage(input: ZipPackageInput): Promise<ZipPackageResult>;
@@ -0,0 +1,78 @@
1
+ import { existsSync } from 'node:fs';
2
+ import { CHECKSUMS_EXTENSION, createChecksums, writeChecksums, } from "./checksums.js";
3
+ import { ValidationError } from "./errors.js";
4
+ import { ignoreOwnFile, ignoreOwnSignature, loadFileRules, selectFiles, } from "./file-selection.js";
5
+ import { createZip } from "./zip.js";
6
+ const ZIP_EXTENSION = '.zip';
7
+ /**
8
+ * Get the checksums manifest path next to an archive, with .checksums.json
9
+ * instead of .zip.
10
+ *
11
+ * @param zipPath Archive path.
12
+ * @return Manifest path.
13
+ */
14
+ export function getChecksumsPath(zipPath) {
15
+ const name = zipPath.endsWith(ZIP_EXTENSION)
16
+ ? zipPath.slice(0, -ZIP_EXTENSION.length)
17
+ : zipPath;
18
+ return name + CHECKSUMS_EXTENSION;
19
+ }
20
+ /**
21
+ * Fail before writing anything when an output file exists.
22
+ *
23
+ * @param paths Output paths.
24
+ * @param force Whether existing files are overwritten.
25
+ * @throws {ValidationError} If a file exists and force isn't set.
26
+ */
27
+ function checkOverwrite(paths, force) {
28
+ const existing = paths.filter((path) => !force && existsSync(path));
29
+ if (existing.length) {
30
+ throw new ValidationError(`${existing.join(' and ')} already exists, pass --force to overwrite it.`);
31
+ }
32
+ }
33
+ /**
34
+ * Write a ZIP of the selected files of a plugin, theme or package, and
35
+ * optionally the checksums manifest of the same files next to it. The zip
36
+ * command, with a directory or a configured package release, and the
37
+ * snapshot builds of a monorepo all use it, so that every ZIP of a package
38
+ * selects its files the same way. Output files inside the source directory,
39
+ * and their signatures, are never archived.
40
+ *
41
+ * @param input Files, archive path and manifest options.
42
+ * @return Paths of the written files and the number of archived files.
43
+ * @throws {ValidationError} Without selected files, or when an output file exists without force.
44
+ */
45
+ export async function zipPackage(input) {
46
+ const { sourceDir, zipPath, force = false } = input;
47
+ const checksumsPath = input.checksums ? getChecksumsPath(zipPath) : null;
48
+ const outputs = [zipPath, checksumsPath].filter((path) => null !== path);
49
+ const rules = await loadFileRules(input.selection);
50
+ outputs.forEach((path) => ignoreOwnSignature(ignoreOwnFile(rules, sourceDir, path), sourceDir, path));
51
+ const files = await selectFiles(sourceDir, rules);
52
+ if (!files.length) {
53
+ throw new ValidationError(`No files to archive in ${sourceDir}.`);
54
+ }
55
+ checkOverwrite(outputs, force);
56
+ await createZip({
57
+ sourceDir,
58
+ files,
59
+ zipPath,
60
+ rootFolder: input.rootFolder,
61
+ force,
62
+ });
63
+ if (checksumsPath) {
64
+ const manifest = await createChecksums({
65
+ sourceDir,
66
+ rules,
67
+ slug: input.slug,
68
+ version: input.version ?? null,
69
+ type: input.type,
70
+ });
71
+ await writeChecksums(checksumsPath, manifest, force);
72
+ }
73
+ return {
74
+ zipPath,
75
+ ...(checksumsPath ? { checksumsPath } : {}),
76
+ files: files.length,
77
+ };
78
+ }
@@ -11,6 +11,8 @@ export interface ChangedFilesOptions {
11
11
  directory?: string;
12
12
  /** Value of --diff-filter, like ACMR for files that still exist. */
13
13
  diffFilter?: string;
14
+ /** Whether to include the uncommitted and untracked files of the working tree. */
15
+ uncommitted?: boolean;
14
16
  }
15
17
  /**
16
18
  * The git queries and writes of a changeset release: tags, the commits of a
@@ -64,11 +66,12 @@ export declare class ReleaseRepository {
64
66
  */
65
67
  createTag(tag: string): Promise<void>;
66
68
  /**
67
- * Push a tag to the origin remote.
69
+ * Push a tag to a remote.
68
70
  *
69
- * @param tag Tag name.
71
+ * @param tag Tag name.
72
+ * @param remote Remote name.
70
73
  */
71
- pushTag(tag: string): Promise<void>;
74
+ pushTag(tag: string, remote: string): Promise<void>;
72
75
  /**
73
76
  * Find the commit that added a file.
74
77
  *
@@ -85,13 +88,23 @@ export declare class ReleaseRepository {
85
88
  */
86
89
  getDirectoryCommits(revision: string, directory: string): Promise<ReleaseCommit[]>;
87
90
  /**
88
- * List the files changed between the merge base of a ref and HEAD.
91
+ * List the files changed between the merge base of a ref and HEAD, or the
92
+ * working tree with the uncommitted option, which adds untracked files.
89
93
  *
90
94
  * @param since Base ref, like origin/main.
91
- * @param options Directory and diff filter.
95
+ * @param options Directory, diff filter and uncommitted files.
92
96
  * @return Changed files relative to the repository root.
93
97
  */
94
98
  getChangedFiles(since: string, options?: ChangedFilesOptions): Promise<string[]>;
99
+ /**
100
+ * Read a file as of the merge base of a ref and HEAD, the side that
101
+ * getChangedFiles() diffs against.
102
+ *
103
+ * @param since Base ref, like origin/main.
104
+ * @param file Absolute or relative file path.
105
+ * @return File contents, or undefined when the file doesn't exist there.
106
+ */
107
+ readFileAtMergeBase(since: string, file: string): Promise<string | undefined>;
95
108
  /**
96
109
  * Run git log and parse the commits.
97
110
  *
@@ -78,12 +78,13 @@ export class ReleaseRepository {
78
78
  await this.run(['tag', tag]);
79
79
  }
80
80
  /**
81
- * Push a tag to the origin remote.
81
+ * Push a tag to a remote.
82
82
  *
83
- * @param tag Tag name.
83
+ * @param tag Tag name.
84
+ * @param remote Remote name.
84
85
  */
85
- async pushTag(tag) {
86
- await this.run(['push', 'origin', tag]);
86
+ async pushTag(tag, remote) {
87
+ await this.run(['push', remote, tag]);
87
88
  }
88
89
  /**
89
90
  * Find the commit that added a file.
@@ -115,25 +116,64 @@ export class ReleaseRepository {
115
116
  ]);
116
117
  }
117
118
  /**
118
- * List the files changed between the merge base of a ref and HEAD.
119
+ * List the files changed between the merge base of a ref and HEAD, or the
120
+ * working tree with the uncommitted option, which adds untracked files.
119
121
  *
120
122
  * @param since Base ref, like origin/main.
121
- * @param options Directory and diff filter.
123
+ * @param options Directory, diff filter and uncommitted files.
122
124
  * @return Changed files relative to the repository root.
123
125
  */
124
126
  async getChangedFiles(since, options = {}) {
127
+ const pathspec = options.directory
128
+ ? ['--', this.relativePath(options.directory)]
129
+ : [];
130
+ const range = options.uncommitted
131
+ ? (await this.run(['merge-base', since, 'HEAD'])).trim()
132
+ : `${since}...HEAD`;
125
133
  const output = await this.run([
126
134
  'diff',
127
135
  '--name-only',
128
136
  ...(options.diffFilter
129
137
  ? [`--diff-filter=${options.diffFilter}`]
130
138
  : []),
131
- `${since}...HEAD`,
132
- ...(options.directory
133
- ? ['--', this.relativePath(options.directory)]
134
- : []),
139
+ range,
140
+ ...pathspec,
135
141
  ]);
136
- return output.trim().split(/\r?\n/).filter(Boolean);
142
+ const untracked = options.uncommitted
143
+ ? await this.run([
144
+ 'ls-files',
145
+ '--others',
146
+ '--exclude-standard',
147
+ '--full-name',
148
+ ...pathspec,
149
+ ])
150
+ : '';
151
+ const files = `${output}\n${untracked}`
152
+ .split(/\r?\n/)
153
+ .filter(Boolean);
154
+ return [...new Set(files)];
155
+ }
156
+ /**
157
+ * Read a file as of the merge base of a ref and HEAD, the side that
158
+ * getChangedFiles() diffs against.
159
+ *
160
+ * @param since Base ref, like origin/main.
161
+ * @param file Absolute or relative file path.
162
+ * @return File contents, or undefined when the file doesn't exist there.
163
+ */
164
+ async readFileAtMergeBase(since, file) {
165
+ const base = (await this.run(['merge-base', since, 'HEAD'])).trim();
166
+ const relative = this.relativePath(file);
167
+ const listed = await this.run([
168
+ 'ls-tree',
169
+ '--name-only',
170
+ base,
171
+ '--',
172
+ relative,
173
+ ]);
174
+ return '' === listed.trim()
175
+ ? undefined
176
+ : this.run(['show', `${base}:${relative}`]);
137
177
  }
138
178
  /**
139
179
  * Run git log and parse the commits.
@@ -33,7 +33,9 @@ export declare function selectTagPackages(packages: ConfiguredPackage[], names:
33
33
  */
34
34
  export declare function getTagVersion(pkg: ConfiguredPackage, expected?: string): Promise<string>;
35
35
  /**
36
- * Find the tag of the newest release before a version.
36
+ * Find the tag of the newest release before a version. A stable release
37
+ * skips the pre-releases before it, so that it compares with the previous
38
+ * stable release.
37
39
  *
38
40
  * @param repository Repository with the tags.
39
41
  * @param prefix Tag prefix of the package, like `plugins/alpha/`.
@@ -1,7 +1,7 @@
1
1
  import { readChangelogSection, getLatestChangelogVersion, } from "./changelog.js";
2
2
  import { assertKnownPackages, toChangesetPackage, } from "./changeset-packages.js";
3
3
  import { ValidationError } from "./errors.js";
4
- import { compareVersions, isVersion } from "./versions.js";
4
+ import { compareVersions, isPrerelease, isVersion } from "./versions.js";
5
5
  /**
6
6
  * Pick the packages to tag.
7
7
  *
@@ -37,7 +37,9 @@ export async function getTagVersion(pkg, expected) {
37
37
  (await getLatestChangelogVersion(pkg.directory, pkg.currentVersion)));
38
38
  }
39
39
  /**
40
- * Find the tag of the newest release before a version.
40
+ * Find the tag of the newest release before a version. A stable release
41
+ * skips the pre-releases before it, so that it compares with the previous
42
+ * stable release.
41
43
  *
42
44
  * @param repository Repository with the tags.
43
45
  * @param prefix Tag prefix of the package, like `plugins/alpha/`.
@@ -50,6 +52,7 @@ export async function getPreviousTag(repository, prefix, version) {
50
52
  .filter((tag) => tag.startsWith(prefix))
51
53
  .map((tag) => ({ tag, version: tag.slice(prefix.length) }))
52
54
  .filter((item) => isVersion(item.version) &&
55
+ (isPrerelease(version) || !isPrerelease(item.version)) &&
53
56
  compareVersions(item.version, version) < 0)
54
57
  .sort((a, b) => compareVersions(b.version, a.version))[0]?.tag;
55
58
  }