@process-engine/ci_tools
Version:
CI tools for process-engine.io
241 lines (234 loc) • 11.8 kB
JavaScript
Object.defineProperty(exports, "__esModule", { value: true });
exports.getChangelogText = exports.run = void 0;
const chalk = require("chalk");
const cross_fetch_1 = require("cross-fetch");
const moment = require("moment");
const yargsParser = require("yargs-parser");
const pull_requests_1 = require("../../github/pull_requests");
const git_1 = require("../../git/git");
const git_helpers_1 = require("../../versions/git_helpers");
const parse_version_1 = require("../../versions/parse_version");
const GITHUB_REPO = (0, git_1.getCurrentRepoNameWithOwner)();
const COMMIT_API_URI = (0, git_1.getCurrentApiBaseUrlWithAuth)('/commits/:commit_sha');
const ISSUES_API_URI = (0, git_1.getCurrentApiBaseUrlWithAuth)('/issues?state=closed&since=:since&page=:page');
const BADGE = '[create-changelog]\t';
const DEFAULT_MODE = 'node';
// A typical release cycle lasts for roughly half a year. A few Additional weeks are applied as a buffer.
const CONSIDER_PULL_REQUESTS_WEEKS_BACK = 30;
/**
* Creates a changelog based on data available in Git and GitHub:
*
* - Git: latest commits and tags
* - GitHub: PRs and Issues
*/
async function run(...args) {
const argv = yargsParser(args, { alias: { help: ['h'] }, default: { mode: DEFAULT_MODE } });
const mode = argv.mode;
let startRef = args[0];
if (!startRef) {
startRef = await (0, git_helpers_1.getPrevVersionTag)(mode);
console.log(`${BADGE}No start ref given, using: "${startRef}"`);
}
const changelogText = await getChangelogText(mode, startRef);
console.log(changelogText);
return true;
}
exports.run = run;
async function getChangelogText(mode, startRef) {
if (startRef == null) {
return '';
}
const apiResponse = await getCommitFromApi(startRef);
if (apiResponse.commit == null) {
console.error(chalk.red(`${BADGE} API responded with: ${apiResponse.message}`));
process.exit(3);
}
const defaultStartDate = moment().subtract(CONSIDER_PULL_REQUESTS_WEEKS_BACK, 'weeks');
const startCommitDate = apiResponse.commit?.committer?.date ?? defaultStartDate;
const startDate = moment(startCommitDate).subtract(CONSIDER_PULL_REQUESTS_WEEKS_BACK, 'weeks').toISOString();
const endRef = 'HEAD';
const nextVersion = await (0, git_helpers_1.getNextVersion)(mode);
if (nextVersion == null) {
console.error(chalk.red(`${BADGE}Could not determine nextVersion!`));
process.exit(3);
}
const nextVersionTag = (0, git_helpers_1.getVersionTag)(nextVersion);
printInfo(startRef, startDate, endRef, nextVersion, nextVersionTag);
const mergedPullRequestsSince = await (0, pull_requests_1.getMergedPullRequests)(startDate);
const mergedPullRequests = filterPullRequestsForBranch(mergedPullRequestsSince, '', startRef, startDate);
console.log(`${BADGE}Found ${mergedPullRequests.length} merged pull requests since the last stable release`);
const closedIssuesSince = await getClosedIssuesFromApi(startDate);
const issuesClosedByPullRequest = closedIssuesSince.filter((issue) => {
const pullRequestClosingThisIssue = mergedPullRequests.find((pr) => pr.closedIssueNumbers.indexOf(issue.number) !== -1);
return pullRequestClosingThisIssue != null;
});
console.log(`${BADGE}Found ${issuesClosedByPullRequest.length} closed issues since the last stable release`);
const previousPreVersions = getPreviousPreVersionsInReleaseChannel(nextVersion);
const currentPreVersion = { tag: nextVersionTag, date: new Date() };
const preVersions = [currentPreVersion].concat(previousPreVersions);
const shouldShowPreviousPreVersions = previousPreVersions.length > 0;
const groupedPullRequests = groupPullRequestsByPreVersion(mergedPullRequests, preVersions);
const mergedPullRequestsText = groupedPullRequests
.map((pullRequestGroup, pullRequestGroupIndex) => {
const preVersion = pullRequestGroup.preVersion;
let text = '';
if (shouldShowPreviousPreVersions) {
const header = pullRequestGroupIndex === 0
? `${preVersion.tag} (this release)`
: `[${preVersion.tag}](https://github.com/${GITHUB_REPO}/releases/tag/${preVersion.tag})`;
text += `**${header}**\n\n`;
}
for (const pr of pullRequestGroup.pullRequests) {
const mergedAtDate = moment(pr.mergedAt);
const mergedAtString = mergedAtDate.format('YYYY-MM-DD');
const title = ensureSpaceAfterLeadingEmoji(pr.title);
let breakingPrefix = '';
if (pr.isBreakingChange && !title.toLowerCase().includes('breaking')) {
breakingPrefix = `**BREAKING CHANGE:** `;
}
text += `- ${breakingPrefix}#${pr.number} ${title} (merged ${mergedAtString})\n`;
}
if (pullRequestGroup.pullRequests.length === 0) {
text += '- none\n';
}
return text;
})
.join('\n');
const issuesClosedByPullRequestText = issuesClosedByPullRequest
.map((issue) => {
const title = ensureSpaceAfterLeadingEmoji(issue.title);
let breakingPrefix = '';
if (issue.isBreakingChange && !title.toLowerCase().includes('breaking')) {
breakingPrefix = `**BREAKING CHANGE:** `;
}
return `- ${breakingPrefix}#${issue.number} ${title}`;
})
.join('\n');
const now = moment();
const changelogText = `
# Changelog ${nextVersionTag} (${now.format('YYYY-MM-DD')})
This changelog covers the changes between [${startRef} and ${nextVersionTag}](https://github.com/${GITHUB_REPO}/compare/${startRef}...${nextVersionTag}).
For further reference, please refer to the changelog of the previous version, [${startRef}](https://github.com/${GITHUB_REPO}/releases/tag/${startRef}).
## Merged Pull Requests
${mergedPullRequestsText || '- none'}
## Corresponding Issues
${issuesClosedByPullRequestText || '- none'}
`.trim();
return changelogText;
}
exports.getChangelogText = getChangelogText;
function getPreviousPreVersionsInReleaseChannel(nextVersion) {
const nextVersionParsed = (0, parse_version_1.parseVersion)(nextVersion);
const releasechannel = nextVersionParsed.releaseChannelName;
const addPreviousPreReleases = releasechannel === 'beta' || releasechannel === 'alpha';
if (!addPreviousPreReleases) {
return [];
}
const nextVersionTag = (0, git_helpers_1.getVersionTag)(nextVersion);
const versionWithoutReleaseChannelNumber = nextVersionTag.replace(/\d$/, '');
const gitTagList = (0, git_1.getGitTagList)();
return gitTagList
.split('\n')
.filter((line) => line.startsWith(versionWithoutReleaseChannelNumber))
.map((tag) => {
return {
tag: tag,
date: moment((0, git_1.getGitTagDate)(tag)).toDate(),
};
});
}
async function getCommitFromApi(ref) {
const url = COMMIT_API_URI.replace(':commit_sha', ref);
const response = await (0, cross_fetch_1.default)(url);
return response.json();
}
async function getClosedIssuesFromApi(since, page = 1) {
const url = ISSUES_API_URI.replace(':since', since).replace(':page', page.toString());
const response = await (0, cross_fetch_1.default)(url);
const issues = (await response.json());
const relevantIssues = issues
.filter((issue) => !issue.pull_request)
.map((issue) => {
const isBreakingChange = issue.labels.find((label) => label.name.toLowerCase().includes('breaking'));
return {
...issue,
isBreakingChange,
};
});
if (relevantIssues.length > 0) {
const nextPageIssues = await getClosedIssuesFromApi(since, page + 1);
return [...relevantIssues].concat(nextPageIssues);
}
return relevantIssues;
}
function printInfo(startRef, startDate, endRef, nextVersion, nextVersionTag) {
console.log(`${BADGE}startRef:`, startRef);
console.log(`${BADGE}startDate:`, startDate);
console.log(`${BADGE}endRef:`, endRef);
console.log(`${BADGE}nextVersion:`, nextVersion);
console.log(`${BADGE}nextVersionTag:`, nextVersionTag);
console.log('');
}
function ensureSpaceAfterLeadingEmoji(text) {
const emojiWithoutTrailingSpaceRegex = /([\u{1f300}-\u{1f5ff}\u{1f900}-\u{1f9ff}\u{1f600}-\u{1f64f}\u{1f680}-\u{1f6ff}\u{2600}-\u{26ff}\u{2700}-\u{27bf}\u{1f1e6}-\u{1f1ff}\u{1f191}-\u{1f251}\u{1f004}\u{1f0cf}\u{1f170}-\u{1f171}\u{1f17e}-\u{1f17f}\u{1f18e}\u{3030}\u{2b50}\u{2b55}\u{2934}-\u{2935}\u{2b05}-\u{2b07}\u{2b1b}-\u{2b1c}\u{3297}\u{3299}\u{303d}\u{00a9}\u{00ae}\u{2122}\u{23f3}\u{24c2}\u{23e9}-\u{23ef}\u{25b6}\u{23f8}-\u{23fa}])(\S)/gu;
return text.replace(emojiWithoutTrailingSpaceRegex, (substring, emojiMatch, characterAfterEmojiMatch) => {
return `${emojiMatch} ${characterAfterEmojiMatch}`;
});
}
function filterPullRequestsForBranch(prs, branchName, startRef, since) {
const allShaInCurrentBranch = (0, git_1.getGitCommitListSince)(branchName, since).split('\n');
const allShaInStartRef = (0, git_1.getGitCommitListSince)(startRef, since).split('\n');
const newShaInCurrentBranch = allShaInCurrentBranch.filter((currentSha) => allShaInStartRef.indexOf(currentSha) === -1);
const filteredPrs = prs.filter((pr) => newShaInCurrentBranch.indexOf(pr.headSha) !== -1 || newShaInCurrentBranch.indexOf(pr.mergeCommitSha) !== -1);
return filteredPrs;
}
function groupPullRequestsByPreVersion(pullRequests, preVersions) {
const groupedPullRequests = [];
const sortedPullRequests = sortPullRequestsByMergeDateDescending(pullRequests);
const sortedPreVersions = sortPreVersionsByDateDescending(preVersions);
let currentPreVersionIndex = 0;
let currentPreVersion = sortedPreVersions[currentPreVersionIndex];
let nextPreVersion = sortedPreVersions[currentPreVersionIndex + 1];
let pullRequestsForCurrentPreVersion = [];
let groupForCurrentPreVersionExists = false;
for (const pullRequest of sortedPullRequests) {
if (currentPreVersion == null) {
break;
}
if (!groupForCurrentPreVersionExists) {
const pullRequestGroup = {
preVersion: currentPreVersion,
pullRequests: pullRequestsForCurrentPreVersion,
};
groupedPullRequests[currentPreVersionIndex] = pullRequestGroup;
groupForCurrentPreVersionExists = true;
}
const mergedAtDate = moment(pullRequest.mergedAt);
const introducedInCurrentVersion = nextPreVersion != null
? mergedAtDate.isBefore(currentPreVersion.date) && mergedAtDate.isAfter(nextPreVersion.date)
: mergedAtDate.isBefore(currentPreVersion.date);
if (!introducedInCurrentVersion) {
currentPreVersionIndex++;
currentPreVersion = sortedPreVersions[currentPreVersionIndex];
nextPreVersion = sortedPreVersions[currentPreVersionIndex + 1];
pullRequestsForCurrentPreVersion = [];
groupForCurrentPreVersionExists = false;
}
pullRequestsForCurrentPreVersion.push(pullRequest);
}
return groupedPullRequests;
}
function sortPreVersionsByDateDescending(preVersions) {
const clonedPreVersions = [...preVersions];
return clonedPreVersions.sort((preVersion1, preVersion2) => {
return preVersion2.date.getTime() - preVersion1.date.getTime();
});
}
function sortPullRequestsByMergeDateDescending(pullRequests) {
const clonedPullRequests = [...pullRequests];
return clonedPullRequests.sort((pullRequest1, pullRequest2) => {
return moment(pullRequest2.mergedAt).diff(moment(pullRequest1.mergedAt));
});
}
//# sourceMappingURL=create-changelog.js.map
;