@wpelevator/wp-release 0.6.1 → 0.8.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 +81 -7
- package/dist/changeset-config.d.ts +31 -0
- package/dist/changeset-config.js +8 -0
- package/dist/changeset-release.d.ts +25 -5
- package/dist/changeset-release.js +335 -21
- package/dist/changesets.d.ts +19 -0
- package/dist/changesets.js +21 -0
- package/dist/cli.js +2 -0
- package/dist/commands/changeset-args.d.ts +14 -0
- package/dist/commands/changeset-args.js +19 -0
- package/dist/commands/changeset-tag.d.ts +7 -0
- package/dist/commands/changeset-tag.js +22 -5
- package/dist/commands/changeset-version.d.ts +7 -0
- package/dist/commands/changeset-version.js +8 -2
- package/dist/commands/deploy-git.d.ts +45 -0
- package/dist/commands/deploy-git.js +81 -0
- package/dist/git-deploy.d.ts +218 -0
- package/dist/git-deploy.js +420 -0
- package/dist/git.d.ts +49 -0
- package/dist/git.js +65 -0
- package/dist/github.d.ts +24 -0
- package/dist/github.js +30 -0
- package/dist/index.d.ts +12 -5
- package/dist/index.js +6 -2
- package/dist/summary.d.ts +51 -0
- package/dist/summary.js +78 -0
- package/package.json +1 -1
|
@@ -0,0 +1,420 @@
|
|
|
1
|
+
import { mkdtemp, rm, stat } from 'node:fs/promises';
|
|
2
|
+
import { tmpdir } from 'node:os';
|
|
3
|
+
import { join, resolve } from 'node:path';
|
|
4
|
+
import { ValidationError } from "./errors.js";
|
|
5
|
+
import { loadFileRules, resolveFileSelection, selectFiles, } from "./file-selection.js";
|
|
6
|
+
import { createGitRunner, redactUrls } from "./git.js";
|
|
7
|
+
import { Logger } from "./logger.js";
|
|
8
|
+
import { getGitDeploySummary } from "./summary.js";
|
|
9
|
+
/**
|
|
10
|
+
* Check a branch or tag name for the common mistakes before it's used in a
|
|
11
|
+
* refspec. It intentionally covers only the main rules of git
|
|
12
|
+
* check-ref-format, since git rejects any other invalid name when it pushes.
|
|
13
|
+
*
|
|
14
|
+
* @param name Branch or tag name.
|
|
15
|
+
* @return Whether the name can be used.
|
|
16
|
+
*/
|
|
17
|
+
export function isValidRefName(name) {
|
|
18
|
+
return (/^[^\s~^:?*[\\]+$/.test(name) &&
|
|
19
|
+
!/(^[-/.]|\/\.|\.\.|\/\/|@\{|\.lock$|[/.]$)/.test(name));
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Apply the defaults to the Git deploy options and validate them.
|
|
23
|
+
*
|
|
24
|
+
* @param input Git deploy input.
|
|
25
|
+
* @return Git deploy options.
|
|
26
|
+
* @throws {ValidationError} For a missing URL or an invalid branch or tag.
|
|
27
|
+
*/
|
|
28
|
+
export function resolveGitDeployOptions(input = {}) {
|
|
29
|
+
const cwd = resolve(input.cwd ?? process.cwd());
|
|
30
|
+
const sourceDir = resolve(cwd, input.sourceDir || '.');
|
|
31
|
+
const names = [
|
|
32
|
+
['branch', input.branch],
|
|
33
|
+
['tag', input.tag],
|
|
34
|
+
].filter(([, name]) => name && !isValidRefName(name));
|
|
35
|
+
if (!input.gitUrl) {
|
|
36
|
+
throw new ValidationError('Pass the URL of the repository to deploy to.');
|
|
37
|
+
}
|
|
38
|
+
if (names.length) {
|
|
39
|
+
throw new ValidationError(names
|
|
40
|
+
.map(([kind, name]) => `Invalid ${kind} "${name}".`)
|
|
41
|
+
.join('\n'));
|
|
42
|
+
}
|
|
43
|
+
return {
|
|
44
|
+
cwd,
|
|
45
|
+
sourceDir,
|
|
46
|
+
gitUrl: input.gitUrl,
|
|
47
|
+
branch: input.branch || null,
|
|
48
|
+
tag: input.tag || null,
|
|
49
|
+
splitPrefix: input.splitPrefix || null,
|
|
50
|
+
sourceRef: input.sourceRef || 'HEAD',
|
|
51
|
+
message: input.message || null,
|
|
52
|
+
dryRun: Boolean(input.dryRun),
|
|
53
|
+
...resolveFileSelection({
|
|
54
|
+
cwd,
|
|
55
|
+
sourceDir,
|
|
56
|
+
distignore: input.distignore,
|
|
57
|
+
exclude: input.exclude,
|
|
58
|
+
}),
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Deploy the files of a plugin, theme or package to a Git repository, as a
|
|
63
|
+
* release commit with exactly the selected files. Its parents are the
|
|
64
|
+
* previous release in the repository and the source commit, or with
|
|
65
|
+
* splitPrefix the history of that directory from git subtree split, so the
|
|
66
|
+
* repository keeps the source history and `git log --first-parent` lists the
|
|
67
|
+
* releases. Pushes are never forced: a branch only moves forward, a release
|
|
68
|
+
* without changes isn't pushed again, and an existing tag is never replaced.
|
|
69
|
+
*
|
|
70
|
+
* Each step is a separate method so that wrappers can run and report them
|
|
71
|
+
* one by one; run() does all of them.
|
|
72
|
+
*/
|
|
73
|
+
export class GitDeploy {
|
|
74
|
+
options;
|
|
75
|
+
logger;
|
|
76
|
+
runner;
|
|
77
|
+
repoRoot = null;
|
|
78
|
+
/**
|
|
79
|
+
* @param options Options from resolveGitDeployOptions().
|
|
80
|
+
* @param logger Logger for the progress.
|
|
81
|
+
* @param runner Git command runner, replaceable in tests.
|
|
82
|
+
*/
|
|
83
|
+
constructor(options, logger = new Logger(), runner = createGitRunner()) {
|
|
84
|
+
this.options = options;
|
|
85
|
+
this.logger = logger;
|
|
86
|
+
this.runner = runner;
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Whether the options ask for any push.
|
|
90
|
+
*
|
|
91
|
+
* @return True with a branch or a tag.
|
|
92
|
+
*/
|
|
93
|
+
hasWrites() {
|
|
94
|
+
return Boolean(this.options.branch || this.options.tag);
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Create the release commit and push it.
|
|
98
|
+
*
|
|
99
|
+
* @return Release, previous release and the pushed refs.
|
|
100
|
+
*/
|
|
101
|
+
async run() {
|
|
102
|
+
const result = {
|
|
103
|
+
revision: null,
|
|
104
|
+
previous: null,
|
|
105
|
+
created: false,
|
|
106
|
+
outdated: false,
|
|
107
|
+
planned: [],
|
|
108
|
+
pushed: [],
|
|
109
|
+
files: 0,
|
|
110
|
+
};
|
|
111
|
+
if (this.hasWrites()) {
|
|
112
|
+
this.logger.info(`Deploying ${this.options.sourceDir} to ${redactUrls(this.options.gitUrl)}`);
|
|
113
|
+
const { tree, files } = await this.createTree();
|
|
114
|
+
const previous = await this.fetchPrevious();
|
|
115
|
+
const tagged = this.options.tag
|
|
116
|
+
? await this.checkTag(tree)
|
|
117
|
+
: null;
|
|
118
|
+
const { revision, created, outdated } = tagged
|
|
119
|
+
? { revision: tagged, created: false, outdated: false }
|
|
120
|
+
: await this.createRelease(tree, previous);
|
|
121
|
+
const planned = this.getRefsToPush(revision, previous, tagged, outdated);
|
|
122
|
+
const pushed = await this.push(revision, planned);
|
|
123
|
+
const deployed = {
|
|
124
|
+
revision,
|
|
125
|
+
previous,
|
|
126
|
+
created,
|
|
127
|
+
outdated,
|
|
128
|
+
planned,
|
|
129
|
+
pushed,
|
|
130
|
+
files,
|
|
131
|
+
};
|
|
132
|
+
this.logger.summary((context) => getGitDeploySummary(context, {
|
|
133
|
+
...deployed,
|
|
134
|
+
gitUrl: this.options.gitUrl,
|
|
135
|
+
dryRun: this.options.dryRun,
|
|
136
|
+
}));
|
|
137
|
+
return deployed;
|
|
138
|
+
}
|
|
139
|
+
this.logger.notice('Nothing to deploy. Pass a branch or a tag.');
|
|
140
|
+
return result;
|
|
141
|
+
}
|
|
142
|
+
/**
|
|
143
|
+
* Write the selected files to the object database of the source
|
|
144
|
+
* repository and create a tree of them, with a temporary index so that
|
|
145
|
+
* the working tree and the index of the repository are left alone.
|
|
146
|
+
*
|
|
147
|
+
* @return Tree hash and the number of files.
|
|
148
|
+
* @throws {ValidationError} When no files are selected, or a file name has a line break.
|
|
149
|
+
*/
|
|
150
|
+
async createTree() {
|
|
151
|
+
const { sourceDir } = this.options;
|
|
152
|
+
const rules = await loadFileRules(this.options);
|
|
153
|
+
const files = await selectFiles(sourceDir, rules);
|
|
154
|
+
if (!files.length) {
|
|
155
|
+
throw new ValidationError(`No files to deploy in ${sourceDir}.`);
|
|
156
|
+
}
|
|
157
|
+
// The git plumbing below reads one path per line.
|
|
158
|
+
const unsupported = files.filter((file) => /[\n\r]|^"/.test(file));
|
|
159
|
+
if (unsupported.length) {
|
|
160
|
+
throw new ValidationError(`File names with line breaks or a leading quote can't be deployed: ${unsupported
|
|
161
|
+
.map((file) => JSON.stringify(file))
|
|
162
|
+
.join(', ')}.`);
|
|
163
|
+
}
|
|
164
|
+
const hashes = await this.git({
|
|
165
|
+
args: ['hash-object', '-w', '--no-filters', '--stdin-paths'],
|
|
166
|
+
input: files
|
|
167
|
+
.map((file) => join(sourceDir, file))
|
|
168
|
+
.join('\n'),
|
|
169
|
+
});
|
|
170
|
+
const modes = await Promise.all(files.map(async (file) =>
|
|
171
|
+
// eslint-disable-next-line no-bitwise
|
|
172
|
+
(await stat(join(sourceDir, file))).mode & 0o111
|
|
173
|
+
? '100755'
|
|
174
|
+
: '100644'));
|
|
175
|
+
const shas = hashes.split('\n');
|
|
176
|
+
const indexDir = await mkdtemp(join(tmpdir(), 'wp-release-git-'));
|
|
177
|
+
const env = { GIT_INDEX_FILE: join(indexDir, 'index') };
|
|
178
|
+
try {
|
|
179
|
+
await this.git({
|
|
180
|
+
args: ['update-index', '--add', '--index-info'],
|
|
181
|
+
input: files
|
|
182
|
+
.map((file, index) => `${modes[index]} ${shas[index]}\t${file}`)
|
|
183
|
+
.join('\n'),
|
|
184
|
+
env,
|
|
185
|
+
});
|
|
186
|
+
const tree = await this.git({ args: ['write-tree'], env });
|
|
187
|
+
this.logger.debug(`Created tree ${tree} of ${files.length} files.`);
|
|
188
|
+
return { tree, files: files.length };
|
|
189
|
+
}
|
|
190
|
+
finally {
|
|
191
|
+
await rm(indexDir, { recursive: true, force: true });
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
/**
|
|
195
|
+
* Fetch the previous release: the branch, or for a tag-only deploy the
|
|
196
|
+
* default branch of the repository.
|
|
197
|
+
*
|
|
198
|
+
* @return Commit hash, or null when the repository has no such branch yet.
|
|
199
|
+
*/
|
|
200
|
+
async fetchPrevious() {
|
|
201
|
+
const { gitUrl, branch } = this.options;
|
|
202
|
+
const ref = branch ? `refs/heads/${branch}` : 'HEAD';
|
|
203
|
+
const remote = await this.git({ args: ['ls-remote', gitUrl, ref] });
|
|
204
|
+
const found = remote
|
|
205
|
+
.split('\n')
|
|
206
|
+
.some((line) => line.endsWith(`\t${ref}`));
|
|
207
|
+
return found ? this.fetchCommit(ref) : null;
|
|
208
|
+
}
|
|
209
|
+
/**
|
|
210
|
+
* Fetch a ref of the repository.
|
|
211
|
+
*
|
|
212
|
+
* @param ref Branch or tag ref, or HEAD.
|
|
213
|
+
* @return Commit hash, with a tag peeled to its commit.
|
|
214
|
+
*/
|
|
215
|
+
async fetchCommit(ref) {
|
|
216
|
+
await this.git({
|
|
217
|
+
args: ['fetch', '--no-tags', '--quiet', this.options.gitUrl, ref],
|
|
218
|
+
});
|
|
219
|
+
return this.git({ args: ['rev-parse', 'FETCH_HEAD^{commit}'] });
|
|
220
|
+
}
|
|
221
|
+
/**
|
|
222
|
+
* Get the release commit. When the files are the same as in the previous
|
|
223
|
+
* release, that release is reused. When the previous release on the
|
|
224
|
+
* branch already has newer source history, like when an older build runs
|
|
225
|
+
* again, the branch isn't moved: only a tag gets a new release commit.
|
|
226
|
+
*
|
|
227
|
+
* @param tree Tree of the released files.
|
|
228
|
+
* @param previous Previous release, or null for the first one.
|
|
229
|
+
* @return Release commit, whether it was created and whether the source is outdated.
|
|
230
|
+
*/
|
|
231
|
+
async createRelease(tree, previous) {
|
|
232
|
+
const previousTree = previous
|
|
233
|
+
? await this.git({
|
|
234
|
+
args: ['rev-parse', `${previous}^{tree}`],
|
|
235
|
+
})
|
|
236
|
+
: null;
|
|
237
|
+
if (previous && tree === previousTree) {
|
|
238
|
+
this.logger.notice('The files are the same as in the previous release.');
|
|
239
|
+
return { revision: previous, created: false, outdated: false };
|
|
240
|
+
}
|
|
241
|
+
const source = await this.getSourceHistory();
|
|
242
|
+
const outdated = this.options.branch && previous
|
|
243
|
+
? await this.isOutdated(source, previous)
|
|
244
|
+
: false;
|
|
245
|
+
if (outdated) {
|
|
246
|
+
this.logger.notice(`The release on ${this.options.branch} already has newer source history than ${source}, so the branch isn't moved.`);
|
|
247
|
+
}
|
|
248
|
+
if (outdated && previous && !this.options.tag) {
|
|
249
|
+
return { revision: previous, created: false, outdated };
|
|
250
|
+
}
|
|
251
|
+
const parents = [previous, source].filter((parent) => Boolean(parent));
|
|
252
|
+
const revision = await this.git({
|
|
253
|
+
args: [
|
|
254
|
+
'commit-tree',
|
|
255
|
+
tree,
|
|
256
|
+
...parents.flatMap((parent) => ['-p', parent]),
|
|
257
|
+
'-m',
|
|
258
|
+
this.options.message ?? (await this.getDefaultMessage()),
|
|
259
|
+
],
|
|
260
|
+
});
|
|
261
|
+
this.logger.notice(`Created release ${revision}.`);
|
|
262
|
+
return { revision, created: true, outdated };
|
|
263
|
+
}
|
|
264
|
+
/**
|
|
265
|
+
* Check whether a release was made from newer source history: the source
|
|
266
|
+
* history of the release, its last parent, already has the source as an
|
|
267
|
+
* older commit. The same source, a newer one, including a revert, and
|
|
268
|
+
* unrelated history aren't outdated. With splitPrefix, git subtree split
|
|
269
|
+
* creates the same commits for the same history, so the split of an older
|
|
270
|
+
* build is part of the split of a newer one.
|
|
271
|
+
*
|
|
272
|
+
* @param source Source history from getSourceHistory().
|
|
273
|
+
* @param previous Previous release.
|
|
274
|
+
* @return Whether the source is older than the source of the release.
|
|
275
|
+
*/
|
|
276
|
+
async isOutdated(source, previous) {
|
|
277
|
+
const [, ...parents] = (await this.git({
|
|
278
|
+
args: ['rev-list', '--parents', '--max-count=1', previous],
|
|
279
|
+
})).split(' ');
|
|
280
|
+
const released = parents.at(-1);
|
|
281
|
+
// Commits of the source history that the released source doesn't have.
|
|
282
|
+
const unreleased = released && released !== source
|
|
283
|
+
? await this.git({
|
|
284
|
+
args: [
|
|
285
|
+
'rev-list',
|
|
286
|
+
'--max-count=1',
|
|
287
|
+
source,
|
|
288
|
+
'--not',
|
|
289
|
+
released,
|
|
290
|
+
],
|
|
291
|
+
})
|
|
292
|
+
: source;
|
|
293
|
+
return '' === unreleased;
|
|
294
|
+
}
|
|
295
|
+
/**
|
|
296
|
+
* Get the source history parent of a release: the source commit, or the
|
|
297
|
+
* history of splitPrefix from git subtree split.
|
|
298
|
+
*
|
|
299
|
+
* @return Commit hash.
|
|
300
|
+
*/
|
|
301
|
+
async getSourceHistory() {
|
|
302
|
+
const { splitPrefix, sourceRef } = this.options;
|
|
303
|
+
return this.git({
|
|
304
|
+
args: splitPrefix
|
|
305
|
+
? ['subtree', 'split', `--prefix=${splitPrefix}`, sourceRef]
|
|
306
|
+
: ['rev-parse', `${sourceRef}^{commit}`],
|
|
307
|
+
});
|
|
308
|
+
}
|
|
309
|
+
/**
|
|
310
|
+
* Check the tag in the repository before anything is pushed. Tags are
|
|
311
|
+
* never replaced, so a tag with other files is an error. A tag with the
|
|
312
|
+
* same files is the release, which makes running a tag release again a
|
|
313
|
+
* no-op, also when the release commit was created on top of an older
|
|
314
|
+
* branch tip. To release a tag again, delete it in the repository first.
|
|
315
|
+
*
|
|
316
|
+
* @param tree Tree of the released files.
|
|
317
|
+
* @return Tagged commit when the tag has the same files, null without the tag.
|
|
318
|
+
* @throws {ValidationError} When the tag has other files.
|
|
319
|
+
*/
|
|
320
|
+
async checkTag(tree) {
|
|
321
|
+
const { gitUrl, tag } = this.options;
|
|
322
|
+
const ref = `refs/tags/${tag}`;
|
|
323
|
+
const remote = await this.git({
|
|
324
|
+
args: ['ls-remote', '--tags', gitUrl, ref],
|
|
325
|
+
});
|
|
326
|
+
const found = remote
|
|
327
|
+
.split('\n')
|
|
328
|
+
.some((line) => line.endsWith(`\t${ref}`));
|
|
329
|
+
const tagged = found ? await this.fetchCommit(ref) : null;
|
|
330
|
+
const taggedTree = tagged
|
|
331
|
+
? await this.git({ args: ['rev-parse', `${tagged}^{tree}`] })
|
|
332
|
+
: null;
|
|
333
|
+
if (tagged && taggedTree !== tree) {
|
|
334
|
+
throw new ValidationError(`Tag ${tag} already exists in ${redactUrls(gitUrl)} with other files. Tags are never replaced: delete the tag in the repository to release it again.`);
|
|
335
|
+
}
|
|
336
|
+
if (tagged) {
|
|
337
|
+
this.logger.notice(`Tag ${tag} already has these files, nothing to push.`);
|
|
338
|
+
}
|
|
339
|
+
return tagged;
|
|
340
|
+
}
|
|
341
|
+
/**
|
|
342
|
+
* Get the refs to push: the branch, when the release moves it and the
|
|
343
|
+
* source isn't outdated, and the tag, unless it already exists with these
|
|
344
|
+
* files.
|
|
345
|
+
*
|
|
346
|
+
* @param revision Release commit.
|
|
347
|
+
* @param previous Previous release.
|
|
348
|
+
* @param tagged Commit of an existing tag with the same files, from checkTag().
|
|
349
|
+
* @param outdated Whether the branch has newer source history, from createRelease().
|
|
350
|
+
* @return Refs like refs/heads/main and refs/tags/1.2.0.
|
|
351
|
+
*/
|
|
352
|
+
getRefsToPush(revision, previous, tagged = null, outdated = false) {
|
|
353
|
+
const { branch, tag } = this.options;
|
|
354
|
+
return [
|
|
355
|
+
branch && !tagged && !outdated && revision !== previous
|
|
356
|
+
? `refs/heads/${branch}`
|
|
357
|
+
: '',
|
|
358
|
+
tag && !tagged ? `refs/tags/${tag}` : '',
|
|
359
|
+
].filter(Boolean);
|
|
360
|
+
}
|
|
361
|
+
/**
|
|
362
|
+
* Push the release to the refs in one atomic push, never forced, so the
|
|
363
|
+
* branch only moves forward and a tag that was created in the meantime
|
|
364
|
+
* is never replaced. A dry run checks the push without writing.
|
|
365
|
+
*
|
|
366
|
+
* @param revision Release commit.
|
|
367
|
+
* @param refs Refs from getRefsToPush().
|
|
368
|
+
* @return Pushed refs, empty in a dry run or without refs.
|
|
369
|
+
*/
|
|
370
|
+
async push(revision, refs) {
|
|
371
|
+
const { gitUrl, dryRun } = this.options;
|
|
372
|
+
if (refs.length) {
|
|
373
|
+
await this.git({
|
|
374
|
+
args: [
|
|
375
|
+
'push',
|
|
376
|
+
'--no-verify',
|
|
377
|
+
'--atomic',
|
|
378
|
+
...(dryRun ? ['--dry-run'] : []),
|
|
379
|
+
gitUrl,
|
|
380
|
+
...refs.map((ref) => `${revision}:${ref}`),
|
|
381
|
+
],
|
|
382
|
+
});
|
|
383
|
+
this.logger.notice(dryRun
|
|
384
|
+
? `Dry run, skipped pushing ${refs.join(', ')}.`
|
|
385
|
+
: `Pushed ${revision} to ${refs.join(', ')}.`);
|
|
386
|
+
}
|
|
387
|
+
else {
|
|
388
|
+
this.logger.notice('No changes to push.');
|
|
389
|
+
}
|
|
390
|
+
return dryRun ? [] : refs;
|
|
391
|
+
}
|
|
392
|
+
async getDefaultMessage() {
|
|
393
|
+
const { tag, branch, sourceRef } = this.options;
|
|
394
|
+
const source = await this.git({
|
|
395
|
+
args: ['rev-parse', `${sourceRef}^{commit}`],
|
|
396
|
+
});
|
|
397
|
+
return `Release ${tag ?? branch} from ${source}`;
|
|
398
|
+
}
|
|
399
|
+
async getRepoRoot() {
|
|
400
|
+
this.repoRoot =
|
|
401
|
+
this.repoRoot ??
|
|
402
|
+
(await this.runner({
|
|
403
|
+
args: ['rev-parse', '--show-toplevel'],
|
|
404
|
+
cwd: this.options.sourceDir,
|
|
405
|
+
})).stdout.trim();
|
|
406
|
+
return this.repoRoot;
|
|
407
|
+
}
|
|
408
|
+
/**
|
|
409
|
+
* Run git in the source repository.
|
|
410
|
+
*
|
|
411
|
+
* @param input Command input without the working directory.
|
|
412
|
+
* @return Trimmed standard output.
|
|
413
|
+
*/
|
|
414
|
+
async git(input) {
|
|
415
|
+
const cwd = await this.getRepoRoot();
|
|
416
|
+
this.logger.debug(redactUrls(`git ${input.args.join(' ')}`));
|
|
417
|
+
const { stdout } = await this.runner({ ...input, cwd });
|
|
418
|
+
return stdout.trim();
|
|
419
|
+
}
|
|
420
|
+
}
|
package/dist/git.d.ts
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
export interface GitCommandInput {
|
|
2
|
+
/** Arguments after "git", passed without a shell. */
|
|
3
|
+
args: string[];
|
|
4
|
+
/** Working directory of the command. */
|
|
5
|
+
cwd?: string;
|
|
6
|
+
/** Text written to the standard input. */
|
|
7
|
+
input?: string;
|
|
8
|
+
/** Extra environment variables, like GIT_INDEX_FILE. */
|
|
9
|
+
env?: Record<string, string>;
|
|
10
|
+
}
|
|
11
|
+
export interface GitCommandOutput {
|
|
12
|
+
/** Standard output. */
|
|
13
|
+
stdout: string;
|
|
14
|
+
/** Standard error. */
|
|
15
|
+
stderr: string;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Run a git command. Rejects with a GitError when it exits with an error.
|
|
19
|
+
*/
|
|
20
|
+
export type GitRunner = (input: GitCommandInput) => Promise<GitCommandOutput>;
|
|
21
|
+
/**
|
|
22
|
+
* Hide the credentials in URLs, like https://token@github.com/owner/repo.git,
|
|
23
|
+
* so that they never end up in logs or error messages.
|
|
24
|
+
*
|
|
25
|
+
* @param text Text with URLs.
|
|
26
|
+
* @return Text with the user info of URLs replaced by ***.
|
|
27
|
+
*/
|
|
28
|
+
export declare function redactUrls(text: string): string;
|
|
29
|
+
/**
|
|
30
|
+
* Failed git command.
|
|
31
|
+
*/
|
|
32
|
+
export declare class GitError extends Error {
|
|
33
|
+
name: string;
|
|
34
|
+
exitCode: number;
|
|
35
|
+
stderr: string;
|
|
36
|
+
/**
|
|
37
|
+
* @param message Error message.
|
|
38
|
+
* @param stderr Standard error of the command.
|
|
39
|
+
*/
|
|
40
|
+
constructor(message: string, stderr?: string);
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Create a runner that spawns git without a shell, with English messages and
|
|
44
|
+
* without prompting for credentials.
|
|
45
|
+
*
|
|
46
|
+
* @param binary Git binary.
|
|
47
|
+
* @return Command runner.
|
|
48
|
+
*/
|
|
49
|
+
export declare function createGitRunner(binary?: string): GitRunner;
|
package/dist/git.js
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { spawn } from 'node:child_process';
|
|
2
|
+
/**
|
|
3
|
+
* Hide the credentials in URLs, like https://token@github.com/owner/repo.git,
|
|
4
|
+
* so that they never end up in logs or error messages.
|
|
5
|
+
*
|
|
6
|
+
* @param text Text with URLs.
|
|
7
|
+
* @return Text with the user info of URLs replaced by ***.
|
|
8
|
+
*/
|
|
9
|
+
export function redactUrls(text) {
|
|
10
|
+
return text.replace(/(\w+:\/\/)[^/@\s]+@/g, '$1***@');
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Failed git command.
|
|
14
|
+
*/
|
|
15
|
+
export class GitError extends Error {
|
|
16
|
+
name = 'GitError';
|
|
17
|
+
exitCode = 1;
|
|
18
|
+
stderr;
|
|
19
|
+
/**
|
|
20
|
+
* @param message Error message.
|
|
21
|
+
* @param stderr Standard error of the command.
|
|
22
|
+
*/
|
|
23
|
+
constructor(message, stderr = '') {
|
|
24
|
+
super(redactUrls(message));
|
|
25
|
+
this.stderr = redactUrls(stderr);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Create a runner that spawns git without a shell, with English messages and
|
|
30
|
+
* without prompting for credentials.
|
|
31
|
+
*
|
|
32
|
+
* @param binary Git binary.
|
|
33
|
+
* @return Command runner.
|
|
34
|
+
*/
|
|
35
|
+
export function createGitRunner(binary = 'git') {
|
|
36
|
+
return ({ args, cwd, input, env = {} }) => new Promise((resolve, reject) => {
|
|
37
|
+
const child = spawn(binary, args, {
|
|
38
|
+
cwd,
|
|
39
|
+
env: {
|
|
40
|
+
...process.env,
|
|
41
|
+
LC_ALL: 'C',
|
|
42
|
+
GIT_TERMINAL_PROMPT: '0',
|
|
43
|
+
...env,
|
|
44
|
+
},
|
|
45
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
46
|
+
});
|
|
47
|
+
const output = { stdout: '', stderr: '' };
|
|
48
|
+
child.stdout.setEncoding('utf8');
|
|
49
|
+
child.stderr.setEncoding('utf8');
|
|
50
|
+
child.stdout.on('data', (data) => (output.stdout += data));
|
|
51
|
+
child.stderr.on('data', (data) => (output.stderr += data));
|
|
52
|
+
child.on('error', (error) => reject(new GitError(`Can't run ${binary}: ${error.message}. Install git.`)));
|
|
53
|
+
child.on('close', (code) => {
|
|
54
|
+
if (0 === code) {
|
|
55
|
+
resolve(output);
|
|
56
|
+
}
|
|
57
|
+
else {
|
|
58
|
+
reject(new GitError(`git ${args[0]} failed: ${output.stderr.trim()}`, output.stderr));
|
|
59
|
+
}
|
|
60
|
+
});
|
|
61
|
+
// A command can exit before it reads all of its input.
|
|
62
|
+
child.stdin.on('error', () => undefined);
|
|
63
|
+
child.stdin.end(input ?? '');
|
|
64
|
+
});
|
|
65
|
+
}
|
package/dist/github.d.ts
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A GitHub pull request of a commit.
|
|
3
|
+
*/
|
|
4
|
+
export interface GitHubPullRequest {
|
|
5
|
+
number: number;
|
|
6
|
+
url: string;
|
|
7
|
+
/** GitHub handle of the pull request author, without the @. */
|
|
8
|
+
author: string;
|
|
9
|
+
}
|
|
10
|
+
export interface GetGitHubPullRequestInput {
|
|
11
|
+
/** Directory of the repository, whose GitHub remote `gh` uses. */
|
|
12
|
+
cwd: string;
|
|
13
|
+
/** Commit hash. */
|
|
14
|
+
commit: string;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Find the pull request that merged a commit, with the authenticated `gh`
|
|
18
|
+
* command line client.
|
|
19
|
+
*
|
|
20
|
+
* @param input Repository directory and commit.
|
|
21
|
+
* @return The first pull request of the commit, or undefined without one.
|
|
22
|
+
* @throws {ValidationError} If `gh` fails, like without authentication.
|
|
23
|
+
*/
|
|
24
|
+
export declare function getGitHubPullRequest(input: GetGitHubPullRequestInput): Promise<GitHubPullRequest | undefined>;
|
package/dist/github.js
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { execFile } from 'node:child_process';
|
|
2
|
+
import { promisify } from 'node:util';
|
|
3
|
+
import { ValidationError } from "./errors.js";
|
|
4
|
+
const execFileAsync = promisify(execFile);
|
|
5
|
+
/**
|
|
6
|
+
* Find the pull request that merged a commit, with the authenticated `gh`
|
|
7
|
+
* command line client.
|
|
8
|
+
*
|
|
9
|
+
* @param input Repository directory and commit.
|
|
10
|
+
* @return The first pull request of the commit, or undefined without one.
|
|
11
|
+
* @throws {ValidationError} If `gh` fails, like without authentication.
|
|
12
|
+
*/
|
|
13
|
+
export async function getGitHubPullRequest(input) {
|
|
14
|
+
let stdout;
|
|
15
|
+
try {
|
|
16
|
+
({ stdout } = await execFileAsync('gh', ['api', `repos/{owner}/{repo}/commits/${input.commit}/pulls`], { cwd: input.cwd }));
|
|
17
|
+
}
|
|
18
|
+
catch (error) {
|
|
19
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
20
|
+
throw new ValidationError(`Could not find the pull request of commit ${input.commit}: ${message}`);
|
|
21
|
+
}
|
|
22
|
+
const [pullRequest] = JSON.parse(stdout);
|
|
23
|
+
return pullRequest
|
|
24
|
+
? {
|
|
25
|
+
number: pullRequest.number,
|
|
26
|
+
url: pullRequest.html_url,
|
|
27
|
+
author: pullRequest.user?.login ?? '',
|
|
28
|
+
}
|
|
29
|
+
: undefined;
|
|
30
|
+
}
|
package/dist/index.d.ts
CHANGED
|
@@ -14,12 +14,15 @@ export { ZIP_MTIME, ZIP_FILE_MODE, createZip } from './zip.ts';
|
|
|
14
14
|
export { isUrl, downloadFile } from './download.ts';
|
|
15
15
|
export { readZipEntries, getZipEntryErrors, extractZip } from './unzip.ts';
|
|
16
16
|
export { GIT_TAG_FORMAT, VersionMismatchError, getGitTagVersion, findConstantValues, findConstantDefinitions, findVersionMismatches, getVersionMismatchProblems, } from './version-check.ts';
|
|
17
|
-
export { CHANGESET_BUMPS, parseChangeset, formatChangeset, getChangesetBump, getNextVersion, } from './changesets.ts';
|
|
18
|
-
export { addChangeset, getChangesetReleasePlan, getChangesetStatus, versionChangesets, tagChangesets, } from './changeset-release.ts';
|
|
17
|
+
export { CHANGESET_BUMPS, parseChangeset, formatChangeset, formatChangelogEntries, formatChangelogList, getChangesetBump, getNextVersion, } from './changesets.ts';
|
|
18
|
+
export { addChangeset, getChangesetPackages, getChangesetReleasePlan, getChangesetStatus, versionChangesets, tagChangesets, } from './changeset-release.ts';
|
|
19
19
|
export { defineConfig, packageJsonVersion, composerJsonVersion, wordpressPluginVersion, wordpressThemeVersion, wordpressStableTag, gitTagVersion, phpConstantVersion, loadChangesetConfig, } from './changeset-config.ts';
|
|
20
20
|
export { Svn, SvnError, createSvnRunner, parseStatus, toSvnPath, } from './svn.ts';
|
|
21
21
|
export { syncDirectory, copyFileIfChanged } from './sync.ts';
|
|
22
22
|
export { ASSET_MIME_TYPES, SvnDeploy } from './svn-deploy.ts';
|
|
23
|
+
export { GitError, createGitRunner, redactUrls } from './git.ts';
|
|
24
|
+
export { GitDeploy, isValidRefName, resolveGitDeployOptions, } from './git-deploy.ts';
|
|
25
|
+
export { getGitDeployInputFromArgs } from './commands/deploy-git.ts';
|
|
23
26
|
export type { CliIo, CommandResults } from './cli.ts';
|
|
24
27
|
export type { ChecksumsResult } from './commands/checksums.ts';
|
|
25
28
|
export type { SignResult } from './commands/sign.ts';
|
|
@@ -37,10 +40,14 @@ export type { FindMainPluginFileInput, HeaderValues, PackageMeta, ParseFileHeade
|
|
|
37
40
|
export type { CreateZipInput } from './zip.ts';
|
|
38
41
|
export type { ExtractZipInput, ZipEntryInfo, ZipEntryType } from './unzip.ts';
|
|
39
42
|
export type { FindVersionMismatchesInput, GitTagInput, VersionCheckResult, VersionMismatch, } from './version-check.ts';
|
|
40
|
-
export type { Changeset, ChangesetBump, ChangesetPackages, ChangesetConfig, ChangesetPackageType, ChangesetPackage, PackageRelease, ChangesetReleasePlan, } from './changesets.ts';
|
|
41
|
-
export type { ChangesetRepository, ChangesetVersionInput, ChangesetTagInput, ChangesetTagResult, } from './changeset-release.ts';
|
|
42
|
-
export type { WpReleaseConfig, ChangesetPackageGroup, PackageVersionConfig, PackageVersionSource, PackageVersionTarget, PackageIdSource, NextVersionInput, PackageJsonVersionOptions, ComposerJsonVersionOptions, WordPressVersionFileOptions, GitTagVersionOptions, PhpConstantVersionOptions, LoadChangesetConfigInput, } from './changeset-config.ts';
|
|
43
|
+
export type { Changeset, ChangesetBump, ChangesetPackages, ChangesetConfig, ChangesetPackageType, ChangesetPackage, PackageRelease, VersionedRelease, ChangesetReleasePlan, } from './changesets.ts';
|
|
44
|
+
export type { ChangesetRepository, ChangesetVersionInput, VersionChangesetsResult, ChangesetTagInput, ChangesetTagResult, } from './changeset-release.ts';
|
|
45
|
+
export type { WpReleaseConfig, ChangesetPackageGroup, PackageVersionConfig, PackageVersionSource, PackageVersionTarget, PackageIdSource, NextVersionInput, ReleaseCommit, ReleaseNotesInput, ChangelogEntryInput, PackageJsonVersionOptions, ComposerJsonVersionOptions, WordPressVersionFileOptions, GitTagVersionOptions, PhpConstantVersionOptions, LoadChangesetConfigInput, } from './changeset-config.ts';
|
|
43
46
|
export type { SvnCheckoutInput, SvnCommandInput, SvnCommandOutput, SvnCommitInput, SvnCopyInput, SvnDepth, SvnOptions, SvnPathsInput, SvnPropsetInput, SvnRunner, SvnStatusEntry, SvnStatusType, SvnUpdateInput, } from './svn.ts';
|
|
44
47
|
export type { SyncDirectoryInput, SyncResult } from './sync.ts';
|
|
45
48
|
export type { SummaryContext } from './summary.ts';
|
|
49
|
+
export { getGitHubPullRequest } from './github.ts';
|
|
50
|
+
export type { GitHubPullRequest, GetGitHubPullRequestInput } from './github.ts';
|
|
46
51
|
export type { CheckoutPath, DeployResult } from './svn-deploy.ts';
|
|
52
|
+
export type { GitCommandInput, GitCommandOutput, GitRunner } from './git.ts';
|
|
53
|
+
export type { GitDeployInput, GitDeployOptions, GitDeployResult, } from './git-deploy.ts';
|
package/dist/index.js
CHANGED
|
@@ -14,9 +14,13 @@ export { ZIP_MTIME, ZIP_FILE_MODE, createZip } from "./zip.js";
|
|
|
14
14
|
export { isUrl, downloadFile } from "./download.js";
|
|
15
15
|
export { readZipEntries, getZipEntryErrors, extractZip } from "./unzip.js";
|
|
16
16
|
export { GIT_TAG_FORMAT, VersionMismatchError, getGitTagVersion, findConstantValues, findConstantDefinitions, findVersionMismatches, getVersionMismatchProblems, } from "./version-check.js";
|
|
17
|
-
export { CHANGESET_BUMPS, parseChangeset, formatChangeset, getChangesetBump, getNextVersion, } from "./changesets.js";
|
|
18
|
-
export { addChangeset, getChangesetReleasePlan, getChangesetStatus, versionChangesets, tagChangesets, } from "./changeset-release.js";
|
|
17
|
+
export { CHANGESET_BUMPS, parseChangeset, formatChangeset, formatChangelogEntries, formatChangelogList, getChangesetBump, getNextVersion, } from "./changesets.js";
|
|
18
|
+
export { addChangeset, getChangesetPackages, getChangesetReleasePlan, getChangesetStatus, versionChangesets, tagChangesets, } from "./changeset-release.js";
|
|
19
19
|
export { defineConfig, packageJsonVersion, composerJsonVersion, wordpressPluginVersion, wordpressThemeVersion, wordpressStableTag, gitTagVersion, phpConstantVersion, loadChangesetConfig, } from "./changeset-config.js";
|
|
20
20
|
export { Svn, SvnError, createSvnRunner, parseStatus, toSvnPath, } from "./svn.js";
|
|
21
21
|
export { syncDirectory, copyFileIfChanged } from "./sync.js";
|
|
22
22
|
export { ASSET_MIME_TYPES, SvnDeploy } from "./svn-deploy.js";
|
|
23
|
+
export { GitError, createGitRunner, redactUrls } from "./git.js";
|
|
24
|
+
export { GitDeploy, isValidRefName, resolveGitDeployOptions, } from "./git-deploy.js";
|
|
25
|
+
export { getGitDeployInputFromArgs } from "./commands/deploy-git.js";
|
|
26
|
+
export { getGitHubPullRequest } from "./github.js";
|