@atomist/sdm-pack-changelog
Version:
Extension Pack for an Atomist SDM to manage changelogs
231 lines (223 loc) • 9.45 kB
JavaScript
;
/*
* Copyright © 2018 Atomist, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
};
Object.defineProperty(exports, "__esModule", { value: true });
const automation_client_1 = require("@atomist/automation-client");
const fs = require("fs-extra");
const _ = require("lodash");
const path = require("path");
const util_1 = require("util");
const changelogLabels_1 = require("../handler/command/changelogLabels");
const parseChangelog = require("./changelogParser");
exports.ChangelogTemplate = `# Changelog
All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](http://keepachangelog.com/)
and this project adheres to [Semantic Versioning](http://semver.org/).
## [0.0.0]
`;
/**
* Add entry to changelog for closed label or pull request
* @param {ClosedIssueWithChangelogLabel.Issue} issue
* @param {string} token
* @returns {Promise<HandlerResult>}
*/
function addChangelogEntryForClosedIssue(issue, token) {
return __awaiter(this, void 0, void 0, function* () {
const p = yield automation_client_1.GitCommandGitProject.cloned({ token }, automation_client_1.GitHubRepoRef.from({ owner: issue.repo.owner, repo: issue.repo.name }));
const url = `https://github.com/${issue.repo.owner}/${issue.repo.name}/issues/${issue.number}`;
const categories = issue.labels.filter(l => l.name.startsWith("changelog:")).map(l => l.name.split(":")[1]);
const qualifiers = issue.labels.some(l => l.name.toLocaleLowerCase() === "breaking") ? ["breaking"] : [];
const entry = {
title: issue.title,
label: `#${issue.number.toString()}`,
url,
qualifiers,
};
yield updateChangelog(p, categories, entry);
return automation_client_1.Success;
});
}
exports.addChangelogEntryForClosedIssue = addChangelogEntryForClosedIssue;
/**
* Add entry to changelog for commits
* @param {PushWithChangelogLabel.Push} commit
* @param {string} token
* @returns {Promise<HandlerResult>}
*/
function addChangelogEntryForCommit(push, token) {
return __awaiter(this, void 0, void 0, function* () {
const p = yield automation_client_1.GitCommandGitProject.cloned({ token }, automation_client_1.GitHubRepoRef.from({ owner: push.repo.owner, repo: push.repo.name, branch: push.branch }));
for (const commit of push.commits) {
const categories = [];
changelogLabels_1.ChangelogLabels.forEach(l => {
if (commit.message.toLowerCase().includes(`[changelog:${l}]`)) {
categories.push(l);
}
});
const entry = {
title: commit.message.split("\n")[0],
label: commit.sha.slice(0, 7),
url: `https://github.com/${push.repo.owner}/${push.repo.name}/commit/${commit.sha}`,
qualifiers: [],
};
if (categories.length > 0) {
yield updateChangelog(p, categories, entry);
}
}
return automation_client_1.Success;
});
}
exports.addChangelogEntryForCommit = addChangelogEntryForCommit;
function updateChangelog(p, categories, entry) {
return __awaiter(this, void 0, void 0, function* () {
const cl = yield p.getFile("CHANGELOG.md");
if (cl) {
// If changelog exists make sure it doesn't already contain the label
const content = yield cl.getContent();
if (!content.includes(entry.url)) {
yield updateAndWriteChangelog(p, categories, entry);
}
}
else {
yield updateAndWriteChangelog(p, categories, entry);
}
if (!(yield p.isClean())) {
yield p.commit(`Changelog: ${entry.label} to ${categories.join(", ")}
[atomist:generated]`);
yield p.push();
}
});
}
function updateAndWriteChangelog(p, categories, entry) {
return __awaiter(this, void 0, void 0, function* () {
let changelog = yield readChangelog(p);
for (const category of categories) {
changelog = addEntryToChangelog(Object.assign(Object.assign({}, entry), { category }), changelog, p);
}
return writeChangelog(changelog, p);
});
}
/**
* Read and parse the project changelog. If the changelog has
* reference links, this function will rewrite the file with inline
* links. If the project does not have a changelog, one will be
* added.
*
* @param p project with changelog file, or not
* @return an object representing the parsed changelog
*/
function readChangelog(p) {
return __awaiter(this, void 0, void 0, function* () {
const changelogFile = path.join(p.baseDir, "CHANGELOG.md");
if (!fs.existsSync(changelogFile)) {
yield fs.writeFile(changelogFile, exports.ChangelogTemplate);
}
// Inline links as we would otherwise lose them
const remark = require("remark");
const links = require("remark-inline-links");
const pr = util_1.promisify(remark().use(links).process);
const inlined = yield pr(yield fs.readFile(changelogFile));
yield fs.writeFile(changelogFile, inlined.contents);
return parseChangelog(changelogFile);
});
}
exports.readChangelog = readChangelog;
function addEntryToChangelog(entry, cl, p) {
const version = readUnreleasedVersion(cl, p);
// Add the entry to the correct section
const category = _.upperFirst(entry.category || "changed");
const qualifiers = (entry.qualifiers || []).map(q => `**${q.toLocaleUpperCase()}**`).join(" ");
const title = entry.title.endsWith(".") ? entry.title : `${entry.title}.`;
const prefix = (qualifiers && qualifiers.length > 0) ? `${qualifiers} ` : "";
const line = `- ${prefix}${title} [${entry.label}](${entry.url})`;
if (version.parsed[category]) {
version.parsed[category].push(line);
}
else {
version.parsed[category] = [line];
}
return cl;
}
exports.addEntryToChangelog = addEntryToChangelog;
/**
* Convert changelog object back to a string.
*
* @param changelog parsed changelog object
* @return markdown formatted changelog file contents
*/
function changelogToString(changelog) {
let content = `# ${changelog.title}`;
if (changelog.description) {
content = `${content}
${changelog.description}`;
}
(changelog.versions || []).filter((v) => v.version !== "0.0.0").forEach((v) => {
content += `
## ${v.title}`;
const keys = Object.keys(v.parsed)
.filter(k => k !== "_")
.sort((k1, k2) => changelogLabels_1.ChangelogLabels.indexOf(k1.toLocaleLowerCase()) - changelogLabels_1.ChangelogLabels.indexOf(k2.toLocaleLowerCase()));
for (const category of keys) {
content += `
### ${category}
${v.parsed[category].join("\n")}`;
}
});
return content + "\n";
}
exports.changelogToString = changelogToString;
/**
* Write changelog back out to the CHANGELOG.md file
* @param changelog
* @param {GitProject} p
* @returns {Promise<void>}
*/
function writeChangelog(changelog, p) {
return __awaiter(this, void 0, void 0, function* () {
const content = changelogToString(changelog);
const changelogFile = path.join(p.baseDir, "CHANGELOG.md");
return fs.writeFile(changelogFile, content);
});
}
exports.writeChangelog = writeChangelog;
function readUnreleasedVersion(cl, p) {
let version;
// Get Unreleased section or create if not already available
if (cl && cl.versions && cl.versions.length > 0
// This github.com version is really odd. Not sure what the parser thinks here
&& (!cl.versions[0].version || cl.versions[0].version === "github.com")) {
version = cl.versions[0];
}
else {
version = {
title: `[Unreleased](https://github.com/${p.id.owner}/${p.id.repo}/${cl.versions && cl.versions.filter((v) => v.version !== "0.0.0").length > 0 ?
`compare/${cl.versions[0].version}...HEAD` : "tree/HEAD"})`,
parsed: {},
};
cl.versions = [version, ...cl.versions];
}
return version;
}
//# sourceMappingURL=changelog.js.map