diff --git a/__snapshots__/cli.js b/__snapshots__/cli.js index e1d2d2e99..d5e8e2c3a 100644 --- a/__snapshots__/cli.js +++ b/__snapshots__/cli.js @@ -4,42 +4,54 @@ release-please github-release create a GitHub release from a release PR Options: - --help Show help [boolean] - --version Show version number [boolean] - --debug print verbose errors (use only for local debugging). - [boolean] [default: false] - --trace print extra verbose errors (use only for local debugging). - [boolean] [default: false] - --token GitHub token with repo write permissions - --api-url URL to use when making API requests + --help Show help [boolean] + --version Show version number [boolean] + --debug print verbose errors (use only for local + debugging). [boolean] [default: false] + --trace print extra verbose errors (use only for local + debugging). [boolean] [default: false] + --token GitHub token with repo write permissions + --api-url URL to use when making API requests [string] [default: "https://api.github.com"] - --graphql-url URL to use when making GraphQL requests + --graphql-url URL to use when making GraphQL requests [string] [default: "https://api.github.com"] - --default-branch The branch to open release PRs against and tag releases on + --default-branch The branch to open release PRs against and tag + releases on [deprecated: use --target-branch instead] [string] - --target-branch The branch to open release PRs against and tag releases on + --target-branch The branch to open release PRs against and tag + releases on [string] + --repo-url GitHub URL to generate release for [required] + --dry-run Prepare but do not take action + [boolean] [default: false] + --monorepo-tags include library name in tags and release + branches [boolean] [default: false] + --pull-request-title-pattern Title pattern to make release PR [string] + --path release from path other than root directory [string] - --repo-url GitHub URL to generate release for [required] - --dry-run Prepare but do not take action [boolean] [default: false] - --path release from path other than root directory [string] - --component name of component release is being minted for [string] - --package-name name of package release is being minted for [string] - --release-type what type of repo is a release being created for? + --component name of component release is being minted for + [string] + --package-name name of package release is being minted for + [string] + --release-type what type of repo is a release being created + for? [choices: "dart", "elixir", "go", "go-yoshi", "helm", "java-backport", "java-bom", "java-lts", "java-yoshi", "krm-blueprint", "node", "ocaml", "php", "php-yoshi", "python", "ruby", "ruby-yoshi", "rust", "simple", "terraform-module"] - --config-file where can the config file be found in the project? - [default: "release-please-config.json"] - --manifest-file where can the manifest file be found in the project? + --config-file where can the config file be found in the + project? [default: "release-please-config.json"] + --manifest-file where can the manifest file be found in the + project? [default: ".release-please-manifest.json"] - --draft 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. + --draft 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. [boolean] [default: false] - --label comma-separated list of labels to remove to from release PR - [default: "autorelease: pending"] - --release-label set a pull request label other than "autorelease: tagged" + --label comma-separated list of labels to remove to from + release PR [default: "autorelease: pending"] + --release-label set a pull request label other than + "autorelease: tagged" [string] [default: "autorelease: tagged"] ` @@ -151,8 +163,6 @@ Options: the minor for non-breaking changes prior to the first major release [boolean] [default: false] - --monorepo-tags include library name in tags and release - branches [boolean] [default: false] --extra-files extra files for the strategy to consider [string] --version-file path to version file to update, e.g., @@ -161,7 +171,6 @@ Options: generated? [boolean] [default: false] --versioning-strategy strategy used for bumping versions [choices: "default", "always-bump-patch", "service-pack"] [default: "default"] - --pull-request-title-pattern Title pattern to make release PR [string] --changelog-path where can the CHANGELOG be found in the project? [string] [default: "CHANGELOG.md"] --last-package-version last version # that package was released as @@ -182,6 +191,9 @@ Options: commit log message using the user and email provided. (format "Name "). [string] + --monorepo-tags include library name in tags and release + branches [boolean] [default: false] + --pull-request-title-pattern Title pattern to make release PR [string] --path release from path other than root directory [string] --component name of component release is being minted diff --git a/__snapshots__/release-notes.js b/__snapshots__/default-changelog-notes.js similarity index 75% rename from __snapshots__/release-notes.js rename to __snapshots__/default-changelog-notes.js index f155c7cfa..fb97aabc5 100644 --- a/__snapshots__/release-notes.js +++ b/__snapshots__/default-changelog-notes.js @@ -1,4 +1,4 @@ -exports['ReleaseNotes buildNotes should build default release notes 1'] = ` +exports['DefaultChangelogNotes buildNotes should build default release notes 1'] = ` ### [1.2.3](https://github.com/googleapis/java-asset/compare/v1.2.2...v1.2.3) (1983-10-10) @@ -16,7 +16,7 @@ exports['ReleaseNotes buildNotes should build default release notes 1'] = ` * some bugfix ([sha2](https://github.com/googleapis/java-asset/commit/sha2)) ` -exports['ReleaseNotes buildNotes should build with custom changelog sections 1'] = ` +exports['DefaultChangelogNotes buildNotes should build with custom changelog sections 1'] = ` ### [1.2.3](https://github.com/googleapis/java-asset/compare/v1.2.2...v1.2.3) (1983-10-10) @@ -39,7 +39,7 @@ exports['ReleaseNotes buildNotes should build with custom changelog sections 1'] * some documentation ([sha3](https://github.com/googleapis/java-asset/commit/sha3)) ` -exports['ReleaseNotes buildNotes should handle BREAKING CHANGE notes 1'] = ` +exports['DefaultChangelogNotes buildNotes should handle BREAKING CHANGE notes 1'] = ` ### [1.2.3](https://github.com/googleapis/java-asset/compare/v1.2.2...v1.2.3) (1983-10-10) @@ -52,7 +52,7 @@ exports['ReleaseNotes buildNotes should handle BREAKING CHANGE notes 1'] = ` * some bugfix ([sha2](https://github.com/googleapis/java-asset/commit/sha2)) ` -exports['ReleaseNotes buildNotes should ignore RELEASE AS notes 1'] = ` +exports['DefaultChangelogNotes buildNotes should ignore RELEASE AS notes 1'] = ` ### [1.2.3](https://github.com/googleapis/java-asset/compare/v1.2.2...v1.2.3) (1983-10-10) @@ -65,7 +65,7 @@ exports['ReleaseNotes buildNotes should ignore RELEASE AS notes 1'] = ` * some bugfix ([sha2](https://github.com/googleapis/java-asset/commit/sha2)) ` -exports['ReleaseNotes buildNotes with commit parsing handles Release-As footers 1'] = ` +exports['DefaultChangelogNotes buildNotes with commit parsing handles Release-As footers 1'] = ` ### [1.2.3](https://github.com/googleapis/java-asset/compare/v1.2.2...v1.2.3) (1983-10-10) @@ -78,7 +78,7 @@ exports['ReleaseNotes buildNotes with commit parsing handles Release-As footers * correct release ([1f64add](https://github.com/googleapis/java-asset/commit/1f64add37f426e87ce1b777616a137ec)) ` -exports['ReleaseNotes buildNotes with commit parsing should handle BREAKING CHANGE body 1'] = ` +exports['DefaultChangelogNotes buildNotes with commit parsing should handle BREAKING CHANGE body 1'] = ` ### [1.2.3](https://github.com/googleapis/java-asset/compare/v1.2.2...v1.2.3) (1983-10-10) @@ -91,7 +91,7 @@ exports['ReleaseNotes buildNotes with commit parsing should handle BREAKING CHAN * some feature ([78abf20](https://github.com/googleapis/java-asset/commit/78abf20625d3ff86d627b5c6e0cacd06)) ` -exports['ReleaseNotes buildNotes with commit parsing should handle a breaking change 1'] = ` +exports['DefaultChangelogNotes buildNotes with commit parsing should handle a breaking change 1'] = ` ### [1.2.3](https://github.com/googleapis/java-asset/compare/v1.2.2...v1.2.3) (1983-10-10) @@ -104,7 +104,7 @@ exports['ReleaseNotes buildNotes with commit parsing should handle a breaking ch * some bugfix ([05670cf](https://github.com/googleapis/java-asset/commit/05670cf2e850beffe53bb2691f8701c7)) ` -exports['ReleaseNotes buildNotes with commit parsing should handle bug links 1'] = ` +exports['DefaultChangelogNotes buildNotes with commit parsing should handle bug links 1'] = ` ### [1.2.3](https://github.com/googleapis/java-asset/compare/v1.2.2...v1.2.3) (1983-10-10) @@ -113,7 +113,7 @@ exports['ReleaseNotes buildNotes with commit parsing should handle bug links 1'] * some fix ([71489e6](https://github.com/googleapis/java-asset/commit/71489e63ad212c54598f5bdcbedec5f6)), closes [#123](https://github.com/googleapis/java-asset/issues/123) ` -exports['ReleaseNotes buildNotes with commit parsing should handle git trailers 1'] = ` +exports['DefaultChangelogNotes buildNotes with commit parsing should handle git trailers 1'] = ` ### [1.2.3](https://github.com/googleapis/java-asset/compare/v1.2.2...v1.2.3) (1983-10-10) @@ -126,7 +126,7 @@ exports['ReleaseNotes buildNotes with commit parsing should handle git trailers * some fix ([c538c97](https://github.com/googleapis/java-asset/commit/c538c973dc84b83ee6b699cf6433f0b3)) ` -exports['ReleaseNotes buildNotes with commit parsing should handle meta commits 1'] = ` +exports['DefaultChangelogNotes buildNotes with commit parsing should handle meta commits 1'] = ` ### [1.2.3](https://github.com/googleapis/java-asset/compare/v1.2.2...v1.2.3) (1983-10-10) @@ -145,7 +145,7 @@ exports['ReleaseNotes buildNotes with commit parsing should handle meta commits * **securitycenter:** fixes security center. ([3cf10aa](https://github.com/googleapis/java-asset/commit/3cf10aa5f94cd40a1d0d08e573eb737f)) ` -exports['ReleaseNotes buildNotes with commit parsing should handle multi-line breaking change, if prefixed with list 1'] = ` +exports['DefaultChangelogNotes buildNotes with commit parsing should handle multi-line breaking change, if prefixed with list 1'] = ` ### [1.2.3](https://github.com/googleapis/java-asset/compare/v1.2.2...v1.2.3) (1983-10-10) @@ -160,7 +160,7 @@ exports['ReleaseNotes buildNotes with commit parsing should handle multi-line br * upgrade to Node 7 ([a931c2d](https://github.com/googleapis/java-asset/commit/a931c2d29e9849c6989dfd4712226699)) ` -exports['ReleaseNotes buildNotes with commit parsing should handle multi-line breaking changes 1'] = ` +exports['DefaultChangelogNotes buildNotes with commit parsing should handle multi-line breaking changes 1'] = ` ### [1.2.3](https://github.com/googleapis/java-asset/compare/v1.2.2...v1.2.3) (1983-10-10) @@ -173,7 +173,7 @@ exports['ReleaseNotes buildNotes with commit parsing should handle multi-line br * upgrade to Node 7 ([8916be7](https://github.com/googleapis/java-asset/commit/8916be74596394c27516696b957fd0d7)) ` -exports['ReleaseNotes buildNotes with commit parsing should not include content two newlines after BREAKING CHANGE 1'] = ` +exports['DefaultChangelogNotes buildNotes with commit parsing should not include content two newlines after BREAKING CHANGE 1'] = ` ### [1.2.3](https://github.com/googleapis/java-asset/compare/v1.2.2...v1.2.3) (1983-10-10) @@ -186,7 +186,7 @@ exports['ReleaseNotes buildNotes with commit parsing should not include content * upgrade to Node 7 ([66925b0](https://github.com/googleapis/java-asset/commit/66925b06f59fc4fdd3031c498e1b0098)) ` -exports['ReleaseNotes buildNotes with commit parsing should parse multiple commit messages from a single commit 1'] = ` +exports['DefaultChangelogNotes buildNotes with commit parsing should parse multiple commit messages from a single commit 1'] = ` ### [1.2.3](https://github.com/googleapis/java-asset/compare/v1.2.2...v1.2.3) (1983-10-10) diff --git a/src/bin/release-please.ts b/src/bin/release-please.ts index fea9d9075..748589415 100644 --- a/src/bin/release-please.ts +++ b/src/bin/release-please.ts @@ -18,7 +18,7 @@ import {coerceOption} from '../util/coerce-option'; import * as yargs from 'yargs'; import {GitHub, GH_API_URL, GH_GRAPHQL_URL} from '../github'; import {Manifest, ManifestOptions, ROOT_PROJECT_PATH} from '../manifest'; -import {ChangelogSection} from '../release-notes'; +import {ChangelogSection} from '../changelog-notes'; import {logger, setLogger, CheckpointLogger} from '../util/logger'; import { getReleaserTypes, @@ -91,7 +91,6 @@ interface PullRequestArgs { interface PullRequestStrategyArgs { snapshot?: boolean; - monorepoTags?: boolean; changelogSections?: ChangelogSection[]; changelogPath?: string; versioningStrategy?: VersioningStrategyType; @@ -99,22 +98,28 @@ interface PullRequestStrategyArgs { // for Ruby: TODO refactor to find version.rb like Python finds version.py // and then remove this property versionFile?: string; - pullRequestTitlePattern?: string; extraFiles?: string[]; } +interface TaggingArgs { + monorepoTags?: boolean; + pullRequestTitlePattern?: string; +} + interface CreatePullRequestArgs extends GitHubArgs, ManifestArgs, ManifestConfigArgs, VersioningArgs, PullRequestArgs, - PullRequestStrategyArgs {} + PullRequestStrategyArgs, + TaggingArgs {} interface CreateReleaseArgs extends GitHubArgs, ManifestArgs, ManifestConfigArgs, - ReleaseArgs {} + ReleaseArgs, + TaggingArgs {} interface CreateManifestPullRequestArgs extends GitHubArgs, ManifestArgs, @@ -239,11 +244,6 @@ function pullRequestStrategyOptions(yargs: yargs.Argv): yargs.Argv { default: false, type: 'boolean', }) - .option('monorepo-tags', { - describe: 'include library name in tags and release branches', - type: 'boolean', - default: false, - }) .option('extra-files', { describe: 'extra files for the strategy to consider', type: 'string', @@ -268,10 +268,6 @@ function pullRequestStrategyOptions(yargs: yargs.Argv): yargs.Argv { choices: getVersioningStrategyTypes(), default: 'default', }) - .option('pull-request-title-pattern', { - describe: 'Title pattern to make release PR', - type: 'string', - }) .option('changelog-path', { default: 'CHANGELOG.md', describe: 'where can the CHANGELOG be found in the project?', @@ -350,6 +346,19 @@ function manifestOptions(yargs: yargs.Argv): yargs.Argv { }); } +function taggingOptions(yargs: yargs.Argv): yargs.Argv { + return yargs + .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', + }); +} + const createReleasePullRequestCommand: yargs.CommandModule< {}, CreatePullRequestArgs @@ -359,7 +368,9 @@ const createReleasePullRequestCommand: yargs.CommandModule< builder(yargs) { return manifestOptions( manifestConfigOptions( - pullRequestOptions(pullRequestStrategyOptions(gitHubOptions(yargs))) + taggingOptions( + pullRequestOptions(pullRequestStrategyOptions(gitHubOptions(yargs))) + ) ) ); }, @@ -384,6 +395,7 @@ const createReleasePullRequestCommand: yargs.CommandModule< versioning: argv.versioningStrategy, extraFiles: argv.extraFiles, versionFile: argv.versionFile, + includeComponentInTag: argv.monorepoTags, }, extractManifestOptions(argv), argv.path @@ -429,7 +441,9 @@ const createReleaseCommand: yargs.CommandModule<{}, CreateReleaseArgs> = { describe: 'create a GitHub release from a release PR', builder(yargs) { return releaseOptions( - manifestOptions(manifestConfigOptions(gitHubOptions(yargs))) + manifestOptions( + manifestConfigOptions(taggingOptions(gitHubOptions(yargs))) + ) ); }, async handler(argv) { @@ -448,6 +462,7 @@ const createReleaseCommand: yargs.CommandModule<{}, CreateReleaseArgs> = { component: argv.component, packageName: argv.packageName, draft: argv.draft, + includeComponentInTag: argv.monorepoTags, }, extractManifestOptions(argv), argv.path diff --git a/src/changelog-notes.ts b/src/changelog-notes.ts new file mode 100644 index 000000000..b5110b678 --- /dev/null +++ b/src/changelog-notes.ts @@ -0,0 +1,37 @@ +// 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. + +import {ConventionalCommit} from './commit'; + +export interface BuildNotesOptions { + host?: string; + owner: string; + repository: string; + version: string; + previousTag?: string; + currentTag: string; +} + +export interface ChangelogNotes { + buildNotes( + commits: ConventionalCommit[], + options: BuildNotesOptions + ): Promise; +} + +export interface ChangelogSection { + type: string; + section: string; + hidden?: boolean; +} diff --git a/src/release-notes.ts b/src/changelog-notes/default.ts similarity index 85% rename from src/release-notes.ts rename to src/changelog-notes/default.ts index 1110e8e92..7de1e8c64 100644 --- a/src/release-notes.ts +++ b/src/changelog-notes/default.ts @@ -12,43 +12,34 @@ // See the License for the specific language governing permissions and // limitations under the License. -import {ConventionalCommit} from './commit'; +import { + ChangelogSection, + ChangelogNotes, + BuildNotesOptions, +} from '../changelog-notes'; +import {ConventionalCommit} from '../commit'; // eslint-disable-next-line @typescript-eslint/no-var-requires const conventionalChangelogWriter = require('conventional-changelog-writer'); // eslint-disable-next-line @typescript-eslint/no-var-requires const presetFactory = require('conventional-changelog-conventionalcommits'); +const DEFAULT_HOST = 'https://github.com'; -export interface ChangelogSection { - type: string; - section: string; - hidden?: boolean; -} - -interface ReleaseNotesOptions { +interface DefaultChangelogNotesOptions { changelogSections?: ChangelogSection[]; commitPartial?: string; headerPartial?: string; mainTemplate?: string; } -interface BuildNotesOptions { - host?: string; - owner: string; - repository: string; - version: string; - previousTag?: string; - currentTag: string; -} - -export class ReleaseNotes { +export class DefaultChangelogNotes implements ChangelogNotes { // allow for customized commit template. private changelogSections?: ChangelogSection[]; private commitPartial?: string; private headerPartial?: string; private mainTemplate?: string; - constructor(options: ReleaseNotesOptions = {}) { + constructor(options: DefaultChangelogNotesOptions = {}) { this.changelogSections = options.changelogSections; this.commitPartial = options.commitPartial; this.headerPartial = options.headerPartial; @@ -60,7 +51,7 @@ export class ReleaseNotes { options: BuildNotesOptions ): Promise { const context = { - host: options.host || 'https://github.com', + host: options.host || DEFAULT_HOST, owner: options.owner, repository: options.repository, version: options.version, diff --git a/src/factory.ts b/src/factory.ts index 8876ad66c..c79577477 100644 --- a/src/factory.ts +++ b/src/factory.ts @@ -111,7 +111,7 @@ export async function buildStrategy( bumpMinorPreMajor: options.bumpMinorPreMajor, bumpPatchForMinorPreMajor: options.bumpPatchForMinorPreMajor, }); - const strategyOptions = { + const strategyOptions: StrategyOptions = { github: options.github, targetBranch, path: options.path, @@ -124,6 +124,8 @@ export async function buildStrategy( versioningStrategy, skipGitHubRelease: options.skipGithubRelease, releaseAs: options.releaseAs, + includeComponentInTag: options.includeComponentInTag, + pullRequestTitlePattern: options.pullRequestTitlePattern, }; switch (options.releaseType) { case 'ruby': { diff --git a/src/manifest.ts b/src/manifest.ts index 8e8a29368..c9a607de3 100644 --- a/src/manifest.ts +++ b/src/manifest.ts @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -import {ChangelogSection} from './release-notes'; +import {ChangelogSection} from './changelog-notes'; import {GitHub, GitHubRelease} from './github'; import {Version, VersionsMap} from './version'; import {Commit} from './commit'; @@ -56,6 +56,8 @@ export interface ReleaserConfig { draftPullRequest?: boolean; component?: string; packageName?: string; + includeComponentInTag?: boolean; + pullRequestTitlePattern?: string; // Ruby-only versionFile?: string; @@ -72,6 +74,7 @@ export interface CandidateReleasePullRequest { export interface CandidateRelease extends Release { pullRequest: PullRequest; draft?: boolean; + path: string; } interface ReleaserConfigJson { @@ -85,6 +88,8 @@ interface ReleaserConfigJson { 'draft-pull-request'?: boolean; label?: string; 'release-label'?: string; + 'include-component-in-tag'?: boolean; + 'pull-request-title-pattern'?: string; // Ruby-only 'version-file'?: string; @@ -140,6 +145,14 @@ const DEFAULT_RELEASE_LABELS = ['autorelease: tagged']; export const MANIFEST_PULL_REQUEST_TITLE_PATTERN = 'chore: release ${branch}'; +interface CreatedRelease extends GitHubRelease { + path: string; + version: string; + major: number; + minor: number; + patch: number; +} + export class Manifest { private repository: Repository; private github: GitHub; @@ -287,7 +300,8 @@ export class Manifest { const latestVersion = await latestReleaseVersion( github, targetBranch, - component + config.includeComponentInTag ? component : '', + config.pullRequestTitlePattern ); if (latestVersion) { releasedVersions[path] = latestVersion; @@ -520,7 +534,7 @@ export class Manifest { * * @returns {number[]} Pull request numbers of release pull requests */ - async createPullRequests(): Promise<(number | undefined)[]> { + async createPullRequests(): Promise<(PullRequest | undefined)[]> { const candidatePullRequests = await this.buildPullRequests(); if (candidatePullRequests.length === 0) { return []; @@ -554,7 +568,7 @@ export class Manifest { } logger.info(`found ${openPullRequests.length} open release pull requests.`); - const promises: Promise[] = []; + const promises: Promise[] = []; for (const pullRequest of candidatePullRequests) { promises.push( this.createOrUpdatePullRequest(pullRequest, openPullRequests) @@ -566,7 +580,7 @@ export class Manifest { private async createOrUpdatePullRequest( pullRequest: ReleasePullRequest, openPullRequests: PullRequest[] - ): Promise { + ): Promise { // look for existing, open pull rquest const existing = openPullRequests.find( openPullRequest => @@ -589,7 +603,7 @@ export class Manifest { signoffUser: this.signoffUser, } ); - return updatedPullRequest.number; + return updatedPullRequest; } else { const newPullRequest = await this.github.createReleasePullRequest( pullRequest, @@ -599,7 +613,7 @@ export class Manifest { signoffUser: this.signoffUser, } ); - return newPullRequest.number; + return newPullRequest; } } @@ -669,6 +683,7 @@ export class Manifest { if (release) { releases.push({ ...release, + path, pullRequest, draft: config.draft ?? this.draft, }); @@ -686,7 +701,7 @@ export class Manifest { * * @returns {GitHubRelease[]} List of created GitHub releases */ - async createReleases(): Promise<(GitHubRelease | undefined)[]> { + async createReleases(): Promise<(CreatedRelease | undefined)[]> { const releasesByPullRequest: Record = {}; const pullRequestsByNumber: Record = {}; for (const release of await this.buildReleases()) { @@ -698,7 +713,7 @@ export class Manifest { } } - const promises: Promise[] = []; + const promises: Promise[] = []; for (const pullNumber in releasesByPullRequest) { promises.push( this.createReleasesForPullRequest( @@ -714,9 +729,9 @@ export class Manifest { private async createReleasesForPullRequest( releases: CandidateRelease[], pullRequest: PullRequest - ): Promise { + ): Promise { // create the release - const promises: Promise[] = []; + const promises: Promise[] = []; for (const release of releases) { promises.push(this.createRelease(release)); } @@ -733,7 +748,7 @@ export class Manifest { private async createRelease( release: CandidateRelease - ): Promise { + ): Promise { const githubRelease = await this.github.createRelease(release, { draft: release.draft, }); @@ -742,7 +757,14 @@ export class Manifest { const comment = `:robot: Release is at ${githubRelease.url} :sunflower:`; await this.github.commentOnIssue(comment, release.pullRequest.number); - return githubRelease; + return { + ...githubRelease, + path: release.path, + version: release.tag.version.toString(), + major: release.tag.version.major, + minor: release.tag.version.minor, + patch: release.tag.version.patch, + }; } private async getStrategiesByPath(): Promise> { @@ -770,8 +792,7 @@ export class Manifest { const strategiesByPath = await this.getStrategiesByPath(); for (const path in this.repositoryConfig) { const strategy = strategiesByPath[path]; - const component = - strategy.component || (await strategy.getDefaultComponent()) || ''; + const component = (await strategy.getComponent()) || ''; if (this._pathsByComponent[component]) { logger.warn( `Multiple paths for ${component}: ${this._pathsByComponent[component]}, ${path}` @@ -806,6 +827,8 @@ function extractReleaserConfig(config: ReleaserPackageConfig): ReleaserConfig { packageName: config['package-name'], versionFile: config['version-file'], extraFiles: config['extra-files'], + includeComponentInTag: config['include-component-in-tag'], + pullRequestTitlePattern: config['pull-request-title-pattern'], }; } @@ -875,7 +898,8 @@ async function parseReleasedVersions( async function latestReleaseVersion( github: GitHub, targetBranch: string, - prefix?: string + prefix?: string, + pullRequestTitlePattern?: string ): Promise { const branchPrefix = prefix ? prefix.endsWith('-') @@ -908,7 +932,10 @@ async function latestReleaseVersion( continue; } - const pullRequestTitle = PullRequestTitle.parse(mergedPullRequest.title); + const pullRequestTitle = PullRequestTitle.parse( + mergedPullRequest.title, + pullRequestTitlePattern + ); if (!pullRequestTitle) { continue; } diff --git a/src/strategies/php-yoshi.ts b/src/strategies/php-yoshi.ts index bb5ab2321..c9a77f4b9 100644 --- a/src/strategies/php-yoshi.ts +++ b/src/strategies/php-yoshi.ts @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -import {Strategy, BuildUpdatesOptions} from '../strategy'; +import {Strategy, BuildUpdatesOptions, StrategyOptions} from '../strategy'; import {Update} from '../update'; import {Changelog} from '../updaters/changelog'; import {RootComposerUpdatePackages} from '../updaters/php/root-composer-update-packages'; @@ -30,7 +30,6 @@ import {PullRequestTitle} from '../util/pull-request-title'; import {BranchName} from '../util/branch-name'; import {PullRequestBody} from '../util/pull-request-body'; import {GitHubFileContents} from '../github'; -import {ReleaseNotes} from '../release-notes'; const CHANGELOG_SECTIONS = [ {type: 'feat', section: 'Features'}, @@ -59,6 +58,12 @@ interface ComponentInfo { } export class PHPYoshi extends Strategy { + constructor(options: StrategyOptions) { + super({ + ...options, + changelogSections: CHANGELOG_SECTIONS, + }); + } async buildReleasePullRequest( commits: Commit[], latestRelease?: Release, @@ -80,12 +85,6 @@ export class PHPYoshi extends Strategy { const topLevelDirectories = Object.keys(splitCommits).sort(); const versionsMap: VersionsMap = new Map(); const directoryVersionContents: Record = {}; - const releaseNotes = new ReleaseNotes({ - changelogSections: CHANGELOG_SECTIONS, - commitPartial: this.commitPartial, - headerPartial: this.headerPartial, - mainTemplate: this.mainTemplate, - }); const component = await this.getComponent(); const newVersionTag = new TagName(newVersion, component); let releaseNotesBody = `## ${newVersion.toString()}`; @@ -109,7 +108,7 @@ export class PHPYoshi extends Strategy { splitCommits[directory] ); versionsMap.set(composer.name, newVersion); - const partialReleaseNotes = await releaseNotes.buildNotes( + const partialReleaseNotes = await this.changelogNotes.buildNotes( splitCommits[directory], { owner: this.repository.owner, diff --git a/src/strategy.ts b/src/strategy.ts index 79a109b8f..a01b09a09 100644 --- a/src/strategy.ts +++ b/src/strategy.ts @@ -20,7 +20,7 @@ import {parseConventionalCommits, Commit, ConventionalCommit} from './commit'; import {VersioningStrategy} from './versioning-strategy'; import {DefaultVersioningStrategy} from './versioning-strategies/default'; import {PullRequestTitle} from './util/pull-request-title'; -import {ReleaseNotes, ChangelogSection} from './release-notes'; +import {ChangelogNotes, ChangelogSection} from './changelog-notes'; import {Update} from './update'; import {Repository} from './repository'; import {PullRequest} from './pull-request'; @@ -32,6 +32,7 @@ import { ROOT_PROJECT_PATH, } from './manifest'; import {PullRequestBody} from './util/pull-request-body'; +import {DefaultChangelogNotes} from './changelog-notes/default'; const DEFAULT_CHANGELOG_PATH = 'CHANGELOG.md'; @@ -58,6 +59,9 @@ export interface StrategyOptions { tagSeparator?: string; skipGitHubRelease?: boolean; releaseAs?: string; + changelogNotes?: ChangelogNotes; + includeComponentInTag?: boolean; + pullRequestTitlePattern?: string; } /** @@ -76,12 +80,13 @@ export abstract class Strategy { protected tagSeparator?: string; private skipGitHubRelease: boolean; private releaseAs?: string; + private includeComponentInTag: boolean; + private pullRequestTitlePattern?: string; + + protected changelogNotes: ChangelogNotes; // CHANGELOG configuration protected changelogSections?: ChangelogSection[]; - protected commitPartial?: string; - protected headerPartial?: string; - protected mainTemplate?: string; constructor(options: StrategyOptions) { this.path = options.path || ROOT_PROJECT_PATH; @@ -95,12 +100,13 @@ export abstract class Strategy { this.repository = options.github.repository; this.changelogPath = options.changelogPath || DEFAULT_CHANGELOG_PATH; this.changelogSections = options.changelogSections; - this.commitPartial = options.commitPartial; - this.headerPartial = options.headerPartial; - this.mainTemplate = options.mainTemplate; this.tagSeparator = options.tagSeparator; this.skipGitHubRelease = options.skipGitHubRelease || false; this.releaseAs = options.releaseAs; + this.changelogNotes = + options.changelogNotes || new DefaultChangelogNotes(options); + this.includeComponentInTag = options.includeComponentInTag ?? true; + this.pullRequestTitlePattern = options.pullRequestTitlePattern; } /** @@ -112,6 +118,9 @@ export abstract class Strategy { ): Promise; async getComponent(): Promise { + if (!this.includeComponentInTag) { + return ''; + } return this.component || (await this.getDefaultComponent()); } @@ -147,13 +156,7 @@ export abstract class Strategy { newVersionTag: TagName, latestRelease?: Release ): Promise { - const releaseNotes = new ReleaseNotes({ - changelogSections: this.changelogSections, - commitPartial: this.commitPartial, - headerPartial: this.headerPartial, - mainTemplate: this.mainTemplate, - }); - return await releaseNotes.buildNotes(conventionalCommits, { + return await this.changelogNotes.buildNotes(conventionalCommits, { owner: this.repository.owner, repository: this.repository.repo, version: newVersion.toString(), @@ -203,11 +206,17 @@ export abstract class Strategy { const component = await this.getComponent(); logger.debug('component:', component); - const newVersionTag = new TagName(newVersion, component); + const newVersionTag = new TagName( + newVersion, + this.includeComponentInTag ? component : undefined, + this.tagSeparator + ); + logger.warn('pull request title pattern:', this.pullRequestTitlePattern); const pullRequestTitle = PullRequestTitle.ofComponentTargetBranchVersion( component || '', this.targetBranch, - newVersion + newVersion, + this.pullRequestTitlePattern ); const branchName = component ? BranchName.ofComponentTargetBranch(component, this.targetBranch) @@ -289,28 +298,36 @@ export abstract class Strategy { mergedPullRequest: PullRequest ): Promise { if (this.skipGitHubRelease) { - return undefined; + logger.info('Release skipped from strategy config'); + return; + } + if (!mergedPullRequest.sha) { + logger.error('Pull request should have been merged'); + return; } const pullRequestTitle = - PullRequestTitle.parse(mergedPullRequest.title) || + PullRequestTitle.parse( + mergedPullRequest.title, + this.pullRequestTitlePattern + ) || PullRequestTitle.parse( mergedPullRequest.title, MANIFEST_PULL_REQUEST_TITLE_PATTERN ); if (!pullRequestTitle) { - throw new Error(`Bad pull request title: '${mergedPullRequest.title}'`); + logger.error(`Bad pull request title: '${mergedPullRequest.title}'`); + return; } const branchName = BranchName.parse(mergedPullRequest.headBranchName); if (!branchName) { - throw new Error(`Bad branch name: ${mergedPullRequest.headBranchName}`); - } - if (!mergedPullRequest.sha) { - throw new Error('Pull request should have been merged'); + logger.error(`Bad branch name: ${mergedPullRequest.headBranchName}`); + return; } const pullRequestBody = PullRequestBody.parse(mergedPullRequest.body); if (!pullRequestBody) { - throw new Error('could not parse pull request body as a release PR'); + logger.error('Could not parse pull request body as a release PR'); + return; } const component = await this.getComponent(); logger.info('component:', component); @@ -330,11 +347,17 @@ export abstract class Strategy { } const version = pullRequestTitle.getVersion() || releaseData?.version; if (!version) { - throw new Error('Pull request should have included version'); + logger.error('Pull request should have included version'); + return; } + const tag = new TagName( + version, + this.includeComponentInTag ? component : undefined, + this.tagSeparator + ); return { - tag: new TagName(version, component, this.tagSeparator), + tag, notes: notes || '', sha: mergedPullRequest.sha, }; diff --git a/src/util/branch-name.ts b/src/util/branch-name.ts index bf9749a54..87f36cee0 100644 --- a/src/util/branch-name.ts +++ b/src/util/branch-name.ts @@ -24,7 +24,13 @@ const RELEASE_PLEASE = 'release-please'; type BranchNameType = typeof BranchName; function getAllResourceNames(): BranchNameType[] { - return [AutoreleaseBranchName, ComponentBranchName, DefaultBranchName]; + return [ + AutoreleaseBranchName, + ComponentBranchName, + DefaultBranchName, + V12ComponentBranchName, + V12DefaultBranchName, + ]; } export class BranchName { @@ -51,14 +57,16 @@ export class BranchName { return new AutoreleaseBranchName(`release-v${version}`); } static ofTargetBranch(targetBranch: string): BranchName { - return new DefaultBranchName(`${RELEASE_PLEASE}/branches/${targetBranch}`); + return new DefaultBranchName( + `${RELEASE_PLEASE}--branches--${targetBranch}` + ); } static ofComponentTargetBranch( component: string, targetBranch: string ): BranchName { return new ComponentBranchName( - `${RELEASE_PLEASE}/branches/${targetBranch}/components/${component}` + `${RELEASE_PLEASE}--branches--${targetBranch}--components--${component}` ); } constructor(_branchName: string) {} @@ -80,6 +88,11 @@ export class BranchName { } } +/** + * This is the legacy branch pattern used by releasetool + * + * @see https://github.com/googleapis/releasetool + */ const AUTORELEASE_PATTERN = /^release-?(?[\w-.]*)?-v(?[0-9].*)$/; class AutoreleaseBranchName extends BranchName { @@ -102,7 +115,56 @@ class AutoreleaseBranchName extends BranchName { } } -const DEFAULT_PATTERN = `^${RELEASE_PLEASE}/branches/(?[^/]+)$`; +/** + * This is a parsable branch pattern used by release-please v12. + * It has potential issues due to git treating `/` like directories. + * This should be removed at some point in the future. + * + * @see https://github.com/googleapis/release-please/issues/1024 + */ +const V12_DEFAULT_PATTERN = `^${RELEASE_PLEASE}/branches/(?[^/]+)$`; +class V12DefaultBranchName extends BranchName { + static matches(branchName: string): boolean { + return !!branchName.match(V12_DEFAULT_PATTERN); + } + constructor(branchName: string) { + super(branchName); + const match = branchName.match(V12_DEFAULT_PATTERN); + if (match?.groups) { + this.targetBranch = match.groups['branch']; + } + } + toString(): string { + return `${RELEASE_PLEASE}/branches/${this.targetBranch}`; + } +} + +/** + * This is a parsable branch pattern used by release-please v12. + * It has potential issues due to git treating `/` like directories. + * This should be removed at some point in the future. + * + * @see https://github.com/googleapis/release-please/issues/1024 + */ +const V12_COMPONENT_PATTERN = `^${RELEASE_PLEASE}/branches/(?[^/]+)/components/(?.+)$`; +class V12ComponentBranchName extends BranchName { + static matches(branchName: string): boolean { + return !!branchName.match(V12_COMPONENT_PATTERN); + } + constructor(branchName: string) { + super(branchName); + const match = branchName.match(V12_COMPONENT_PATTERN); + if (match?.groups) { + this.targetBranch = match.groups['branch']; + this.component = match.groups['component']; + } + } + toString(): string { + return `${RELEASE_PLEASE}/branches/${this.targetBranch}/components/${this.component}`; + } +} + +const DEFAULT_PATTERN = `^${RELEASE_PLEASE}--branches--(?.+)$`; class DefaultBranchName extends BranchName { static matches(branchName: string): boolean { return !!branchName.match(DEFAULT_PATTERN); @@ -115,11 +177,11 @@ class DefaultBranchName extends BranchName { } } toString(): string { - return `${RELEASE_PLEASE}/branches/${this.targetBranch}`; + return `${RELEASE_PLEASE}--branches--${this.targetBranch}`; } } -const COMPONENT_PATTERN = `^${RELEASE_PLEASE}/branches/(?[^/]+)/components/(?.+)$`; +const COMPONENT_PATTERN = `^${RELEASE_PLEASE}--branches--(?.+)--components--(?.+)$`; class ComponentBranchName extends BranchName { static matches(branchName: string): boolean { return !!branchName.match(COMPONENT_PATTERN); @@ -133,6 +195,6 @@ class ComponentBranchName extends BranchName { } } toString(): string { - return `${RELEASE_PLEASE}/branches/${this.targetBranch}/components/${this.component}`; + return `${RELEASE_PLEASE}--branches--${this.targetBranch}--components--${this.component}`; } } diff --git a/test/release-notes.ts b/test/changelog-notes/default-changelog-notes.ts similarity index 74% rename from test/release-notes.ts rename to test/changelog-notes/default-changelog-notes.ts index 161b85e04..14c14bb61 100644 --- a/test/release-notes.ts +++ b/test/changelog-notes/default-changelog-notes.ts @@ -14,11 +14,15 @@ import {describe, it} from 'mocha'; import {expect} from 'chai'; -import {ReleaseNotes} from '../src/release-notes'; -import {buildCommitFromFixture, buildMockCommit, safeSnapshot} from './helpers'; -import {parseConventionalCommits} from '../src/commit'; +import { + buildCommitFromFixture, + buildMockCommit, + safeSnapshot, +} from '../helpers'; +import {DefaultChangelogNotes} from '../../src/changelog-notes/default'; +import {parseConventionalCommits} from '../../src/commit'; -describe('ReleaseNotes', () => { +describe('DefaultChangelogNotes', () => { const commits = [ { sha: 'sha1', @@ -63,20 +67,20 @@ describe('ReleaseNotes', () => { currentTag: 'v1.2.3', }; it('should build default release notes', async () => { - const releaseNotes = new ReleaseNotes(); - const notes = await releaseNotes.buildNotes(commits, notesOptions); + const changelogNotes = new DefaultChangelogNotes(); + const notes = await changelogNotes.buildNotes(commits, notesOptions); expect(notes).to.is.string; safeSnapshot(notes); }); it('should build with custom changelog sections', async () => { - const releaseNotes = new ReleaseNotes({ + const changelogNotes = new DefaultChangelogNotes({ changelogSections: [ {type: 'feat', section: 'Features'}, {type: 'fix', section: 'Bug Fixes'}, {type: 'docs', section: 'Documentation'}, ], }); - const notes = await releaseNotes.buildNotes(commits, notesOptions); + const notes = await changelogNotes.buildNotes(commits, notesOptions); expect(notes).to.is.string; safeSnapshot(notes); }); @@ -94,8 +98,8 @@ describe('ReleaseNotes', () => { breaking: true, }, ]; - const releaseNotes = new ReleaseNotes(); - const notes = await releaseNotes.buildNotes(commits, notesOptions); + const changelogNotes = new DefaultChangelogNotes(); + const notes = await changelogNotes.buildNotes(commits, notesOptions); expect(notes).to.is.string; safeSnapshot(notes); }); @@ -113,16 +117,16 @@ describe('ReleaseNotes', () => { breaking: true, }, ]; - const releaseNotes = new ReleaseNotes(); - const notes = await releaseNotes.buildNotes(commits, notesOptions); + const changelogNotes = new DefaultChangelogNotes(); + const notes = await changelogNotes.buildNotes(commits, notesOptions); expect(notes).to.is.string; safeSnapshot(notes); }); describe('with commit parsing', () => { it('should handle a breaking change', async () => { const commits = [buildMockCommit('fix!: some bugfix')]; - const releaseNotes = new ReleaseNotes(); - const notes = await releaseNotes.buildNotes( + const changelogNotes = new DefaultChangelogNotes(); + const notes = await changelogNotes.buildNotes( parseConventionalCommits(commits), notesOptions ); @@ -131,8 +135,8 @@ describe('ReleaseNotes', () => { }); it('should parse multiple commit messages from a single commit', async () => { const commits = [buildCommitFromFixture('multiple-messages')]; - const releaseNotes = new ReleaseNotes(); - const notes = await releaseNotes.buildNotes( + const changelogNotes = new DefaultChangelogNotes(); + const notes = await changelogNotes.buildNotes( parseConventionalCommits(commits), notesOptions ); @@ -141,8 +145,8 @@ describe('ReleaseNotes', () => { }); it('should handle BREAKING CHANGE body', async () => { const commits = [buildCommitFromFixture('breaking-body')]; - const releaseNotes = new ReleaseNotes(); - const notes = await releaseNotes.buildNotes( + const changelogNotes = new DefaultChangelogNotes(); + const notes = await changelogNotes.buildNotes( parseConventionalCommits(commits), notesOptions ); @@ -151,8 +155,8 @@ describe('ReleaseNotes', () => { }); it('should handle bug links', async () => { const commits = [buildCommitFromFixture('bug-link')]; - const releaseNotes = new ReleaseNotes(); - const notes = await releaseNotes.buildNotes( + const changelogNotes = new DefaultChangelogNotes(); + const notes = await changelogNotes.buildNotes( parseConventionalCommits(commits), notesOptions ); @@ -161,8 +165,8 @@ describe('ReleaseNotes', () => { }); it('should handle git trailers', async () => { const commits = [buildCommitFromFixture('git-trailers-with-breaking')]; - const releaseNotes = new ReleaseNotes(); - const notes = await releaseNotes.buildNotes( + const changelogNotes = new DefaultChangelogNotes(); + const notes = await changelogNotes.buildNotes( parseConventionalCommits(commits), notesOptions ); @@ -171,8 +175,8 @@ describe('ReleaseNotes', () => { }); it('should handle meta commits', async () => { const commits = [buildCommitFromFixture('meta')]; - const releaseNotes = new ReleaseNotes(); - const notes = await releaseNotes.buildNotes( + const changelogNotes = new DefaultChangelogNotes(); + const notes = await changelogNotes.buildNotes( parseConventionalCommits(commits), notesOptions ); @@ -181,8 +185,8 @@ describe('ReleaseNotes', () => { }); it('should handle multi-line breaking changes', async () => { const commits = [buildCommitFromFixture('multi-line-breaking-body')]; - const releaseNotes = new ReleaseNotes(); - const notes = await releaseNotes.buildNotes( + const changelogNotes = new DefaultChangelogNotes(); + const notes = await changelogNotes.buildNotes( parseConventionalCommits(commits), notesOptions ); @@ -193,8 +197,8 @@ describe('ReleaseNotes', () => { const commits = [ buildCommitFromFixture('multi-line-breaking-body-list'), ]; - const releaseNotes = new ReleaseNotes(); - const notes = await releaseNotes.buildNotes( + const changelogNotes = new DefaultChangelogNotes(); + const notes = await changelogNotes.buildNotes( parseConventionalCommits(commits), notesOptions ); @@ -203,8 +207,8 @@ describe('ReleaseNotes', () => { }); it('should not include content two newlines after BREAKING CHANGE', async () => { const commits = [buildCommitFromFixture('breaking-body-content-after')]; - const releaseNotes = new ReleaseNotes(); - const notes = await releaseNotes.buildNotes( + const changelogNotes = new DefaultChangelogNotes(); + const notes = await changelogNotes.buildNotes( parseConventionalCommits(commits), notesOptions ); @@ -213,8 +217,8 @@ describe('ReleaseNotes', () => { }); it('handles Release-As footers', async () => { const commits = [buildCommitFromFixture('release-as')]; - const releaseNotes = new ReleaseNotes(); - const notes = await releaseNotes.buildNotes( + const changelogNotes = new DefaultChangelogNotes(); + const notes = await changelogNotes.buildNotes( parseConventionalCommits(commits), notesOptions ); @@ -223,8 +227,8 @@ describe('ReleaseNotes', () => { }); // it('ignores reverted commits', async () => { // const commits = [buildCommitFromFixture('multiple-messages')]; - // const releaseNotes = new ReleaseNotes(); - // const notes = await releaseNotes.buildNotes(parseConventionalCommits(commits), notesOptions); + // const changelogNotes = new DefaultChangelogNotes(); + // const notes = await changelogNotes.buildNotes(parseConventionalCommits(commits), notesOptions); // expect(notes).to.is.string; // safeSnapshot(notes); // }); diff --git a/test/cli.ts b/test/cli.ts index 328d0d3eb..a1bf0e9b2 100644 --- a/test/cli.ts +++ b/test/cli.ts @@ -98,7 +98,17 @@ describe('CLI', () => { .resolves(fakeManifest); createPullRequestsStub = sandbox .stub(fakeManifest, 'createPullRequests') - .resolves([123]); + .resolves([ + { + title: 'fake title', + body: 'fake body', + headBranchName: 'head-branch-name', + baseBranchName: 'base-branch-name', + number: 123, + files: [], + labels: [], + }, + ]); }); it('instantiates a basic Manifest', async () => { await await parser.parseAsync( @@ -282,6 +292,11 @@ describe('CLI', () => { sha: 'abc123', notes: 'some release notes', url: 'url-of-release', + path: '.', + version: 'v1.2.3', + major: 1, + minor: 2, + patch: 3, }, ]); }); @@ -435,16 +450,26 @@ describe('CLI', () => { describe('release-pr', () => { describe('with manifest options', () => { let fromManifestStub: sinon.SinonStub; + let createPullRequestsStub: sinon.SinonStub; beforeEach(() => { fromManifestStub = sandbox .stub(Manifest, 'fromManifest') .resolves(fakeManifest); + createPullRequestsStub = sandbox + .stub(fakeManifest, 'createPullRequests') + .resolves([ + { + title: 'fake title', + body: 'fake body', + headBranchName: 'head-branch-name', + baseBranchName: 'base-branch-name', + number: 123, + files: [], + labels: [], + }, + ]); }); it('instantiates a basic Manifest', async () => { - const createPullRequestsStub = sandbox - .stub(fakeManifest, 'createPullRequests') - .resolves([123]); - await parser.parseAsync( 'release-pr --repo-url=googleapis/release-please-cli' ); @@ -465,10 +490,6 @@ describe('CLI', () => { sinon.assert.calledOnce(createPullRequestsStub); }); it('instantiates Manifest with custom config/manifest', async () => { - const createPullRequestsStub = sandbox - .stub(fakeManifest, 'createPullRequests') - .resolves([123]); - await parser.parseAsync( 'release-pr --repo-url=googleapis/release-please-cli --config-file=foo.json --manifest-file=.bar.json' ); @@ -490,10 +511,6 @@ describe('CLI', () => { }); for (const flag of ['--target-branch', '--default-branch']) { it(`handles ${flag}`, async () => { - const createPullRequestsStub = sandbox - .stub(fakeManifest, 'createPullRequests') - .resolves([123]); - await parser.parseAsync( `release-pr --repo-url=googleapis/release-please-cli ${flag}=1.x` ); @@ -548,7 +565,17 @@ describe('CLI', () => { .resolves(fakeManifest); createPullRequestsStub = sandbox .stub(fakeManifest, 'createPullRequests') - .resolves([123]); + .resolves([ + { + title: 'fake title', + body: 'fake body', + headBranchName: 'head-branch-name', + baseBranchName: 'base-branch-name', + number: 123, + files: [], + labels: [], + }, + ]); }); it('instantiates a basic Manifest', async () => { await parser.parseAsync( @@ -886,6 +913,27 @@ describe('CLI', () => { ); sinon.assert.calledOnce(createPullRequestsStub); }); + + it('handles --monorepo-tags', async () => { + await parser.parseAsync( + 'release-pr --repo-url=googleapis/release-please-cli --release-type=java-yoshi --monorepo-tags' + ); + + sinon.assert.calledOnceWithExactly(gitHubCreateStub, { + owner: 'googleapis', + repo: 'release-please-cli', + token: undefined, + }); + sinon.assert.calledOnceWithExactly( + fromConfigStub, + fakeGitHub, + 'main', + sinon.match({releaseType: 'java-yoshi', includeComponentInTag: true}), + sinon.match.any, + undefined + ); + sinon.assert.calledOnce(createPullRequestsStub); + }); }); }); describe('github-release', () => { @@ -904,6 +952,11 @@ describe('CLI', () => { sha: 'abc123', notes: 'some release notes', url: 'url-of-release', + path: '.', + version: 'v1.2.3', + major: 1, + minor: 2, + patch: 3, }, ]); }); @@ -1054,6 +1107,11 @@ describe('CLI', () => { sha: 'abc123', notes: 'some release notes', url: 'url-of-release', + path: '.', + version: 'v1.2.3', + major: 1, + minor: 2, + patch: 3, }, ]); }); @@ -1205,6 +1263,27 @@ describe('CLI', () => { ); sinon.assert.calledOnce(createReleasesStub); }); + + it('handles --monorepo-tags', async () => { + await parser.parseAsync( + 'github-release --repo-url=googleapis/release-please-cli --release-type=java-yoshi --monorepo-tags' + ); + + sinon.assert.calledOnceWithExactly(gitHubCreateStub, { + owner: 'googleapis', + repo: 'release-please-cli', + token: undefined, + }); + sinon.assert.calledOnceWithExactly( + fromConfigStub, + fakeGitHub, + 'main', + sinon.match({releaseType: 'java-yoshi', includeComponentInTag: true}), + sinon.match.any, + undefined + ); + sinon.assert.calledOnce(createReleasesStub); + }); }); }); diff --git a/test/manifest.ts b/test/manifest.ts index 514b1710c..1b1232c68 100644 --- a/test/manifest.ts +++ b/test/manifest.ts @@ -180,6 +180,91 @@ describe('Manifest', () => { expect(Object.keys(manifest.repositoryConfig)).lengthOf(1); expect(Object.keys(manifest.releasedVersions)).lengthOf(1); }); + it('should find custom release pull request title', async () => { + mockCommits(github, [ + { + sha: 'abc123', + message: 'some commit message', + files: [], + pullRequest: { + headBranchName: 'release-please/branches/main', + baseBranchName: 'main', + title: 'release: 1.2.3', + number: 123, + body: '', + labels: [], + files: [], + }, + }, + ]); + + const manifest = await Manifest.fromConfig(github, 'target-branch', { + releaseType: 'simple', + bumpMinorPreMajor: true, + bumpPatchForMinorPreMajor: true, + pullRequestTitlePattern: 'release: ${version}', + component: 'foobar', + includeComponentInTag: false, + }); + expect(Object.keys(manifest.repositoryConfig)).lengthOf(1); + expect(Object.keys(manifest.releasedVersions)).lengthOf(1); + }); + it('finds previous release without tag', async () => { + mockCommits(github, [ + { + sha: 'abc123', + message: 'some commit message', + files: [], + pullRequest: { + title: 'chore: release 1.2.3', + headBranchName: 'release-please/branches/main', + baseBranchName: 'main', + number: 123, + body: '', + labels: [], + files: [], + }, + }, + ]); + + const manifest = await Manifest.fromConfig(github, 'target-branch', { + releaseType: 'simple', + bumpMinorPreMajor: true, + bumpPatchForMinorPreMajor: true, + component: 'foobar', + includeComponentInTag: false, + }); + expect(Object.keys(manifest.repositoryConfig)).lengthOf(1); + expect(Object.keys(manifest.releasedVersions)).lengthOf(1); + }); + it('finds previous release with tag', async () => { + mockCommits(github, [ + { + sha: 'abc123', + message: 'some commit message', + files: [], + pullRequest: { + headBranchName: 'release-please/branches/main/components/foobar', + baseBranchName: 'main', + number: 123, + title: 'chore: release foobar 1.2.3', + body: '', + labels: [], + files: [], + }, + }, + ]); + + const manifest = await Manifest.fromConfig(github, 'target-branch', { + releaseType: 'simple', + bumpMinorPreMajor: true, + bumpPatchForMinorPreMajor: true, + component: 'foobar', + includeComponentInTag: true, + }); + expect(Object.keys(manifest.repositoryConfig)).lengthOf(1); + expect(Object.keys(manifest.releasedVersions)).lengthOf(1); + }); }); describe('buildPullRequests', () => { @@ -237,6 +322,9 @@ describe('Manifest', () => { assertHasUpdate(pullRequest.updates, 'CHANGELOG.md'); assertHasUpdate(pullRequest.updates, 'version.txt'); assertHasUpdate(pullRequest.updates, '.release-please-manifest.json'); + expect(pullRequest.headRefName).to.eql( + 'release-please--branches--main' + ); }); it('should create a draft pull request', async () => { @@ -302,6 +390,26 @@ describe('Manifest', () => { const pullRequest = pullRequests[0]; expect(pullRequest.labels).to.eql(['some-special-label']); }); + + it('allows customizing pull request title', async () => { + const manifest = new Manifest( + github, + 'main', + { + '.': { + releaseType: 'simple', + pullRequestTitlePattern: 'release: ${version}', + }, + }, + { + '.': Version.parse('1.0.0'), + } + ); + const pullRequests = await manifest.buildPullRequests(); + expect(pullRequests).lengthOf(1); + const pullRequest = pullRequests[0]; + expect(pullRequest.title.toString()).to.eql('release: 1.0.1'); + }); }); it('should find the component from config', async () => { @@ -362,6 +470,9 @@ describe('Manifest', () => { expect(pullRequests).lengthOf(1); const pullRequest = pullRequests[0]; expect(pullRequest.version?.toString()).to.eql('1.0.1'); + expect(pullRequest.headRefName).to.eql( + 'release-please--branches--main--components--pkg1' + ); }); it('should handle multiple package repository', async () => { @@ -1023,8 +1134,8 @@ describe('Manifest', () => { } ); sandbox.stub(manifest, 'buildPullRequests').resolves([]); - const pullRequestNumbers = await manifest.createPullRequests(); - expect(pullRequestNumbers).to.be.empty; + const pullRequests = await manifest.createPullRequests(); + expect(pullRequests).to.be.empty; }); it('handles a single pull request', async function () { @@ -1085,8 +1196,8 @@ describe('Manifest', () => { draft: false, }, ]); - const pullRequestNumbers = await manifest.createPullRequests(); - expect(pullRequestNumbers).lengthOf(1); + const pullRequests = await manifest.createPullRequests(); + expect(pullRequests).lengthOf(1); }); it('handles a multiple pull requests', async () => { @@ -1207,8 +1318,10 @@ describe('Manifest', () => { draft: false, }, ]); - const pullRequestNumbers = await manifest.createPullRequests(); - expect(pullRequestNumbers).to.eql([123, 124]); + const pullRequests = await manifest.createPullRequests(); + expect(pullRequests.map(pullRequest => pullRequest!.number)).to.eql([ + 123, 124, + ]); }); it('handles signoff users', async function () { @@ -1531,6 +1644,7 @@ describe('Manifest', () => { expect(releases[0].notes) .to.be.a('string') .and.satisfy((msg: string) => msg.startsWith('### Bug Fixes')); + expect(releases[0].path).to.eql('.'); }); it('should handle a multiple manifest release', async () => { @@ -1615,21 +1729,25 @@ describe('Manifest', () => { expect(releases[0].notes) .to.be.a('string') .and.satisfy((msg: string) => msg.startsWith('### Features')); + expect(releases[0].path).to.eql('packages/bot-config-utils'); expect(releases[1].tag.toString()).to.eql('label-utils-v1.1.0'); expect(releases[1].sha).to.eql('abc123'); expect(releases[1].notes) .to.be.a('string') .and.satisfy((msg: string) => msg.startsWith('### Features')); + expect(releases[1].path).to.eql('packages/label-utils'); expect(releases[2].tag.toString()).to.eql('object-selector-v1.1.0'); expect(releases[2].sha).to.eql('abc123'); expect(releases[2].notes) .to.be.a('string') .and.satisfy((msg: string) => msg.startsWith('### Features')); + expect(releases[2].path).to.eql('packages/object-selector'); expect(releases[3].tag.toString()).to.eql('datastore-lock-v2.1.0'); expect(releases[3].sha).to.eql('abc123'); expect(releases[3].notes) .to.be.a('string') .and.satisfy((msg: string) => msg.startsWith('### Features')); + expect(releases[3].path).to.eql('packages/datastore-lock'); }); it('should handle a single standalone release', async () => { @@ -1668,6 +1786,7 @@ describe('Manifest', () => { expect(releases[0].notes) .to.be.a('string') .and.satisfy((msg: string) => msg.startsWith('### [3.2.7]')); + expect(releases[0].path).to.eql('.'); }); it('should allow skipping releases', async () => { @@ -1858,6 +1977,92 @@ describe('Manifest', () => { expect(releases).lengthOf(1); expect(releases[0].draft).to.be.true; }); + + it('should skip component in tag', async () => { + mockPullRequests( + github, + [], + [ + { + headBranchName: 'release-please/branches/main', + baseBranchName: 'main', + number: 1234, + title: 'chore(main): release v1.3.1', + body: pullRequestBody('release-notes/single.txt'), + labels: ['autorelease: pending'], + files: [], + sha: 'abc123', + }, + ] + ); + const getFileContentsStub = sandbox.stub( + github, + 'getFileContentsOnBranch' + ); + getFileContentsStub + .withArgs('package.json', 'main') + .resolves( + buildGitHubFileRaw( + JSON.stringify({name: '@google-cloud/release-brancher'}) + ) + ); + const manifest = new Manifest( + github, + 'main', + { + '.': { + releaseType: 'node', + includeComponentInTag: false, + }, + }, + { + '.': Version.parse('1.3.0'), + } + ); + const releases = await manifest.buildReleases(); + expect(releases).lengthOf(1); + expect(releases[0].tag.toString()).to.eql('v1.3.1'); + }); + + it('should handle customized pull request title', async () => { + mockPullRequests( + github, + [], + [ + { + headBranchName: 'release-please/branches/main', + baseBranchName: 'main', + number: 1234, + title: 'release: 3.2.7', + body: pullRequestBody('release-notes/single.txt'), + labels: ['autorelease: pending'], + files: [], + sha: 'abc123', + }, + ] + ); + const manifest = new Manifest( + github, + 'main', + { + '.': { + releaseType: 'simple', + pullRequestTitlePattern: 'release: ${version}', + }, + }, + { + '.': Version.parse('3.2.6'), + } + ); + const releases = await manifest.buildReleases(); + expect(releases).lengthOf(1); + expect(releases[0].tag.toString()).to.eql('v3.2.7'); + expect(releases[0].sha).to.eql('abc123'); + expect(releases[0].notes) + .to.be.a('string') + .and.satisfy((msg: string) => msg.startsWith('### [3.2.7]')); + expect(releases[0].path).to.eql('.'); + }); }); describe('createReleases', () => { @@ -1914,6 +2119,7 @@ describe('Manifest', () => { expect(releases[0]!.tagName).to.eql('release-brancher-v1.3.1'); expect(releases[0]!.sha).to.eql('abc123'); expect(releases[0]!.notes).to.eql('some release notes'); + expect(releases[0]!.path).to.eql('.'); sinon.assert.calledOnce(commentStub); sinon.assert.calledOnceWithExactly( addLabelsStub, @@ -2018,15 +2224,19 @@ describe('Manifest', () => { expect(releases[0]!.tagName).to.eql('bot-config-utils-v3.2.0'); expect(releases[0]!.sha).to.eql('abc123'); expect(releases[0]!.notes).to.be.string; + expect(releases[0]!.path).to.eql('packages/bot-config-utils'); expect(releases[1]!.tagName).to.eql('label-utils-v1.1.0'); expect(releases[1]!.sha).to.eql('abc123'); expect(releases[1]!.notes).to.be.string; + expect(releases[1]!.path).to.eql('packages/label-utils'); expect(releases[2]!.tagName).to.eql('object-selector-v1.1.0'); expect(releases[2]!.sha).to.eql('abc123'); expect(releases[2]!.notes).to.be.string; + expect(releases[2]!.path).to.eql('packages/object-selector'); expect(releases[3]!.tagName).to.eql('datastore-lock-v2.1.0'); expect(releases[3]!.sha).to.eql('abc123'); expect(releases[3]!.notes).to.be.string; + expect(releases[3]!.path).to.eql('packages/datastore-lock'); sinon.assert.callCount(commentStub, 4); sinon.assert.calledOnceWithExactly( addLabelsStub, @@ -2079,6 +2289,7 @@ describe('Manifest', () => { expect(releases[0]!.tagName).to.eql('v3.2.7'); expect(releases[0]!.sha).to.eql('abc123'); expect(releases[0]!.notes).to.be.string; + expect(releases[0]!.path).to.eql('.'); sinon.assert.calledOnce(commentStub); sinon.assert.calledOnceWithExactly( addLabelsStub, diff --git a/test/strategies/base.ts b/test/strategies/base.ts index aa778cf0a..2ec952a7b 100644 --- a/test/strategies/base.ts +++ b/test/strategies/base.ts @@ -50,6 +50,27 @@ describe('Strategy', () => { const pullRequest = await strategy.buildReleasePullRequest([]); expect(pullRequest).to.be.undefined; }); + }); + describe('buildRelease', () => { + it('builds a release tag', async () => { + const strategy = new TestStrategy({ + targetBranch: 'main', + github, + component: 'google-cloud-automl', + }); + const release = await strategy.buildRelease({ + title: 'chore(main): release v1.2.3', + headBranchName: 'release-please/branches/main', + baseBranchName: 'main', + number: 1234, + body: new PullRequestBody([]).toString(), + labels: [], + files: [], + sha: 'abc123', + }); + expect(release, 'Release').to.not.be.undefined; + expect(release!.tag.toString()).to.eql('google-cloud-automl-v1.2.3'); + }); it('overrides the tag separator', async () => { const strategy = new TestStrategy({ targetBranch: 'main', @@ -68,7 +89,27 @@ describe('Strategy', () => { sha: 'abc123', }); expect(release, 'Release').to.not.be.undefined; - expect(release!.tag.separator).to.eql('/'); + expect(release!.tag.toString()).to.eql('google-cloud-automl/v1.2.3'); + }); + it('skips component in release tag', async () => { + const strategy = new TestStrategy({ + targetBranch: 'main', + github, + component: 'google-cloud-automl', + includeComponentInTag: false, + }); + const release = await strategy.buildRelease({ + title: 'chore(main): release v1.2.3', + headBranchName: 'release-please/branches/main', + baseBranchName: 'main', + number: 1234, + body: new PullRequestBody([]).toString(), + labels: [], + files: [], + sha: 'abc123', + }); + expect(release, 'Release').to.not.be.undefined; + expect(release!.tag.toString()).to.eql('v1.2.3'); }); }); }); diff --git a/test/util/branch-name.ts b/test/util/branch-name.ts index 4c1e5b6e6..bf4786d5b 100644 --- a/test/util/branch-name.ts +++ b/test/util/branch-name.ts @@ -39,8 +39,30 @@ describe('BranchName', () => { expect(branchName?.toString()).to.eql(name); }); }); + describe('v12 format', () => { + it('parses a target branch', () => { + const name = 'release-please/branches/main'; + const branchName = BranchName.parse(name); + expect(branchName).to.not.be.undefined; + expect(branchName?.getTargetBranch()).to.eql('main'); + expect(branchName?.getComponent()).to.be.undefined; + expect(branchName?.getVersion()).to.be.undefined; + expect(branchName?.toString()).to.eql(name); + }); + + it('parses a target branch and component', () => { + const name = 'release-please/branches/main/components/storage'; + const branchName = BranchName.parse(name); + expect(branchName).to.not.be.undefined; + expect(branchName?.getTargetBranch()).to.eql('main'); + expect(branchName?.getComponent()).to.eql('storage'); + expect(branchName?.getVersion()).to.be.undefined; + expect(branchName?.toString()).to.eql(name); + }); + }); + it('parses a target branch', () => { - const name = 'release-please/branches/main'; + const name = 'release-please--branches--main'; const branchName = BranchName.parse(name); expect(branchName).to.not.be.undefined; expect(branchName?.getTargetBranch()).to.eql('main'); @@ -50,7 +72,7 @@ describe('BranchName', () => { }); it('parses a target branch and component', () => { - const name = 'release-please/branches/main/components/storage'; + const name = 'release-please--branches--main--components--storage'; const branchName = BranchName.parse(name); expect(branchName).to.not.be.undefined; expect(branchName?.getTargetBranch()).to.eql('main'); @@ -82,14 +104,14 @@ describe('BranchName', () => { describe('ofTargetBranch', () => { it('builds branchname with only target branch', () => { const branchName = BranchName.ofTargetBranch('main'); - expect(branchName.toString()).to.eql('release-please/branches/main'); + expect(branchName.toString()).to.eql('release-please--branches--main'); }); }); describe('ofComponentTargetBranch', () => { it('builds branchname with target branch and component', () => { const branchName = BranchName.ofComponentTargetBranch('foo', 'main'); expect(branchName.toString()).to.eql( - 'release-please/branches/main/components/foo' + 'release-please--branches--main--components--foo' ); }); });