UNPKG

13.3 kBJavaScriptView Raw
1"use strict";
2Object.defineProperty(exports, "__esModule", { value: true });
3const tslib_1 = require("tslib");
4const url_1 = require("url");
5const url_join_1 = tslib_1.__importDefault(require("url-join"));
6const bot_list_1 = tslib_1.__importDefault(require("@auto-it/bot-list"));
7const make_hooks_1 = require("./utils/make-hooks");
8const get_current_branch_1 = require("./utils/get-current-branch");
9const semver_1 = tslib_1.__importDefault(require("./semver"));
10/** Determine how deep the markdown headers are in a string */
11const getHeaderDepth = (line) => line.split("").reduce((count, char) => (char === "#" ? count + 1 : count), 0);
12/** Filter for only commits that have a specific label */
13const filterLabel = (commits, label) => commits.filter((commit) => commit.labels.includes(label));
14/**
15 * Manages creating the "Release Notes" that are included in
16 * both the CHANGELOG.md and GitHub releases.
17 */
18class Changelog {
19 /** Initialize the changelog generator with default hooks and labels */
20 constructor(logger, options) {
21 this.logger = logger;
22 this.options = options;
23 this.hooks = make_hooks_1.makeChangelogHooks();
24 const currentBranch = get_current_branch_1.getCurrentBranch();
25 if (!this.options.labels.find((l) => l.name === "pushToBaseBranch")) {
26 // Either put the name of a prerelease branch or the base-branch in the changelog
27 const branch = (currentBranch &&
28 options.prereleaseBranches.includes(currentBranch) &&
29 currentBranch) ||
30 options.baseBranch;
31 this.options.labels = [
32 ...this.options.labels,
33 {
34 name: "pushToBaseBranch",
35 changelogTitle: `⚠️ Pushed to \`${branch}\``,
36 description: "N/A",
37 releaseType: semver_1.default.patch,
38 },
39 ];
40 }
41 }
42 /** Load the default configuration */
43 loadDefaultHooks() {
44 this.hooks.renderChangelogAuthor.tap("Default", (author, commit) => this.createUserLink(author, commit));
45 this.hooks.renderChangelogAuthorLine.tap("Default", (author, user) => {
46 const authorString = author.name && user ? `${author.name} (${user})` : user;
47 return authorString ? `- ${authorString}` : undefined;
48 });
49 this.hooks.renderChangelogLine.tap("Default", ([commit, line]) => [
50 commit,
51 line,
52 ]);
53 this.hooks.renderChangelogTitle.tap("Default", (label, changelogTitles) => `#### ${changelogTitles[label]}\n`);
54 this.hooks.omitReleaseNotes.tap("Bots", (commit) => {
55 if (commit.authors.some((author) => (author.name && bot_list_1.default.includes(author.name)) ||
56 (author.username && bot_list_1.default.includes(author.username)))) {
57 return true;
58 }
59 });
60 }
61 /** Generate the release notes for a group of commits */
62 async generateReleaseNotes(commits) {
63 if (commits.length === 0) {
64 return "";
65 }
66 this.logger.verbose.info("Generating release notes for:\n", commits);
67 const split = this.splitCommits(commits);
68 this.logger.verbose.info("Split commits into groups");
69 this.logger.veryVerbose.info("\n", split);
70 const sections = [];
71 const extraNotes = (await this.hooks.addToBody.promise([], commits)) || [];
72 extraNotes.filter(Boolean).forEach((note) => sections.push(note));
73 await this.createReleaseNotesSection(commits, sections);
74 this.logger.verbose.info("Added release notes to changelog");
75 this.authors = this.getAllAuthors(split);
76 await this.createLabelSection(split, sections);
77 this.logger.verbose.info("Added groups to changelog");
78 await this.createAuthorSection(sections);
79 this.logger.verbose.info("Added authors to changelog");
80 const result = sections.join("\n\n");
81 this.logger.verbose.info("Successfully generated release notes.");
82 return result;
83 }
84 /** Create a link to a user for use in the changelog */
85 createUserLink(author, commit) {
86 const githubUrl = new url_1.URL(this.options.baseUrl).origin;
87 if (author.username === "invalid-email-address") {
88 return;
89 }
90 return author.username
91 ? `[@${author.username}](${url_join_1.default(githubUrl, author.username)})`
92 : author.email || commit.authorEmail;
93 }
94 /** Split commits into changelogTitle sections. */
95 splitCommits(commits) {
96 let currentCommits = [...commits];
97 const order = ["major", "minor", "patch"];
98 const sections = this.options.labels
99 .filter((label) => label.changelogTitle)
100 .sort((a, b) => {
101 const bIndex = order.indexOf(b.releaseType || "") + 1 || order.length + 1;
102 const aIndex = order.indexOf(a.releaseType || "") + 1 || order.length + 1;
103 return aIndex - bIndex;
104 })
105 .reduce((acc, item) => [...acc, item], []);
106 const defaultPatchLabelName = this.getFirstLabelNameFromLabelKey(this.options.labels, "patch");
107 commits
108 .filter(({ labels }) =>
109 // in case pr commit doesn't contain a label for section inclusion
110 !sections.some((section) => labels.includes(section.name)) ||
111 // in this case we auto attached a patch when it was merged
112 (labels[0] === "released" && labels.length === 1))
113 .map(({ labels }) => labels.push(defaultPatchLabelName));
114 return Object.assign({}, ...sections.map((label) => {
115 const matchedCommits = filterLabel(currentCommits, label.name);
116 currentCommits = currentCommits.filter((commit) => !matchedCommits.includes(commit));
117 return matchedCommits.length === 0
118 ? {}
119 : { [label.name]: matchedCommits };
120 }));
121 }
122 /** Get the default label for a label key */
123 getFirstLabelNameFromLabelKey(labels, labelKey) {
124 var _a;
125 return ((_a = labels.find((l) => l.releaseType === labelKey)) === null || _a === void 0 ? void 0 : _a.name) || labelKey;
126 }
127 /** Create a list of users */
128 async createUserLinkList(commit) {
129 const result = new Set();
130 await Promise.all(commit.authors.map(async (rawAuthor) => {
131 const data = this.authors.find(([, commitAuthor]) => (commitAuthor.name &&
132 rawAuthor.name &&
133 commitAuthor.name === rawAuthor.name) ||
134 (commitAuthor.email &&
135 rawAuthor.email &&
136 commitAuthor.email === rawAuthor.email) ||
137 (commitAuthor.username &&
138 rawAuthor.username &&
139 commitAuthor.username === rawAuthor.username)) || [{}, rawAuthor];
140 const link = await this.hooks.renderChangelogAuthor.promise(data[1], commit, this.options);
141 if (link) {
142 result.add(link);
143 }
144 }));
145 return [...result].join(" ");
146 }
147 /** Transform a commit into a line in the changelog */
148 async generateCommitNote(commit) {
149 var _a;
150 const subject = commit.subject
151 ? commit.subject
152 .split("\n")[0]
153 .trim()
154 .replace("[skip ci]", "\\[skip ci\\]")
155 : "";
156 let pr = "";
157 if ((_a = commit.pullRequest) === null || _a === void 0 ? void 0 : _a.number) {
158 const prLink = url_join_1.default(this.options.baseUrl, "pull", commit.pullRequest.number.toString());
159 pr = `[#${commit.pullRequest.number}](${prLink})`;
160 }
161 const user = await this.createUserLinkList(commit);
162 return `- ${subject}${pr ? ` ${pr}` : ""}${user ? ` (${user})` : ""}`;
163 }
164 /** Get all the authors in the provided commits */
165 getAllAuthors(split) {
166 const commits = Object.values(split).reduce((labeledCommits, sectionCommits) => [...labeledCommits, ...sectionCommits], []);
167 return commits
168 .map((commit) => commit.authors
169 .filter((author) => author.username !== "invalid-email-address" &&
170 (author.name || author.email || author.username))
171 .map((author) => [commit, author]))
172 .reduce((all, more) => [...all, ...more], [])
173 .sort((a) => ("id" in a[1] ? 0 : 1));
174 }
175 /** Create a section in the changelog to showcase contributing authors */
176 async createAuthorSection(sections) {
177 const authors = new Set();
178 const authorsWithFullData = this.authors.map(([, author]) => author).filter((author) => "id" in author);
179 await Promise.all(this.authors.map(async ([commit, author]) => {
180 const info = authorsWithFullData.find((u) => (author.name && u.name === author.name) ||
181 (author.email && u.email === author.email)) || author;
182 const user = await this.hooks.renderChangelogAuthor.promise(info, commit, this.options);
183 const authorEntry = await this.hooks.renderChangelogAuthorLine.promise(info, user);
184 if (authorEntry && !authors.has(authorEntry)) {
185 authors.add(authorEntry);
186 }
187 }));
188 if (authors.size === 0) {
189 return;
190 }
191 let authorSection = `#### Authors: ${authors.size}\n\n`;
192 authorSection += [...authors].sort((a, b) => a.localeCompare(b)).join("\n");
193 sections.push(authorSection);
194 }
195 /** Create a section in the changelog to with all of the changelog notes organized by change type */
196 async createLabelSection(split, sections) {
197 const changelogTitles = this.options.labels.reduce((titles, label) => {
198 if (label.changelogTitle) {
199 titles[label.name] = label.changelogTitle;
200 }
201 return titles;
202 }, {});
203 const labelSections = await Promise.all(Object.entries(split).map(async ([label, labelCommits]) => {
204 const title = await this.hooks.renderChangelogTitle.promise(label, changelogTitles);
205 const lines = new Set();
206 await Promise.all(labelCommits.map(async (commit) => {
207 var _a;
208 const base = ((_a = commit.pullRequest) === null || _a === void 0 ? void 0 : _a.base) || "";
209 const branch = base.includes("/") ? base.split("/")[1] : base;
210 // We want to keep the release notes for a prerelease branch but
211 // omit the changelog item
212 if (branch && this.options.prereleaseBranches.includes(branch)) {
213 return true;
214 }
215 const [, line] = await this.hooks.renderChangelogLine.promise([
216 commit,
217 await this.generateCommitNote(commit),
218 ]);
219 lines.add(line);
220 }));
221 return [
222 title,
223 [...lines].sort((a, b) => a.split("\n").length - b.split("\n").length),
224 ];
225 }));
226 const mergedSections = labelSections.reduce((acc, [title, commits]) => (Object.assign(Object.assign({}, acc), { [title]: [...(acc[title] || []), ...commits] })), {});
227 Object.entries(mergedSections)
228 .map(([title, lines]) => [title, ...lines].join("\n"))
229 .map((section) => sections.push(section));
230 }
231 /** Gather extra release notes to display at the top of the changelog */
232 async createReleaseNotesSection(commits, sections) {
233 if (!commits.length) {
234 return;
235 }
236 let section = "";
237 const visited = new Set();
238 const included = await Promise.all(commits.map(async (commit) => {
239 const omit = await this.hooks.omitReleaseNotes.promise(commit);
240 if (!omit) {
241 return commit;
242 }
243 }));
244 included.forEach((commit) => {
245 if (!commit) {
246 return;
247 }
248 const pr = commit.pullRequest;
249 if (!pr || !pr.body) {
250 return;
251 }
252 const title = /^[#]{0,5}[ ]*[R|r]elease [N|n]otes$/;
253 const lines = pr.body.split("\n").map((line) => line.replace(/\r$/, ""));
254 const notesStart = lines.findIndex((line) => Boolean(line.match(title)));
255 if (notesStart === -1 || visited.has(pr.number)) {
256 return;
257 }
258 const depth = getHeaderDepth(lines[notesStart]);
259 visited.add(pr.number);
260 let notes = "";
261 for (let index = notesStart; index < lines.length; index++) {
262 const line = lines[index];
263 const isTitle = line.match(title);
264 if (line.startsWith("#") && getHeaderDepth(line) <= depth && !isTitle) {
265 break;
266 }
267 if (!isTitle) {
268 notes += `${line}\n`;
269 }
270 }
271 section += `_From #${pr.number}_\n\n${notes.trim()}\n\n`;
272 });
273 if (!section) {
274 return;
275 }
276 sections.push(`### Release Notes\n\n${section}---`);
277 }
278}
279exports.default = Changelog;
280//# sourceMappingURL=changelog.js.map
\No newline at end of file