UNPKG

release-please

Version:

generate release PRs based on the conventionalcommits.org spec

691 lines 27.3 kB
#!/usr/bin/env node "use strict"; // Copyright 2019 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.handleError = exports.parser = void 0; const coerce_option_1 = require("../util/coerce-option"); const yargs = require("yargs"); const github_1 = require("../github"); const manifest_1 = require("../manifest"); const changelog_notes_1 = require("../changelog-notes"); const logger_1 = require("../util/logger"); const factory_1 = require("../factory"); const bootstrapper_1 = require("../bootstrapper"); const diff_1 = require("diff"); // eslint-disable-next-line @typescript-eslint/no-var-requires const parseGithubRepoUrl = require('parse-github-repo-url'); function gitHubOptions(yargs) { return yargs .option('token', { describe: 'GitHub token with repo write permissions' }) .option('api-url', { describe: 'URL to use when making API requests', default: github_1.GH_API_URL, type: 'string', }) .option('graphql-url', { describe: 'URL to use when making GraphQL requests', default: github_1.GH_GRAPHQL_URL, type: 'string', }) .option('default-branch', { describe: 'The branch to open release PRs against and tag releases on', type: 'string', deprecated: 'use --target-branch instead', }) .option('target-branch', { describe: 'The branch to open release PRs against and tag releases on', type: 'string', }) .option('repo-url', { describe: 'GitHub URL to generate release for', demand: true, }) .option('dry-run', { describe: 'Prepare but do not take action', type: 'boolean', default: false, }) .middleware(_argv => { const argv = _argv; // allow secrets to be loaded from file path // rather than being passed directly to the bin. if (argv.token) argv.token = (0, coerce_option_1.coerceOption)(argv.token); if (argv.apiUrl) argv.apiUrl = (0, coerce_option_1.coerceOption)(argv.apiUrl); if (argv.graphqlUrl) argv.graphqlUrl = (0, coerce_option_1.coerceOption)(argv.graphqlUrl); }); } function releaseOptions(yargs) { return yargs .option('draft', { describe: 'mark release as a draft. no tag is created but tag_name and ' + 'target_commitish are associated with the release for future ' + 'tag creation upon "un-drafting" the release.', type: 'boolean', default: false, }) .option('prerelease', { describe: 'mark release that have prerelease versions ' + 'as as a prerelease on Github', type: 'boolean', default: false, }) .option('label', { default: 'autorelease: pending', describe: 'comma-separated list of labels to remove to from release PR', }) .option('release-label', { describe: 'set a pull request label other than "autorelease: tagged"', default: 'autorelease: tagged', type: 'string', }) .option('snapshot-label', { describe: 'set a java snapshot pull request label other than "autorelease: snapshot"', default: 'autorelease: snapshot', type: 'string', }); } function pullRequestOptions(yargs) { // common to ReleasePR and GitHubRelease return yargs .option('label', { default: 'autorelease: pending', describe: 'comma-separated list of labels to add to from release PR', }) .option('skip-labeling', { describe: 'skip application of labels to pull requests', type: 'boolean', default: false, }) .option('fork', { describe: 'should the PR be created from a fork', type: 'boolean', default: false, }) .option('draft-pull-request', { describe: 'mark pull request as a draft', type: 'boolean', default: false, }) .option('signoff', { describe: 'Add Signed-off-by line at the end of the commit log message using the user and email provided. (format "Name <email@example.com>").', type: 'string', }); } function pullRequestStrategyOptions(yargs) { return yargs .option('release-as', { describe: 'override the semantically determined release version', type: 'string', }) .option('bump-minor-pre-major', { describe: 'should we bump the semver minor prior to the first major release', default: false, type: 'boolean', }) .option('bump-patch-for-minor-pre-major', { describe: 'should we bump the semver patch instead of the minor for non-breaking' + ' changes prior to the first major release', default: false, type: 'boolean', }) .option('prerelease-type', { describe: 'type of the prerelease, e.g., alpha', type: 'string', }) .option('extra-files', { describe: 'extra files for the strategy to consider', type: 'string', coerce(arg) { if (arg) { return arg.split(','); } return arg; }, }) .option('version-file', { describe: 'path to version file to update, e.g., version.rb', type: 'string', }) .option('snapshot', { describe: 'is it a snapshot (or pre-release) being generated?', type: 'boolean', default: false, }) .option('versioning-strategy', { describe: 'strategy used for bumping versions', choices: (0, factory_1.getVersioningStrategyTypes)(), default: 'default', }) .option('changelog-path', { default: 'CHANGELOG.md', describe: 'where can the CHANGELOG be found in the project?', type: 'string', }) .option('changelog-type', { describe: 'type of changelog to build', choices: (0, factory_1.getChangelogTypes)(), }) .option('changelog-sections', { describe: 'comma-separated list of scopes to include in the changelog', type: 'string', coerce: (arg) => { if (arg) { return (0, changelog_notes_1.buildChangelogSections)(arg.split(',')); } return arg; }, }) .option('changelog-host', { describe: 'host for hyperlinks in the changelog', type: 'string', }) .option('last-package-version', { describe: 'last version # that package was released as', type: 'string', deprecated: 'use --latest-tag-version instead', }) .option('latest-tag-version', { describe: 'Override the detected latest tag version', type: 'string', }) .option('latest-tag-sha', { describe: 'Override the detected latest tag SHA', type: 'string', }) .option('latest-tag-name', { describe: 'Override the detected latest tag name', type: 'string', }) .option('date-format', { describe: 'format in strftime format for updating dates', type: 'string', }) .middleware(_argv => { const argv = _argv; if (argv.defaultBranch) { logger_1.logger.warn('--default-branch is deprecated. Please use --target-branch instead.'); argv.targetBranch = argv.targetBranch || argv.defaultBranch; } if (argv.lastPackageVersion) { logger_1.logger.warn('--latest-package-version is deprecated. Please use --latest-tag-version instead.'); argv.latestTagVersion = argv.latestTagVersion || argv.lastPackageVersion; } }); } function manifestConfigOptions(yargs, defaultType) { return yargs .option('path', { describe: 'release from path other than root directory', type: 'string', }) .option('component', { describe: 'name of component release is being minted for', type: 'string', }) .option('package-name', { describe: 'name of package release is being minted for', type: 'string', }) .option('release-type', { describe: 'what type of repo is a release being created for?', choices: (0, factory_1.getReleaserTypes)(), default: defaultType, }); } function manifestOptions(yargs) { return yargs .option('config-file', { default: 'release-please-config.json', describe: 'where can the config file be found in the project?', }) .option('manifest-file', { default: '.release-please-manifest.json', describe: 'where can the manifest file be found in the project?', }); } function taggingOptions(yargs) { return yargs .option('include-v-in-tags', { describe: 'include "v" in tag versions', type: 'boolean', default: true, }) .option('monorepo-tags', { describe: 'include library name in tags and release branches', type: 'boolean', default: false, }) .option('pull-request-title-pattern', { describe: 'Title pattern to make release PR', type: 'string', }) .option('pull-request-header', { describe: 'Header for release PR', type: 'string', }) .option('pull-request-footer', { describe: 'Footer for release PR', type: 'string', }) .option('component-no-space', { describe: 'release-please automatically adds ` ` (space) in front of parsed ${component}. Should this be disabled?', type: 'boolean', default: false, }); } const createReleasePullRequestCommand = { command: 'release-pr', describe: 'create or update a PR representing the next release', builder(yargs) { return manifestOptions(manifestConfigOptions(taggingOptions(pullRequestOptions(pullRequestStrategyOptions(gitHubOptions(yargs)))))); }, async handler(argv) { const github = await buildGitHub(argv); const targetBranch = argv.targetBranch || github.repository.defaultBranch; let manifest; if (argv.releaseType) { manifest = await manifest_1.Manifest.fromConfig(github, targetBranch, { releaseType: argv.releaseType, component: argv.component, packageName: argv.packageName, draftPullRequest: argv.draftPullRequest, bumpMinorPreMajor: argv.bumpMinorPreMajor, bumpPatchForMinorPreMajor: argv.bumpPatchForMinorPreMajor, prereleaseType: argv.prereleaseType, changelogPath: argv.changelogPath, changelogType: argv.changelogType, changelogHost: argv.changelogHost, pullRequestTitlePattern: argv.pullRequestTitlePattern, pullRequestHeader: argv.pullRequestHeader, pullRequestFooter: argv.pullRequestFooter, componentNoSpace: argv.componentNoSpace, changelogSections: argv.changelogSections, releaseAs: argv.releaseAs, versioning: argv.versioningStrategy, extraFiles: argv.extraFiles, versionFile: argv.versionFile, includeComponentInTag: argv.monorepoTags, includeVInTag: argv.includeVInTags, }, extractManifestOptions(argv), argv.path); } else { const manifestOptions = extractManifestOptions(argv); manifest = await manifest_1.Manifest.fromManifest(github, targetBranch, argv.configFile, argv.manifestFile, manifestOptions, argv.path, argv.releaseAs); } if (argv.dryRun) { const pullRequests = await manifest.buildPullRequests(); console.log(`Would open ${pullRequests.length} pull requests`); console.log('fork:', manifest.fork); for (const pullRequest of pullRequests) { console.log('title:', pullRequest.title.toString()); console.log('branch:', pullRequest.headRefName); console.log('draft:', pullRequest.draft); console.log('body:', pullRequest.body.toString()); console.log('updates:', pullRequest.updates.length); const changes = await github.buildChangeSet(pullRequest.updates, targetBranch); for (const update of pullRequest.updates) { console.log(` ${update.path}: `, // eslint-disable-next-line @typescript-eslint/no-explicit-any update.updater.constructor); if (argv.trace) { const change = changes.get(update.path); if (change) { const patch = (0, diff_1.createPatch)(update.path, change.originalContent || '', change.content || ''); console.log(patch); } else { console.warn(`no change found for ${update.path}`); } } } } } else { const pullRequestNumbers = await manifest.createPullRequests(); console.log(pullRequestNumbers); } }, }; const createReleaseCommand = { command: 'github-release', describe: 'create a GitHub release from a release PR', builder(yargs) { return releaseOptions(manifestOptions(manifestConfigOptions(taggingOptions(gitHubOptions(yargs))))); }, async handler(argv) { const github = await buildGitHub(argv); const targetBranch = argv.targetBranch || argv.defaultBranch || github.repository.defaultBranch; let manifest; if (argv.releaseType) { manifest = await manifest_1.Manifest.fromConfig(github, targetBranch, { releaseType: argv.releaseType, component: argv.component, packageName: argv.packageName, draft: argv.draft, prerelease: argv.prerelease, includeComponentInTag: argv.monorepoTags, includeVInTag: argv.includeVInTags, }, extractManifestOptions(argv), argv.path); } else { const manifestOptions = extractManifestOptions(argv); manifest = await manifest_1.Manifest.fromManifest(github, targetBranch, argv.configFile, argv.manifestFile, manifestOptions); } if (argv.dryRun) { const releases = await manifest.buildReleases(); logger_1.logger.info(`Would tag ${releases.length} releases:`); for (const release of releases) { logger_1.logger.info({ name: release.name, tag: release.tag.toString(), notes: release.notes, sha: release.sha, draft: release.draft, prerelease: release.prerelease, pullNumber: release.pullRequest.number, }); } } else { const releaseNumbers = await manifest.createReleases(); console.log(releaseNumbers); } }, }; const createManifestPullRequestCommand = { command: 'manifest-pr', describe: 'create a release-PR using a manifest file', deprecated: 'use release-pr instead.', builder(yargs) { return manifestOptions(pullRequestOptions(gitHubOptions(yargs))); }, async handler(argv) { logger_1.logger.warn('manifest-pr is deprecated. Please use release-pr instead.'); const github = await buildGitHub(argv); const targetBranch = argv.targetBranch || argv.defaultBranch || github.repository.defaultBranch; const manifestOptions = extractManifestOptions(argv); const manifest = await manifest_1.Manifest.fromManifest(github, targetBranch, argv.configFile, argv.manifestFile, manifestOptions); if (argv.dryRun) { const pullRequests = await manifest.buildPullRequests(); console.log(`Would open ${pullRequests.length} pull requests`); console.log('fork:', manifest.fork); for (const pullRequest of pullRequests) { console.log('title:', pullRequest.title.toString()); console.log('branch:', pullRequest.headRefName); console.log('draft:', pullRequest.draft); console.log('body:', pullRequest.body.toString()); console.log('updates:', pullRequest.updates.length); for (const update of pullRequest.updates) { console.log(` ${update.path}: `, // eslint-disable-next-line @typescript-eslint/no-explicit-any update.updater.constructor); } } } else { const pullRequestNumbers = await manifest.createPullRequests(); console.log(pullRequestNumbers); } }, }; const createManifestReleaseCommand = { command: 'manifest-release', describe: 'create releases/tags from last release-PR using a manifest file', deprecated: 'use github-release instead', builder(yargs) { return manifestOptions(releaseOptions(gitHubOptions(yargs))); }, async handler(argv) { logger_1.logger.warn('manifest-release is deprecated. Please use github-release instead.'); const github = await buildGitHub(argv); const targetBranch = argv.targetBranch || argv.defaultBranch || github.repository.defaultBranch; const manifestOptions = extractManifestOptions(argv); const manifest = await manifest_1.Manifest.fromManifest(github, targetBranch, argv.configFile, argv.manifestFile, manifestOptions); if (argv.dryRun) { const releases = await manifest.buildReleases(); logger_1.logger.info(releases); } else { const releaseNumbers = await manifest.createReleases(); console.log(releaseNumbers); } }, }; const bootstrapCommand = { command: 'bootstrap', describe: 'configure release manifest', builder(yargs) { return manifestConfigOptions(manifestOptions(releaseOptions(pullRequestStrategyOptions(gitHubOptions(yargs))))) .option('initial-version', { description: 'current version', }) .coerce('path', arg => { return arg || manifest_1.ROOT_PROJECT_PATH; }); }, async handler(argv) { const github = await buildGitHub(argv); const targetBranch = argv.targetBranch || argv.defaultBranch || github.repository.defaultBranch; const bootstrapper = new bootstrapper_1.Bootstrapper(github, targetBranch, argv.manifestFile, argv.configFile, argv.initialVersion); const path = argv.path || manifest_1.ROOT_PROJECT_PATH; const releaserConfig = { releaseType: argv.releaseType, component: argv.component, packageName: argv.packageName, draft: argv.draft, prerelease: argv.prerelease, draftPullRequest: argv.draftPullRequest, bumpMinorPreMajor: argv.bumpMinorPreMajor, bumpPatchForMinorPreMajor: argv.bumpPatchForMinorPreMajor, prereleaseType: argv.prereleaseType, changelogPath: argv.changelogPath, changelogHost: argv.changelogHost, changelogSections: argv.changelogSections, releaseAs: argv.releaseAs, versioning: argv.versioningStrategy, extraFiles: argv.extraFiles, versionFile: argv.versionFile, }; if (argv.dryRun) { const pullRequest = await await bootstrapper.buildPullRequest(path, releaserConfig); console.log('Would open 1 pull request'); console.log('title:', pullRequest.title); console.log('branch:', pullRequest.headBranchName); console.log('body:', pullRequest.body); console.log('updates:', pullRequest.updates.length); const changes = await github.buildChangeSet(pullRequest.updates, targetBranch); for (const update of pullRequest.updates) { console.log(` ${update.path}: `, // eslint-disable-next-line @typescript-eslint/no-explicit-any update.updater.constructor); if (argv.trace) { const change = changes.get(update.path); if (change) { const patch = (0, diff_1.createPatch)(update.path, change.originalContent || '', change.content || ''); console.log(patch); } else { console.warn(`no change found for ${update.path}`); } } } } else { const pullRequest = await bootstrapper.bootstrap(path, releaserConfig); console.log(pullRequest); } }, }; const debugConfigCommand = { command: 'debug-config', describe: 'debug manifest config', builder(yargs) { return manifestConfigOptions(manifestOptions(gitHubOptions(yargs))); }, async handler(argv) { const github = await buildGitHub(argv); const manifestOptions = extractManifestOptions(argv); const targetBranch = argv.targetBranch || argv.defaultBranch || github.repository.defaultBranch; const manifest = await manifest_1.Manifest.fromManifest(github, targetBranch, argv.configFile, argv.manifestFile, manifestOptions); console.log(manifest); }, }; async function buildGitHub(argv) { const [owner, repo] = parseGithubRepoUrl(argv.repoUrl); const github = await github_1.GitHub.create({ owner, repo, token: argv.token, apiUrl: argv.apiUrl, graphqlUrl: argv.graphqlUrl, }); return github; } exports.parser = yargs .command(createReleasePullRequestCommand) .command(createReleaseCommand) .command(createManifestPullRequestCommand) .command(createManifestReleaseCommand) .command(bootstrapCommand) .command(debugConfigCommand) .option('debug', { describe: 'print verbose errors (use only for local debugging).', default: false, type: 'boolean', }) .option('trace', { describe: 'print extra verbose errors (use only for local debugging).', default: false, type: 'boolean', }) .middleware(argv => { if (argv.trace) { (0, logger_1.setLogger)(new logger_1.CheckpointLogger(true, true)); } else if (argv.debug) { (0, logger_1.setLogger)(new logger_1.CheckpointLogger(true)); } }) .option('plugin', { describe: 'load plugin named release-please-<plugin-name>', type: 'array', default: [], }) .middleware(argv => { for (const pluginName of argv.plugin) { console.log(`requiring plugin: ${pluginName}`); try { const plugin = require(pluginName.toString()); if (plugin === null || plugin === void 0 ? void 0 : plugin.init) { console.log(`loading plugin: ${pluginName}`); } else { console.warn(`plugin: ${pluginName} did not have an init() function.`); } } catch (e) { console.warn(`failed to require plugin: ${pluginName}:`, e); } } }) .demandCommand(1) .strict(true) .scriptName('release-please'); function extractManifestOptions(argv) { const manifestOptions = {}; if ('fork' in argv && argv.fork !== undefined) { manifestOptions.fork = argv.fork; } if (argv.label !== undefined) { let labels = argv.label.split(','); if (labels.length === 1 && labels[0] === '') labels = []; manifestOptions.labels = labels; } if ('skipLabeling' in argv && argv.skipLabeling !== undefined) { manifestOptions.skipLabeling = argv.skipLabeling; } if ('releaseLabel' in argv && argv.releaseLabel) { manifestOptions.releaseLabels = argv.releaseLabel.split(','); } if ('snapshotLabel' in argv && argv.snapshotLabel) { manifestOptions.snapshotLabels = argv.snapshotLabel.split(','); } if ('signoff' in argv && argv.signoff) { manifestOptions.signoff = argv.signoff; } if ('draft' in argv && argv.draft !== undefined) { manifestOptions.draft = argv.draft; } if ('draftPullRequest' in argv && argv.draftPullRequest !== undefined) { manifestOptions.draftPullRequest = argv.draftPullRequest; } return manifestOptions; } // The errors returned by octokit currently contain the // request object, this contains information we don't want to // leak. For this reason, we capture exceptions and print // a less verbose error message (run with --debug to output // the request object, don't do this in CI/CD). const handleError = (err) => { var _a, _b; let status = ''; if (exports.handleError.yargsArgs === undefined) { throw new Error('Set handleError.yargsArgs with a yargs.Arguments instance.'); } const ya = exports.handleError.yargsArgs; const errorLogger = (_a = exports.handleError.logger) !== null && _a !== void 0 ? _a : logger_1.logger; const command = ((_b = ya === null || ya === void 0 ? void 0 : ya._) === null || _b === void 0 ? void 0 : _b.length) ? ya._[0] : ''; if (err.status) { status = '' + err.status; } errorLogger.error(`command ${command} failed${status ? ` with status ${status}` : ''}`); if (ya === null || ya === void 0 ? void 0 : ya.debug) { logger_1.logger.error('---------'); logger_1.logger.error(err.stack); } process.exitCode = 1; }; exports.handleError = handleError; // Only run parser if executed with node bin, this allows // for the parser to be easily tested: let argv; if (require.main === module) { (async () => { argv = await exports.parser.parseAsync(); exports.handleError.yargsArgs = argv; process.on('unhandledRejection', err => { (0, exports.handleError)(err); }); process.on('uncaughtException', err => { (0, exports.handleError)(err); }); })(); } //# sourceMappingURL=release-please.js.map