UNPKG

release-please

Version:

generate release PRs based on the conventionalcommits.org spec

480 lines 23.6 kB
"use strict"; // Copyright 2021 Google LLC // // 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. Object.defineProperty(exports, "__esModule", { value: true }); exports.BaseStrategy = void 0; const manifest_1 = require("../manifest"); const default_1 = require("../versioning-strategies/default"); const default_2 = require("../changelog-notes/default"); const version_1 = require("../version"); const tag_name_1 = require("../util/tag-name"); const logger_1 = require("../util/logger"); const pull_request_title_1 = require("../util/pull-request-title"); const branch_name_1 = require("../util/branch-name"); const pull_request_body_1 = require("../util/pull-request-body"); const composite_1 = require("../updaters/composite"); const generic_1 = require("../updaters/generic"); const generic_json_1 = require("../updaters/generic-json"); const generic_xml_1 = require("../updaters/generic-xml"); const pom_xml_1 = require("../updaters/java/pom-xml"); const generic_yaml_1 = require("../updaters/generic-yaml"); const generic_toml_1 = require("../updaters/generic-toml"); const DEFAULT_CHANGELOG_PATH = 'CHANGELOG.md'; /** * A strategy is responsible for determining which files are * necessary to update in a release pull request. */ class BaseStrategy { constructor(options) { var _a, _b, _c; this.logger = (_a = options.logger) !== null && _a !== void 0 ? _a : logger_1.logger; this.path = options.path || manifest_1.ROOT_PROJECT_PATH; this.github = options.github; this.packageName = options.packageName; this.component = options.component || this.normalizeComponent(this.packageName); this.versioningStrategy = options.versioningStrategy || new default_1.DefaultVersioningStrategy({ logger: this.logger }); this.targetBranch = options.targetBranch; this.repository = options.github.repository; this.changelogPath = options.changelogPath || DEFAULT_CHANGELOG_PATH; this.changelogHost = options.changelogHost; this.changelogSections = options.changelogSections; this.tagSeparator = options.tagSeparator; this.skipGitHubRelease = options.skipGitHubRelease || false; this.releaseAs = options.releaseAs; this.changelogNotes = options.changelogNotes || new default_2.DefaultChangelogNotes(options); this.includeComponentInTag = (_b = options.includeComponentInTag) !== null && _b !== void 0 ? _b : true; this.includeVInTag = (_c = options.includeVInTag) !== null && _c !== void 0 ? _c : true; this.pullRequestTitlePattern = options.pullRequestTitlePattern; this.pullRequestHeader = options.pullRequestHeader; this.pullRequestFooter = options.pullRequestFooter; this.componentNoSpace = options.componentNoSpace; this.extraFiles = options.extraFiles || []; this.initialVersion = options.initialVersion; this.extraLabels = options.extraLabels || []; this.dateFormat = options.dateFormat || generic_1.DEFAULT_DATE_FORMAT; } /** * Return the component for this strategy. This may be a computed field. * @returns {string} */ async getComponent() { if (!this.includeComponentInTag) { return ''; } return this.component || (await this.getDefaultComponent()); } async getDefaultComponent() { var _a; return this.normalizeComponent((_a = this.packageName) !== null && _a !== void 0 ? _a : (await this.getDefaultPackageName())); } async getBranchComponent() { return this.component || (await this.getDefaultComponent()); } async getPackageName() { var _a; return (_a = this.packageName) !== null && _a !== void 0 ? _a : (await this.getDefaultPackageName()); } async getDefaultPackageName() { var _a; return (_a = this.packageName) !== null && _a !== void 0 ? _a : ''; } normalizeComponent(component) { if (!component) { return ''; } return component; } /** * Override this method to post process commits * @param {ConventionalCommit[]} commits parsed commits * @returns {ConventionalCommit[]} modified commits */ async postProcessCommits(commits) { return commits; } async buildReleaseNotes(conventionalCommits, newVersion, newVersionTag, latestRelease, commits) { var _a; return await this.changelogNotes.buildNotes(conventionalCommits, { host: this.changelogHost, owner: this.repository.owner, repository: this.repository.repo, version: newVersion.toString(), previousTag: (_a = latestRelease === null || latestRelease === void 0 ? void 0 : latestRelease.tag) === null || _a === void 0 ? void 0 : _a.toString(), currentTag: newVersionTag.toString(), targetBranch: this.targetBranch, changelogSections: this.changelogSections, commits: commits, }); } async buildPullRequestBody(component, newVersion, releaseNotesBody, _conventionalCommits, _latestRelease, pullRequestHeader, pullRequestFooter) { return new pull_request_body_1.PullRequestBody([ { component, version: newVersion, notes: releaseNotesBody, }, ], { header: pullRequestHeader, footer: pullRequestFooter, }); } /** * Builds a candidate release pull request * @param {Commit[]} commits Raw commits to consider for this release. * @param {Release} latestRelease Optional. The last release for this * component if available. * @param {boolean} draft Optional. Whether or not to create the pull * request as a draft. Defaults to `false`. * @returns {ReleasePullRequest | undefined} The release pull request to * open for this path/component. Returns undefined if we should not * open a pull request. */ async buildReleasePullRequest(commits, latestRelease, draft, labels = [], bumpOnlyOptions) { var _a; const conventionalCommits = await this.postProcessCommits(commits); this.logger.info(`Considering: ${conventionalCommits.length} commits`); if (!bumpOnlyOptions && conventionalCommits.length === 0) { this.logger.info(`No commits for path: ${this.path}, skipping`); return undefined; } const newVersion = (_a = bumpOnlyOptions === null || bumpOnlyOptions === void 0 ? void 0 : bumpOnlyOptions.newVersion) !== null && _a !== void 0 ? _a : (await this.buildNewVersion(conventionalCommits, latestRelease)); const versionsMap = await this.updateVersionsMap(await this.buildVersionsMap(conventionalCommits), conventionalCommits, newVersion); const component = await this.getComponent(); this.logger.debug('component:', component); const newVersionTag = new tag_name_1.TagName(newVersion, this.includeComponentInTag ? component : undefined, this.tagSeparator, this.includeVInTag); this.logger.debug('pull request title pattern:', this.pullRequestTitlePattern); this.logger.debug('componentNoSpace:', this.componentNoSpace); const pullRequestTitle = pull_request_title_1.PullRequestTitle.ofComponentTargetBranchVersion(component || '', this.targetBranch, newVersion, this.pullRequestTitlePattern, this.componentNoSpace); const branchComponent = await this.getBranchComponent(); const branchName = branchComponent ? branch_name_1.BranchName.ofComponentTargetBranch(branchComponent, this.targetBranch) : branch_name_1.BranchName.ofTargetBranch(this.targetBranch); const releaseNotesBody = await this.buildReleaseNotes(conventionalCommits, newVersion, newVersionTag, latestRelease, commits); if (!bumpOnlyOptions && this.changelogEmpty(releaseNotesBody)) { this.logger.info(`No user facing commits found since ${latestRelease ? latestRelease.sha : 'beginning of time'} - skipping`); return undefined; } const updates = await this.buildUpdates({ changelogEntry: releaseNotesBody, newVersion, versionsMap, latestVersion: latestRelease === null || latestRelease === void 0 ? void 0 : latestRelease.tag.version, commits: conventionalCommits, }); const updatesWithExtras = (0, composite_1.mergeUpdates)(updates.concat(...(await this.extraFileUpdates(newVersion, versionsMap, this.dateFormat)))); const pullRequestBody = await this.buildPullRequestBody(component, newVersion, releaseNotesBody, conventionalCommits, latestRelease, this.pullRequestHeader, this.pullRequestFooter); return { title: pullRequestTitle, body: pullRequestBody, updates: updatesWithExtras, labels: [...labels, ...this.extraLabels], headRefName: branchName.toString(), version: newVersion, draft: draft !== null && draft !== void 0 ? draft : false, }; } // Helper to convert extra files with globs to the file paths to add async extraFilePaths(extraFile) { if (typeof extraFile !== 'object') { return [extraFile]; } if (!extraFile.glob) { return [extraFile.path]; } if (extraFile.path.startsWith('/')) { // glob is relative to root, strip the leading `/` for glob matching // and re-add the leading `/` to make the file relative to the root return (await this.github.findFilesByGlobAndRef(extraFile.path.slice(1), this.targetBranch)).map(file => `/${file}`); } else if (this.path === manifest_1.ROOT_PROJECT_PATH) { // root component, ignore path prefix return this.github.findFilesByGlobAndRef(extraFile.path, this.targetBranch); } else { // glob is relative to current path return this.github.findFilesByGlobAndRef(extraFile.path, this.targetBranch, this.path); } } async extraFileUpdates(version, versionsMap, dateFormat) { const extraFileUpdates = []; for (const extraFile of this.extraFiles) { if (typeof extraFile === 'object') { const paths = await this.extraFilePaths(extraFile); for (const path of paths) { switch (extraFile.type) { case 'generic': extraFileUpdates.push({ path: this.addPath(path), createIfMissing: false, updater: new generic_1.Generic({ version, versionsMap, dateFormat: dateFormat, }), }); break; case 'json': extraFileUpdates.push({ path: this.addPath(path), createIfMissing: false, updater: new generic_json_1.GenericJson(extraFile.jsonpath, version), }); break; case 'yaml': extraFileUpdates.push({ path: this.addPath(path), createIfMissing: false, updater: new generic_yaml_1.GenericYaml(extraFile.jsonpath, version), }); break; case 'toml': extraFileUpdates.push({ path: this.addPath(path), createIfMissing: false, updater: new generic_toml_1.GenericToml(extraFile.jsonpath, version), }); break; case 'xml': extraFileUpdates.push({ path: this.addPath(path), createIfMissing: false, updater: new generic_xml_1.GenericXml(extraFile.xpath, version), }); break; case 'pom': extraFileUpdates.push({ path: this.addPath(path), createIfMissing: false, updater: new pom_xml_1.PomXml(version), }); break; default: throw new Error(`unsupported extraFile type: ${extraFile.type}`); } } } else if (extraFile.endsWith('.json')) { extraFileUpdates.push({ path: this.addPath(extraFile), createIfMissing: false, updater: new composite_1.CompositeUpdater(new generic_json_1.GenericJson('$.version', version), new generic_1.Generic({ version, versionsMap, dateFormat: dateFormat })), }); } else if (extraFile.endsWith('.yaml') || extraFile.endsWith('.yml')) { extraFileUpdates.push({ path: this.addPath(extraFile), createIfMissing: false, updater: new composite_1.CompositeUpdater(new generic_yaml_1.GenericYaml('$.version', version), new generic_1.Generic({ version, versionsMap, dateFormat: dateFormat })), }); } else if (extraFile.endsWith('.toml')) { extraFileUpdates.push({ path: this.addPath(extraFile), createIfMissing: false, updater: new composite_1.CompositeUpdater(new generic_toml_1.GenericToml('$.version', version), new generic_1.Generic({ version, versionsMap, dateFormat: dateFormat })), }); } else if (extraFile.endsWith('.xml')) { extraFileUpdates.push({ path: this.addPath(extraFile), createIfMissing: false, updater: new composite_1.CompositeUpdater( // Updates "version" element that is a child of the root element. new generic_xml_1.GenericXml('/*/version', version), new generic_1.Generic({ version, versionsMap, dateFormat: dateFormat })), }); } else { extraFileUpdates.push({ path: this.addPath(extraFile), createIfMissing: false, updater: new generic_1.Generic({ version, versionsMap, dateFormat: dateFormat }), }); } } return extraFileUpdates; } changelogEmpty(changelogEntry) { return changelogEntry.split('\n').length <= 1; } async updateVersionsMap(versionsMap, conventionalCommits, _newVersion) { for (const [component, version] of versionsMap.entries()) { versionsMap.set(component, await this.versioningStrategy.bump(version, conventionalCommits)); } return versionsMap; } async buildNewVersion(conventionalCommits, latestRelease) { if (this.releaseAs) { this.logger.warn(`Setting version for ${this.path} from release-as configuration`); return version_1.Version.parse(this.releaseAs); } const releaseAsCommit = conventionalCommits.find(conventionalCommit => conventionalCommit.notes.find(note => note.title === 'RELEASE AS')); if (releaseAsCommit) { const note = releaseAsCommit.notes.find(note => note.title === 'RELEASE AS'); if (note) { return version_1.Version.parse(note.text); } } if (latestRelease) { return await this.versioningStrategy.bump(latestRelease.tag.version, conventionalCommits); } return this.initialReleaseVersion(); } async buildVersionsMap(_conventionalCommits) { return new Map(); } async parsePullRequestBody(pullRequestBody) { return pull_request_body_1.PullRequestBody.parse(pullRequestBody, this.logger); } /** * Given a merged pull request, build the candidate release. * @param {PullRequest} mergedPullRequest The merged release pull request. * @returns {Release} The candidate release. * @deprecated Use buildReleases() instead. */ async buildRelease(mergedPullRequest, options) { var _a; if (this.skipGitHubRelease) { this.logger.info('Release skipped from strategy config'); return; } if (!mergedPullRequest.sha) { this.logger.error('Pull request should have been merged'); return; } const mergedTitlePattern = (_a = options === null || options === void 0 ? void 0 : options.groupPullRequestTitlePattern) !== null && _a !== void 0 ? _a : manifest_1.MANIFEST_PULL_REQUEST_TITLE_PATTERN; const pullRequestTitle = pull_request_title_1.PullRequestTitle.parse(mergedPullRequest.title, this.pullRequestTitlePattern, this.componentNoSpace, this.logger) || pull_request_title_1.PullRequestTitle.parse(mergedPullRequest.title, mergedTitlePattern, this.componentNoSpace, this.logger); if (!pullRequestTitle) { this.logger.error(`Bad pull request title: '${mergedPullRequest.title}'`); return; } const branchName = branch_name_1.BranchName.parse(mergedPullRequest.headBranchName, this.logger); if (!branchName) { this.logger.error(`Bad branch name: ${mergedPullRequest.headBranchName}`); return; } const pullRequestBody = await this.parsePullRequestBody(mergedPullRequest.body); if (!pullRequestBody) { this.logger.error('Could not parse pull request body as a release PR'); return; } const component = await this.getComponent(); let releaseData; if (pullRequestBody.releaseData.length === 1 && !pullRequestBody.releaseData[0].component) { const branchComponent = await this.getBranchComponent(); // standalone release PR, ensure the components match if (this.normalizeComponent(branchName.component) !== this.normalizeComponent(branchComponent)) { this.logger.warn(`PR component: ${branchName.component} does not match configured component: ${branchComponent}`); return; } releaseData = pullRequestBody.releaseData[0]; } else { // manifest release with multiple components - find the release notes // for the component to see if it was included in this release (parsed // from the release pull request body) releaseData = pullRequestBody.releaseData.find(datum => { return (this.normalizeComponent(datum.component) === this.normalizeComponent(component)); }); if (!releaseData && pullRequestBody.releaseData.length > 0) { this.logger.info(`Pull request contains releases, but not for component: ${component}`); return; } } const notes = releaseData === null || releaseData === void 0 ? void 0 : releaseData.notes; if (notes === undefined) { this.logger.warn('Failed to find release notes'); } let version = pullRequestTitle.getVersion(); if (!version || (pullRequestBody.releaseData.length > 1 && (releaseData === null || releaseData === void 0 ? void 0 : releaseData.version))) { // prioritize pull-request body version for multi-component releases version = releaseData === null || releaseData === void 0 ? void 0 : releaseData.version; } if (!version) { this.logger.error('Pull request should have included version'); return; } if (!this.isPublishedVersion(version)) { this.logger.warn(`Skipping non-published version: ${version.toString()}`); return; } const tag = new tag_name_1.TagName(version, this.includeComponentInTag ? component : undefined, this.tagSeparator, this.includeVInTag); const releaseName = component && this.includeComponentInTag ? `${component}: v${version.toString()}` : `v${version.toString()}`; return { name: releaseName, tag, notes: notes || '', sha: mergedPullRequest.sha, }; } /** * Given a merged pull request, build the candidate releases. * @param {PullRequest} mergedPullRequest The merged release pull request. * @returns {Release} The candidate release. */ async buildReleases(mergedPullRequest, options) { const release = await this.buildRelease(mergedPullRequest, options); if (release) { return [release]; } return []; } isPublishedVersion(_version) { return true; } /** * Override this to handle the initial version of a new library. */ initialReleaseVersion() { if (this.initialVersion) { return version_1.Version.parse(this.initialVersion); } return version_1.Version.parse('1.0.0'); } /** * Adds a given file path to the strategy path. * @param {string} file Desired file path. * @returns {string} The file relative to the strategy. * @throws {Error} If the file path contains relative pathing characters, i.e. ../, ~/ */ addPath(file) { // There is no strategy path to join, the strategy is at the root, or the // file is at the root (denoted by a leading slash or tilde) if (!this.path || this.path === manifest_1.ROOT_PROJECT_PATH || file.startsWith('/')) { file = file.replace(/^\/+/, ''); } // Otherwise, the file is relative to the strategy path else { file = `${this.path.replace(/\/+$/, '')}/${file}`; } // Ensure the file path does not escape the workspace if (/((^|\/)\.{1,2}|^~|^\/*)+\//.test(file)) { throw new Error(`illegal pathing characters in path: ${file}`); } // Strip any trailing slashes and return return file.replace(/\/+$/, ''); } } exports.BaseStrategy = BaseStrategy; //# sourceMappingURL=base.js.map