1 | "use strict";
|
2 | Object.defineProperty(exports, "__esModule", { value: true });
|
3 | exports.getVersionMap = exports.defaultLabels = exports.labelDefinition = exports.isVersionLabel = exports.releaseLabels = void 0;
|
4 | const tslib_1 = require("tslib");
|
5 | const await_to_js_1 = tslib_1.__importDefault(require("await-to-js"));
|
6 | const fs = tslib_1.__importStar(require("fs"));
|
7 | const lodash_chunk_1 = tslib_1.__importDefault(require("lodash.chunk"));
|
8 | const semver_1 = require("semver");
|
9 | const util_1 = require("util");
|
10 | const t = tslib_1.__importStar(require("io-ts"));
|
11 | const typescript_memoize_1 = require("typescript-memoize");
|
12 | const changelog_1 = tslib_1.__importDefault(require("./changelog"));
|
13 | const log_parse_1 = tslib_1.__importDefault(require("./log-parse"));
|
14 | const semver_2 = tslib_1.__importStar(require("./semver"));
|
15 | const exec_promise_1 = tslib_1.__importDefault(require("./utils/exec-promise"));
|
16 | const logger_1 = require("./utils/logger");
|
17 | const make_hooks_1 = require("./utils/make-hooks");
|
18 | const child_process_1 = require("child_process");
|
19 | const match_sha_to_pr_1 = require("./match-sha-to-pr");
|
20 | exports.releaseLabels = [
|
21 | semver_2.default.major,
|
22 | semver_2.default.minor,
|
23 | semver_2.default.patch,
|
24 | "skip",
|
25 | "release",
|
26 | ];
|
27 |
|
28 | exports.isVersionLabel = (label) => exports.releaseLabels.includes(label);
|
29 | const labelDefinitionRequired = t.type({
|
30 |
|
31 | name: t.string,
|
32 | });
|
33 | const labelDefinitionOptional = t.partial({
|
34 |
|
35 | changelogTitle: t.string,
|
36 |
|
37 | color: t.string,
|
38 |
|
39 | description: t.string,
|
40 |
|
41 | releaseType: t.union([
|
42 | t.literal("none"),
|
43 | t.literal("skip"),
|
44 | ...exports.releaseLabels.map((l) => t.literal(l)),
|
45 | ]),
|
46 |
|
47 | overwrite: t.boolean,
|
48 | });
|
49 | exports.labelDefinition = t.intersection([
|
50 | labelDefinitionOptional,
|
51 | labelDefinitionRequired,
|
52 | ]);
|
53 | exports.defaultLabels = [
|
54 | {
|
55 | name: "major",
|
56 | changelogTitle: "💥 Breaking Change",
|
57 | description: "Increment the major version when merged",
|
58 | releaseType: semver_2.default.major,
|
59 | },
|
60 | {
|
61 | name: "minor",
|
62 | changelogTitle: "🚀 Enhancement",
|
63 | description: "Increment the minor version when merged",
|
64 | releaseType: semver_2.default.minor,
|
65 | },
|
66 | {
|
67 | name: "patch",
|
68 | changelogTitle: "🐛 Bug Fix",
|
69 | description: "Increment the patch version when merged",
|
70 | releaseType: semver_2.default.patch,
|
71 | },
|
72 | {
|
73 | name: "skip-release",
|
74 | description: "Preserve the current version when merged",
|
75 | releaseType: "skip",
|
76 | },
|
77 | {
|
78 | name: "release",
|
79 | description: "Create a release when this pr is merged",
|
80 | releaseType: "release",
|
81 | },
|
82 | {
|
83 | name: "internal",
|
84 | changelogTitle: "🏠 Internal",
|
85 | description: "Changes only affect the internal API",
|
86 | releaseType: "none",
|
87 | },
|
88 | {
|
89 | name: "documentation",
|
90 | changelogTitle: "📝 Documentation",
|
91 | description: "Changes only affect the documentation",
|
92 | releaseType: "none",
|
93 | },
|
94 | {
|
95 | name: "tests",
|
96 | changelogTitle: "🧪 Tests",
|
97 | description: "Add or improve existing tests",
|
98 | releaseType: "none",
|
99 | },
|
100 | {
|
101 | name: "dependencies",
|
102 | changelogTitle: "🔩 Dependency Updates",
|
103 | description: "Update one or more dependencies version",
|
104 | releaseType: "none",
|
105 | },
|
106 | {
|
107 | name: "performance",
|
108 | changelogTitle: "🏎 Performance",
|
109 | description: "Improve performance of an existing feature",
|
110 | releaseType: semver_2.default.patch,
|
111 | },
|
112 | ];
|
113 |
|
114 | exports.getVersionMap = (labels = exports.defaultLabels) => labels.reduce((semVer, { releaseType: type, name }) => {
|
115 | if (type && (exports.isVersionLabel(type) || type === "none")) {
|
116 | const list = semVer.get(type) || [];
|
117 | semVer.set(type, [...list, name]);
|
118 | }
|
119 | return semVer;
|
120 | }, new Map());
|
121 | const readFile = util_1.promisify(fs.readFile);
|
122 | const writeFile = util_1.promisify(fs.writeFile);
|
123 |
|
124 |
|
125 |
|
126 | class Release {
|
127 |
|
128 | constructor(git, config = {
|
129 | baseBranch: "master",
|
130 | prereleaseBranches: ["next"],
|
131 | labels: exports.defaultLabels,
|
132 | }, logger = logger_1.dummyLog()) {
|
133 | this.config = config;
|
134 | this.logger = logger;
|
135 | this.hooks = make_hooks_1.makeReleaseHooks();
|
136 | this.versionLabels = exports.getVersionMap(config.labels);
|
137 | this.git = git;
|
138 | }
|
139 |
|
140 | async makeChangelog(version) {
|
141 | const project = await this.git.getProject();
|
142 | const changelog = new changelog_1.default(this.logger, {
|
143 | owner: this.git.options.owner,
|
144 | repo: this.git.options.repo,
|
145 | baseUrl: project.html_url,
|
146 | labels: this.config.labels,
|
147 | baseBranch: this.config.baseBranch,
|
148 | prereleaseBranches: this.config.prereleaseBranches,
|
149 | });
|
150 | this.hooks.onCreateChangelog.call(changelog, version);
|
151 | changelog.loadDefaultHooks();
|
152 | return changelog;
|
153 | }
|
154 | |
155 |
|
156 |
|
157 |
|
158 |
|
159 |
|
160 | async generateReleaseNotes(from, to = "HEAD", version) {
|
161 | const commits = await this.getCommitsInRelease(from, to);
|
162 | const changelog = await this.makeChangelog(version);
|
163 | return changelog.generateReleaseNotes(commits);
|
164 | }
|
165 |
|
166 | async getCommitsInRelease(from, to = "HEAD") {
|
167 | const allCommits = await this.getCommits(from, to);
|
168 | const allPrCommits = await Promise.all(allCommits
|
169 | .filter((commit) => commit.pullRequest)
|
170 | .map(async (commit) => {
|
171 | const [err, commits = []] = await await_to_js_1.default(this.git.getCommitsForPR(Number(commit.pullRequest.number)));
|
172 | return err ? [] : commits;
|
173 | }));
|
174 | const allPrCommitHashes = allPrCommits
|
175 | .filter(Boolean)
|
176 | .reduce((all, pr) => [...all, ...pr.map((subCommit) => subCommit.sha)], []);
|
177 | const uniqueCommits = allCommits.filter((commit) => (commit.pullRequest || !allPrCommitHashes.includes(commit.hash)) &&
|
178 | !commit.subject.includes("[skip ci]"));
|
179 | const commitsWithoutPR = uniqueCommits.filter((commit) => !commit.pullRequest);
|
180 | const batches = lodash_chunk_1.default(commitsWithoutPR, 10);
|
181 | const queries = await Promise.all(batches
|
182 | .map((batch) => match_sha_to_pr_1.buildSearchQuery(this.git.options.owner, this.git.options.repo, batch.map((c) => c.hash)))
|
183 | .filter((q) => Boolean(q))
|
184 | .map((q) => this.git.graphql(q)));
|
185 | const data = queries.filter((q) => Boolean(q));
|
186 | if (!data.length) {
|
187 | return uniqueCommits;
|
188 | }
|
189 | const commitsInRelease = [
|
190 | ...uniqueCommits,
|
191 | ];
|
192 | const logParse = await this.createLogParse();
|
193 | const entries = data.reduce((acc, result) => [...acc, ...Object.entries(result)], []);
|
194 | await Promise.all(entries
|
195 | .filter((result) => Boolean(result[1]))
|
196 | .map(([key, result]) => match_sha_to_pr_1.processQueryResult({
|
197 | sha: key,
|
198 | result,
|
199 | commitsWithoutPR,
|
200 | owner: this.git.options.owner,
|
201 | prereleaseBranches: this.config.prereleaseBranches,
|
202 | }))
|
203 | .filter((commit) => Boolean(commit))
|
204 | .map(async (commit) => {
|
205 | const index = commitsInRelease.findIndex((c) => c && c.hash === commit.hash);
|
206 | commitsInRelease[index] = await logParse.normalizeCommit(commit);
|
207 | }));
|
208 | return commitsInRelease.filter((commit) => Boolean(commit));
|
209 | }
|
210 |
|
211 | async updateChangelogFile(title, releaseNotes, changelogPath) {
|
212 | const date = new Date().toDateString();
|
213 | let newChangelog = "#";
|
214 | if (title) {
|
215 | newChangelog += ` ${title}`;
|
216 | }
|
217 | newChangelog += ` (${date})\n\n${releaseNotes}`;
|
218 | if (fs.existsSync(changelogPath)) {
|
219 | this.logger.verbose.info("Old changelog exists, prepending changes.");
|
220 | const oldChangelog = await readFile(changelogPath, "utf8");
|
221 | newChangelog = `${newChangelog}\n\n---\n\n${oldChangelog}`;
|
222 | }
|
223 | else {
|
224 | newChangelog += "\n";
|
225 | }
|
226 | await writeFile(changelogPath, newChangelog);
|
227 | this.logger.verbose.info("Wrote new changelog to filesystem.");
|
228 | await exec_promise_1.default("git", ["add", changelogPath]);
|
229 | }
|
230 | |
231 |
|
232 |
|
233 |
|
234 |
|
235 |
|
236 |
|
237 | async addToChangelog(releaseNotes, lastRelease, currentVersion) {
|
238 | this.hooks.createChangelogTitle.tapPromise("Default", async () => {
|
239 | let version;
|
240 | if (lastRelease.match(/\d+\.\d+\.\d+/)) {
|
241 | version = await this.calcNextVersion(lastRelease);
|
242 | }
|
243 | else {
|
244 |
|
245 | const bump = await this.getSemverBump(lastRelease);
|
246 | version = semver_1.inc(currentVersion, bump);
|
247 | }
|
248 | this.logger.verbose.info("Calculated next version to be:", version);
|
249 | if (!version) {
|
250 | return "";
|
251 | }
|
252 | return this.config.noVersionPrefix || version.startsWith("v")
|
253 | ? version
|
254 | : `v${version}`;
|
255 | });
|
256 | this.logger.verbose.info("Adding new changes to changelog.");
|
257 | const title = await this.hooks.createChangelogTitle.promise();
|
258 | await this.updateChangelogFile(title || "", releaseNotes, "CHANGELOG.md");
|
259 | }
|
260 | |
261 |
|
262 |
|
263 |
|
264 |
|
265 |
|
266 | async getCommits(from, to = "HEAD") {
|
267 | this.logger.verbose.info(`Getting commits from ${from} to ${to}`);
|
268 | const gitlog = await this.git.getGitLog(from, to);
|
269 | this.logger.veryVerbose.info("Got gitlog:\n", gitlog);
|
270 | const logParse = await this.createLogParse();
|
271 | const commits = (await logParse.normalizeCommits(gitlog)).filter((commit) => {
|
272 | let released;
|
273 | try {
|
274 |
|
275 |
|
276 | child_process_1.execSync(`git merge-base --is-ancestor ${from} ${commit.hash}`, {
|
277 | encoding: "utf8",
|
278 | });
|
279 | released = false;
|
280 | }
|
281 | catch (error) {
|
282 | try {
|
283 |
|
284 |
|
285 |
|
286 |
|
287 | child_process_1.execSync(`git merge-base --is-ancestor ${commit.hash} ${from}`, {
|
288 | encoding: "utf8",
|
289 | });
|
290 | released = true;
|
291 | }
|
292 | catch (error) {
|
293 |
|
294 | released = false;
|
295 | }
|
296 | }
|
297 | if (released) {
|
298 | const shortHash = commit.hash.slice(0, 8);
|
299 | this.logger.verbose.warn(`Commit already released, omitting: ${shortHash}: "${commit.subject}"`);
|
300 | }
|
301 | return !released;
|
302 | });
|
303 | this.logger.veryVerbose.info("Added labels to commits:\n", commits);
|
304 | return commits;
|
305 | }
|
306 |
|
307 | async addLabelsToProject(labels, options = {}) {
|
308 | const oldLabels = ((await this.git.getProjectLabels()) || []).map((l) => l.toLowerCase());
|
309 | const labelsToCreate = labels.filter((label) => {
|
310 | if (label.releaseType === "release" &&
|
311 | !this.config.onlyPublishWithReleaseLabel) {
|
312 | return false;
|
313 | }
|
314 | if (label.releaseType === "skip" &&
|
315 | this.config.onlyPublishWithReleaseLabel) {
|
316 | return false;
|
317 | }
|
318 | return true;
|
319 | });
|
320 | if (!options.dryRun) {
|
321 | await Promise.all(labelsToCreate.map(async (label) => {
|
322 | if (oldLabels.some((o) => label.name.toLowerCase() === o)) {
|
323 | return this.git.updateLabel(label);
|
324 | }
|
325 | return this.git.createLabel(label);
|
326 | }));
|
327 | }
|
328 | const repoMetadata = await this.git.getProject();
|
329 | const justLabelNames = labelsToCreate.reduce((acc, label) => [...acc, label.name], []);
|
330 | if (justLabelNames.length > 0) {
|
331 | const state = options.dryRun ? "Would have created" : "Created";
|
332 | this.logger.log.log(`${state} labels: ${justLabelNames.join(", ")}`);
|
333 | }
|
334 | else {
|
335 | const state = options.dryRun ? "would have been" : "were";
|
336 | this.logger.log.log(`No labels ${state} created, they must have already been present on your project.`);
|
337 | }
|
338 | if (options.dryRun) {
|
339 | return;
|
340 | }
|
341 | this.logger.log.log(`\nYou can see these, and more at ${repoMetadata.html_url}/labels`);
|
342 | }
|
343 | |
344 |
|
345 |
|
346 |
|
347 |
|
348 |
|
349 | async getSemverBump(from, to = "HEAD") {
|
350 | const commits = await this.getCommits(from, to);
|
351 | const labels = commits.map((commit) => commit.labels);
|
352 | const { onlyPublishWithReleaseLabel } = this.config;
|
353 | const options = { onlyPublishWithReleaseLabel };
|
354 | this.logger.verbose.info("Calculating SEMVER bump using:\n", {
|
355 | labels,
|
356 | versionLabels: this.versionLabels,
|
357 | options,
|
358 | });
|
359 | const result = semver_2.calculateSemVerBump(labels, this.versionLabels, options);
|
360 | this.logger.verbose.success("Calculated SEMVER bump:", result);
|
361 | return result;
|
362 | }
|
363 |
|
364 | async calcNextVersion(lastTag) {
|
365 | const bump = await this.getSemverBump(lastTag);
|
366 | return semver_1.inc(lastTag, bump);
|
367 | }
|
368 |
|
369 | async createLogParse() {
|
370 | const logParse = new log_parse_1.default();
|
371 | logParse.hooks.parseCommit.tapPromise("Author Info", async (commit) => this.attachAuthor(commit));
|
372 | logParse.hooks.parseCommit.tapPromise("PR Information", async (commit) => this.addPrInfoToCommit(commit));
|
373 | logParse.hooks.parseCommit.tapPromise("PR Commits", async (commit) => {
|
374 | const prsSinceLastRelease = await this.getPRsSinceLastRelease();
|
375 | return this.getPRForRebasedCommits(commit, prsSinceLastRelease);
|
376 | });
|
377 | this.hooks.onCreateLogParse.call(logParse);
|
378 | return logParse;
|
379 | }
|
380 |
|
381 | async getPRsSinceLastRelease() {
|
382 | let lastRelease;
|
383 | try {
|
384 | lastRelease = await this.git.getLatestReleaseInfo();
|
385 | }
|
386 | catch (error) {
|
387 | const firstCommit = await this.git.getFirstCommit();
|
388 | lastRelease = {
|
389 | published_at: await this.git.getCommitDate(firstCommit),
|
390 | };
|
391 | }
|
392 | if (!lastRelease) {
|
393 | return [];
|
394 | }
|
395 | const prsSinceLastRelease = await this.git.searchRepo({
|
396 | q: `is:pr is:merged merged:>=${lastRelease.published_at}`,
|
397 | });
|
398 | if (!prsSinceLastRelease || !prsSinceLastRelease.items) {
|
399 | return [];
|
400 | }
|
401 | const data = await Promise.all(prsSinceLastRelease.items.map(async (pr) => this.git.getPullRequest(Number(pr.number))));
|
402 | return data.map((item) => item.data);
|
403 | }
|
404 | |
405 |
|
406 |
|
407 |
|
408 |
|
409 | async addPrInfoToCommit(commit) {
|
410 | const modifiedCommit = Object.assign({}, commit);
|
411 | if (!modifiedCommit.labels) {
|
412 | modifiedCommit.labels = [];
|
413 | }
|
414 | if (modifiedCommit.pullRequest) {
|
415 | const [err, info] = await await_to_js_1.default(this.git.getPr(modifiedCommit.pullRequest.number));
|
416 | if (err || !info || !info.data) {
|
417 | return modifiedCommit;
|
418 | }
|
419 | const labels = info ? info.data.labels.map((l) => l.name) : [];
|
420 | modifiedCommit.labels = [
|
421 | ...new Set([...labels, ...modifiedCommit.labels]),
|
422 | ];
|
423 | modifiedCommit.pullRequest.body = info.data.body;
|
424 | modifiedCommit.subject = info.data.title || modifiedCommit.subject;
|
425 | const hasPrOpener = modifiedCommit.authors.some((author) => author.username === info.data.user.login);
|
426 |
|
427 |
|
428 | if (!hasPrOpener) {
|
429 | const user = await this.git.getUserByUsername(info.data.user.login);
|
430 | if (user) {
|
431 | modifiedCommit.authors.push(Object.assign(Object.assign({}, user), { username: user.login }));
|
432 | }
|
433 | }
|
434 | }
|
435 | return modifiedCommit;
|
436 | }
|
437 | |
438 |
|
439 |
|
440 |
|
441 |
|
442 | getPRForRebasedCommits(commit, pullRequests) {
|
443 | const matchPr = pullRequests.find((pr) => pr.merge_commit_sha === commit.hash);
|
444 | if (!commit.pullRequest && matchPr) {
|
445 | const labels = matchPr.labels.map((label) => label.name) || [];
|
446 | commit.labels = [...new Set([...labels, ...commit.labels])];
|
447 | commit.pullRequest = {
|
448 | number: matchPr.number,
|
449 | };
|
450 | }
|
451 | return commit;
|
452 | }
|
453 |
|
454 | async attachAuthor(commit) {
|
455 | var _a, _b;
|
456 | const modifiedCommit = Object.assign({}, commit);
|
457 | let resolvedAuthors = [];
|
458 |
|
459 |
|
460 | if (modifiedCommit.pullRequest) {
|
461 | const [prCommitsErr, prCommits] = await await_to_js_1.default(this.git.getCommitsForPR(Number(modifiedCommit.pullRequest.number)));
|
462 | if (prCommitsErr || !prCommits) {
|
463 | return commit;
|
464 | }
|
465 | resolvedAuthors = await Promise.all(prCommits.map(async (prCommit) => {
|
466 | if (!prCommit.author) {
|
467 | return prCommit.commit.author;
|
468 | }
|
469 | return Object.assign(Object.assign(Object.assign({}, prCommit.author), (await this.git.getUserByUsername(prCommit.author.login))), { hash: prCommit.sha });
|
470 | }));
|
471 | }
|
472 | else {
|
473 | const [, response] = await await_to_js_1.default(this.git.getCommit(commit.hash));
|
474 | if ((_b = (_a = response === null || response === void 0 ? void 0 : response.data) === null || _a === void 0 ? void 0 : _a.author) === null || _b === void 0 ? void 0 : _b.login) {
|
475 | const username = response.data.author.login;
|
476 | const author = await this.git.getUserByUsername(username);
|
477 | resolvedAuthors.push(Object.assign(Object.assign({ name: commit.authorName, email: commit.authorEmail }, author), { hash: commit.hash }));
|
478 | }
|
479 | else if (commit.authorEmail) {
|
480 | const author = await this.git.getUserByEmail(commit.authorEmail);
|
481 | resolvedAuthors.push(Object.assign(Object.assign({ email: commit.authorEmail, name: commit.authorName }, author), { hash: commit.hash }));
|
482 | }
|
483 | }
|
484 | modifiedCommit.authors = resolvedAuthors.map((author) => (Object.assign(Object.assign({}, author), (author && "login" in author ? { username: author.login } : {}))));
|
485 | modifiedCommit.authors.forEach((author) => {
|
486 | this.logger.veryVerbose.info(`Found author: ${author.username} ${author.email} ${author.name}`);
|
487 | });
|
488 | return modifiedCommit;
|
489 | }
|
490 | }
|
491 | tslib_1.__decorate([
|
492 | typescript_memoize_1.Memoize()
|
493 | ], Release.prototype, "makeChangelog", null);
|
494 | tslib_1.__decorate([
|
495 | typescript_memoize_1.Memoize()
|
496 | ], Release.prototype, "getPRsSinceLastRelease", null);
|
497 | exports.default = Release;
|
498 |
|
\ | No newline at end of file |