From 0a2d5c6fc53feb01ad66b937bb39c20fa4ba53b0 Mon Sep 17 00:00:00 2001 From: Hubert Date: Tue, 17 Sep 2024 00:04:54 +0200 Subject: [PATCH] feat: Introduce `componentNoSpace` parameter (Removes whitespace character from `${component}` title pattern) (#2330) * fix: Remove space from pull-request-title.ts * test: Fix failing tests * feat: Introduce new parameter `componentNoSpace`; Partially revert previous changes * feat: Update docs * chore: Remove unnecessary `console.log()` * style: Run `npm fix` across the project * chore: Update snapshots * feat: Add `componentNoSpace` parameter to merge plugin * style: Adjust code style; Run `npm fix` * fix: Docs parameter name --- __snapshots__/cli.js | 7 + __snapshots__/manifest.js | 44 +++++- docs/cli.md | 55 +++---- docs/customizing.md | 8 +- schemas/config.json | 11 +- src/bin/release-please.ts | 8 + src/manifest.ts | 13 +- src/plugins/merge.ts | 6 +- src/strategies/base.ts | 9 +- src/strategies/java.ts | 1 + src/updaters/release-please-config.ts | 1 + src/util/pull-request-title.ts | 72 +++++++-- test/manifest.ts | 220 +++++++++++++++++++++++++- test/util/pull-request-title.ts | 180 ++++++++++++++++++++- 14 files changed, 589 insertions(+), 46 deletions(-) diff --git a/__snapshots__/cli.js b/__snapshots__/cli.js index 60389f1fa..6b74353e2 100644 --- a/__snapshots__/cli.js +++ b/__snapshots__/cli.js @@ -32,6 +32,9 @@ Options: --pull-request-title-pattern Title pattern to make release PR [string] --pull-request-header Header for release PR [string] --pull-request-footer Footer for release PR [string] + --component-no-space release-please automatically adds \` \` (space) in + front of parsed \${component}. Should this be + disabled? [boolean] [default: false] --path release from path other than root directory [string] --component name of component release is being minted for @@ -233,6 +236,10 @@ Options: --pull-request-title-pattern Title pattern to make release PR [string] --pull-request-header Header for release PR [string] --pull-request-footer Footer for release PR [string] + --component-no-space release-please automatically adds \` \` + (space) in front of parsed \${component}. + Should this be disabled? + [boolean] [default: false] --path release from path other than root directory [string] --component name of component release is being minted diff --git a/__snapshots__/manifest.js b/__snapshots__/manifest.js index 2e175987c..fccd533c3 100644 --- a/__snapshots__/manifest.js +++ b/__snapshots__/manifest.js @@ -30,7 +30,49 @@ exports['Manifest buildPullRequests should allow creating multiple pull requests This PR was generated with [Release Please](https://github.com/googleapis/release-please). See [documentation](https://github.com/googleapis/release-please#release-please). ` -exports['Manifest buildPullRequests should allow customizing pull request title with root package 1'] = ` +exports['Manifest buildPullRequests should allow customizing pull request title with root package with SPACE in component 1'] = ` +:robot: I have created a release *beep* *boop* +--- + + +
root: 1.2.2 + +## [1.2.2](https://github.com/fake-owner/fake-repo/compare/root-v1.2.1...root-v1.2.2) (1983-10-10) + + +### Bug Fixes + +* some bugfix ([aaaaaa](https://github.com/fake-owner/fake-repo/commit/aaaaaa)) +* some bugfix ([bbbbbb](https://github.com/fake-owner/fake-repo/commit/bbbbbb)) +* some bugfix ([cccccc](https://github.com/fake-owner/fake-repo/commit/cccccc)) +
+ +
pkg1: 1.0.2 + +## [1.0.2](https://github.com/fake-owner/fake-repo/compare/pkg1-v1.0.1...pkg1-v1.0.2) (1983-10-10) + + +### Bug Fixes + +* some bugfix ([aaaaaa](https://github.com/fake-owner/fake-repo/commit/aaaaaa)) +* some bugfix ([cccccc](https://github.com/fake-owner/fake-repo/commit/cccccc)) +
+ +
pkg2: 0.2.4 + +## [0.2.4](https://github.com/fake-owner/fake-repo/compare/pkg2-v0.2.3...pkg2-v0.2.4) (1983-10-10) + + +### Bug Fixes + +* some bugfix ([bbbbbb](https://github.com/fake-owner/fake-repo/commit/bbbbbb)) +
+ +--- +This PR was generated with [Release Please](https://github.com/googleapis/release-please). See [documentation](https://github.com/googleapis/release-please#release-please). +` + +exports['Manifest buildPullRequests should allow customizing pull request title with root package without SPACE in component 1'] = ` :robot: I have created a release *beep* *boop* --- diff --git a/docs/cli.md b/docs/cli.md index d6a6abedd..f688d41db 100644 --- a/docs/cli.md +++ b/docs/cli.md @@ -39,33 +39,34 @@ release-please bootstrap \ Extra options: -| Option | Type | Description | -| ------ | ---- | ----------- | -| `--config-file` | `string` | Override the path to the release-please config file. Defaults to `release-please-config.json` | -| `--manifest-file` | `string` | Override the path to the release-please manifest file. Defaults to `.release-please-manifest.json` | -| `--path` | `string` | Path for changes to consider part of this component's release. Defaults to `.` Other paths should be relative to the repository root and not include `.` | -| `--package-name` | `string` | Name of the package being released. Defaults to a value determined by the configured release type | -| `--component` | `string` | Name of the component used for branch naming and release tagging. Defaults to a normalized version based on the package name | -| `--release-type` | [`ReleaseType`](/docs/customizing.md#strategy-language-types-supported) | Language strategy that determines which files to update | -| `--initial-version` | `string` | Version string to set as the last released version of this package. Defaults to `0.0.0` | -| `--versioning-strategy` | [`VersioningStrategyType`](/docs/customizing.md#versioning-strategies) | Override method of determining SemVer version bumps based on commits. Defaults to `default` | -| `--bump-minor-pre-major` | `boolean` | Configuration option for the versioning strategy. If set, will bump the minor version for breaking changes for versions < 1.0.0 | -| `--bump-patch-for-minor-pre-major` | `boolean` | Configuration option for the versioning strategy. If set, will bump the patch version for features for versions < 1.0.0 | -| `--prerelease-type` | `string` | Configuration option for the prerelease versioning strategy. If prerelease strategy used and type set, will set the prerelease part of the version to the provided value in case prerelease part is not present. | -| `--draft` | `boolean` | If set, create releases as drafts | -| `--prerelease` | `boolean` | If set, create releases that are pre-major or pre-release version marked as pre-release on Github| -| `--draft-pull-request` | `boolean` | If set, create pull requests as drafts | -| `--label` | `string` | Comma-separated list of labels to apply to the release pull requests. Defaults to `autorelease: pending` | -| `--release-label` | `string` | Comma-separated list of labels to apply to the pull request after the release has been tagged. Defaults to `autorelease: tagged` | -| `--changelog-path` | `string` | Override the path to the managed CHANGELOG. Defaults to `CHANGELOG.md` | -| `--changelog-type` | [`ChangelogType`](/docs/customizing.md#changelog-types) | Strategy for building the changelog contents. Defaults to `default` | -| `--changelog-sections` | `string` | Comma-separated list of commit scopes to show in changelog headings | -| `--changelog-host` | `string` | Host for commit hyperlinks in the changelog. Defaults to `https://github.com` | -| `--pull-request-title-pattern` | `string` | Override the pull request title pattern. Defaults to `chore${scope}: release${component} ${version}` | -| `--pull-request-header` | `string` | Override the pull request header. Defaults to `:robot: I have created a release *beep* *boop*` | -| `--pull-request-footer` | `string` | Override the pull request footer. Defaults to `This PR was generated with Release Please. See documentation.` | -| `--extra-files` | `string[]` | Extra file paths for the release strategy to consider | -| `--version-file` | `string` | Ruby only. Path to the `version.rb` file | +| Option | Type | Description | +|------------------------------------|-------------------------------------------------------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `--config-file` | `string` | Override the path to the release-please config file. Defaults to `release-please-config.json` | +| `--manifest-file` | `string` | Override the path to the release-please manifest file. Defaults to `.release-please-manifest.json` | +| `--path` | `string` | Path for changes to consider part of this component's release. Defaults to `.` Other paths should be relative to the repository root and not include `.` | +| `--package-name` | `string` | Name of the package being released. Defaults to a value determined by the configured release type | +| `--component` | `string` | Name of the component used for branch naming and release tagging. Defaults to a normalized version based on the package name | +| `--release-type` | [`ReleaseType`](/docs/customizing.md#strategy-language-types-supported) | Language strategy that determines which files to update | +| `--initial-version` | `string` | Version string to set as the last released version of this package. Defaults to `0.0.0` | +| `--versioning-strategy` | [`VersioningStrategyType`](/docs/customizing.md#versioning-strategies) | Override method of determining SemVer version bumps based on commits. Defaults to `default` | +| `--bump-minor-pre-major` | `boolean` | Configuration option for the versioning strategy. If set, will bump the minor version for breaking changes for versions < 1.0.0 | +| `--bump-patch-for-minor-pre-major` | `boolean` | Configuration option for the versioning strategy. If set, will bump the patch version for features for versions < 1.0.0 | +| `--prerelease-type` | `string` | Configuration option for the prerelease versioning strategy. If prerelease strategy used and type set, will set the prerelease part of the version to the provided value in case prerelease part is not present. | +| `--draft` | `boolean` | If set, create releases as drafts | +| `--prerelease` | `boolean` | If set, create releases that are pre-major or pre-release version marked as pre-release on Github | +| `--draft-pull-request` | `boolean` | If set, create pull requests as drafts | +| `--label` | `string` | Comma-separated list of labels to apply to the release pull requests. Defaults to `autorelease: pending` | +| `--release-label` | `string` | Comma-separated list of labels to apply to the pull request after the release has been tagged. Defaults to `autorelease: tagged` | +| `--changelog-path` | `string` | Override the path to the managed CHANGELOG. Defaults to `CHANGELOG.md` | +| `--changelog-type` | [`ChangelogType`](/docs/customizing.md#changelog-types) | Strategy for building the changelog contents. Defaults to `default` | +| `--changelog-sections` | `string` | Comma-separated list of commit scopes to show in changelog headings | +| `--changelog-host` | `string` | Host for commit hyperlinks in the changelog. Defaults to `https://github.com` | +| `--pull-request-title-pattern` | `string` | Override the pull request title pattern. Defaults to `chore${scope}: release${component} ${version}` | +| `--pull-request-header` | `string` | Override the pull request header. Defaults to `:robot: I have created a release *beep* *boop*` | +| `--pull-request-footer` | `string` | Override the pull request footer. Defaults to `This PR was generated with Release Please. See documentation.` | +| `--component-no-space` | `boolean` | release-please automatically adds ` ` (space) in front of parsed ${component}. This option indicates whether that behaviour should be disabled. Defaults to `false` | +| `--extra-files` | `string[]` | Extra file paths for the release strategy to consider | +| `--version-file` | `string` | Ruby only. Path to the `version.rb` file | ## Creating/updating release PRs diff --git a/docs/customizing.md b/docs/customizing.md index 3f290f5d1..98c796789 100644 --- a/docs/customizing.md +++ b/docs/customizing.md @@ -101,7 +101,13 @@ title or body format). The default pull request title uses this pattern: `chore${scope}: release${component} ${version}` so a common release pull -request title would be `chore(main): release foo-bar v1.2.3`. +request title would be `chore(main): release foo-bar v1.2.3`. +Please note that by default `${component}` will be parsed to ` ${component}` (With space in front of). +If you wish to avoid that, consider using `component-no-space: true`/`--component-no-space=true` parameter. + +> [!WARNING] +> Setting `component-no-space` option when release PR already exists might break the parsing +> resulting in another PR being opened. | Pattern | Description | | ------- | ----------- | diff --git a/schemas/config.json b/schemas/config.json index e47aae6df..3fc917901 100644 --- a/schemas/config.json +++ b/schemas/config.json @@ -233,6 +233,10 @@ "initial-version": { "description": "Releases the initial library with a specified version", "type": "string" + }, + "component-no-space": { + "description": "release-please automatically adds ` ` (space) in front of parsed ${component}. This option indicates whether that behaviour should be disabled. Defaults to `false`", + "type": "boolean" } } } @@ -423,6 +427,10 @@ "release-label": { "description": "Comma-separated list of labels to add to a pull request that has been released/tagged", "type": "string" + }, + "component-no-space": { + "description": "release-please automatically adds ` ` (space) in front of parsed ${component}. This option indicates whether that behaviour should be disabled. Defaults to `false`", + "type": "boolean" } }, "required": ["packages"] @@ -467,6 +475,7 @@ "version-file": true, "snapshot-label": true, "initial-version": true, - "exclude-paths": true + "exclude-paths": true, + "component-no-space": false } } diff --git a/src/bin/release-please.ts b/src/bin/release-please.ts index 4baf2c626..b3240fc69 100644 --- a/src/bin/release-please.ts +++ b/src/bin/release-please.ts @@ -116,6 +116,7 @@ interface TaggingArgs { pullRequestTitlePattern?: string; pullRequestHeader?: string; pullRequestFooter?: string; + componentNoSpace?: boolean; } interface CreatePullRequestArgs @@ -424,6 +425,12 @@ function taggingOptions(yargs: yargs.Argv): yargs.Argv { .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, }); } @@ -464,6 +471,7 @@ const createReleasePullRequestCommand: yargs.CommandModule< pullRequestTitlePattern: argv.pullRequestTitlePattern, pullRequestHeader: argv.pullRequestHeader, pullRequestFooter: argv.pullRequestFooter, + componentNoSpace: argv.componentNoSpace, changelogSections: argv.changelogSections, releaseAs: argv.releaseAs, versioning: argv.versioningStrategy, diff --git a/src/manifest.ts b/src/manifest.ts index 281e20ee7..f152cd1c5 100644 --- a/src/manifest.ts +++ b/src/manifest.ts @@ -115,6 +115,7 @@ export interface ReleaserConfig { pullRequestTitlePattern?: string; pullRequestHeader?: string; pullRequestFooter?: string; + componentNoSpace?: boolean; tagSeparator?: string; separatePullRequests?: boolean; labels?: string[]; @@ -173,6 +174,7 @@ interface ReleaserConfigJson { 'pull-request-title-pattern'?: string; 'pull-request-header'?: string; 'pull-request-footer'?: string; + 'component-no-space'?: boolean; 'separate-pull-requests'?: boolean; 'tag-separator'?: string; 'extra-files'?: ExtraFile[]; @@ -788,6 +790,12 @@ export class Manifest { ) { mergeOptions.pullRequestFooter = config.pullRequestFooter; } + if ( + 'componentNoSpace' in config && + !('componentNoSpace' in mergeOptions) + ) { + mergeOptions.componentNoSpace = config.componentNoSpace; + } } this.plugins.push( new Merge( @@ -1368,6 +1376,7 @@ function extractReleaserConfig( pullRequestTitlePattern: config['pull-request-title-pattern'], pullRequestHeader: config['pull-request-header'], pullRequestFooter: config['pull-request-footer'], + componentNoSpace: config['component-no-space'], tagSeparator: config['tag-separator'], separatePullRequests: config['separate-pull-requests'], labels: config['label']?.split(','), @@ -1541,7 +1550,6 @@ function isPublishedVersion(strategy: Strategy, version: Version): boolean { * @param {string} targetBranch Name of the scanned branch. * @param releaseFilter Validator function for release version. Used to filter-out SNAPSHOT releases for Java strategy. * @param {string} prefix Limit the release to a specific component. - * @param pullRequestTitlePattern Configured PR title pattern. */ async function latestReleaseVersion( github: GitHub, @@ -1607,6 +1615,7 @@ async function latestReleaseVersion( const pullRequestTitle = PullRequestTitle.parse( mergedPullRequest.title, config.pullRequestTitlePattern, + config.componentNoSpace, logger ); if (!pullRequestTitle) { @@ -1725,6 +1734,8 @@ function mergeReleaserConfig( pathConfig.pullRequestHeader ?? defaultConfig.pullRequestHeader, pullRequestFooter: pathConfig.pullRequestFooter ?? defaultConfig.pullRequestFooter, + componentNoSpace: + pathConfig.componentNoSpace ?? defaultConfig.componentNoSpace, separatePullRequests: pathConfig.separatePullRequests ?? defaultConfig.separatePullRequests, skipSnapshot: pathConfig.skipSnapshot ?? defaultConfig.skipSnapshot, diff --git a/src/plugins/merge.ts b/src/plugins/merge.ts index 493e02626..811ac3f72 100644 --- a/src/plugins/merge.ts +++ b/src/plugins/merge.ts @@ -30,6 +30,7 @@ export interface MergeOptions { pullRequestTitlePattern?: string; pullRequestHeader?: string; pullRequestFooter?: string; + componentNoSpace?: boolean; headBranchName?: string; forceMerge?: boolean; } @@ -44,6 +45,7 @@ export class Merge extends ManifestPlugin { private pullRequestTitlePattern?: string; private pullRequestHeader?: string; private pullRequestFooter?: string; + private componentNoSpace?: boolean; private headBranchName?: string; private forceMerge: boolean; @@ -58,6 +60,7 @@ export class Merge extends ManifestPlugin { options.pullRequestTitlePattern ?? MANIFEST_PULL_REQUEST_TITLE_PATTERN; this.pullRequestHeader = options.pullRequestHeader; this.pullRequestFooter = options.pullRequestFooter; + this.componentNoSpace = options.componentNoSpace; this.headBranchName = options.headBranchName; this.forceMerge = options.forceMerge ?? false; } @@ -106,7 +109,8 @@ export class Merge extends ManifestPlugin { rootRelease?.pullRequest.title.component, this.targetBranch, rootRelease?.pullRequest.title.version, - this.pullRequestTitlePattern + this.pullRequestTitlePattern, + this.componentNoSpace ), body: new PullRequestBody(releaseData, { useComponents: true, diff --git a/src/strategies/base.ts b/src/strategies/base.ts index b076cc07b..00495e426 100644 --- a/src/strategies/base.ts +++ b/src/strategies/base.ts @@ -76,6 +76,7 @@ export interface BaseStrategyOptions { pullRequestTitlePattern?: string; pullRequestHeader?: string; pullRequestFooter?: string; + componentNoSpace?: boolean; extraFiles?: ExtraFile[]; versionFile?: string; snapshotLabels?: string[]; // Java-only @@ -109,6 +110,7 @@ export abstract class BaseStrategy implements Strategy { readonly pullRequestTitlePattern?: string; readonly pullRequestHeader?: string; readonly pullRequestFooter?: string; + readonly componentNoSpace?: boolean; readonly extraFiles: ExtraFile[]; readonly extraLabels: string[]; @@ -142,6 +144,7 @@ export abstract class BaseStrategy implements Strategy { 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 || []; @@ -292,11 +295,13 @@ export abstract class BaseStrategy implements Strategy { 'pull request title pattern:', this.pullRequestTitlePattern ); + this.logger.debug('componentNoSpace:', this.componentNoSpace); const pullRequestTitle = PullRequestTitle.ofComponentTargetBranchVersion( component || '', this.targetBranch, newVersion, - this.pullRequestTitlePattern + this.pullRequestTitlePattern, + this.componentNoSpace ); const branchComponent = await this.getBranchComponent(); const branchName = branchComponent @@ -580,11 +585,13 @@ export abstract class BaseStrategy implements Strategy { PullRequestTitle.parse( mergedPullRequest.title, this.pullRequestTitlePattern, + this.componentNoSpace, this.logger ) || PullRequestTitle.parse( mergedPullRequest.title, mergedTitlePattern, + this.componentNoSpace, this.logger ); if (!pullRequestTitle) { diff --git a/src/strategies/java.ts b/src/strategies/java.ts index 62ecc1b87..c4a2e1696 100644 --- a/src/strategies/java.ts +++ b/src/strategies/java.ts @@ -186,6 +186,7 @@ export class Java extends BaseStrategy { PullRequestTitle.parse( commit.pullRequest?.title || commit.message, this.pullRequestTitlePattern, + this.componentNoSpace, this.logger ) ) diff --git a/src/updaters/release-please-config.ts b/src/updaters/release-please-config.ts index feedfe1d5..4cea722fa 100644 --- a/src/updaters/release-please-config.ts +++ b/src/updaters/release-please-config.ts @@ -76,6 +76,7 @@ function releaserConfigToJsonConfig( 'pull-request-title-pattern': config.pullRequestTitlePattern, 'pull-request-header': config.pullRequestHeader, 'pull-request-footer': config.pullRequestFooter, + 'component-no-space': config.componentNoSpace, 'separate-pull-requests': config.separatePullRequests, 'tag-separator': config.tagSeparator, 'extra-files': config.extraFiles, diff --git a/src/util/pull-request-title.ts b/src/util/pull-request-title.ts index c9df13721..d5125332e 100644 --- a/src/util/pull-request-title.ts +++ b/src/util/pull-request-title.ts @@ -21,8 +21,11 @@ import {Version} from '../version'; const DEFAULT_PR_TITLE_PATTERN = 'chore${scope}: release${component} ${version}'; +const COMPONENT_NO_SPACE = false; + export function generateMatchPattern( pullRequestTitlePattern?: string, + componentNoSpace?: boolean, logger: Logger = defaultLogger ): RegExp { if ( @@ -47,7 +50,12 @@ export function generateMatchPattern( .replace('(', '\\(') .replace(')', '\\)') .replace('${scope}', '(\\((?[\\w-./]+)\\))?') - .replace('${component}', ' ?(?@?[\\w-./]*)?') + .replace( + '${component}', + componentNoSpace === true + ? '?(?@?[\\w-./]*)?' + : ' ?(?@?[\\w-./]*)?' + ) .replace('${version}', 'v?(?[0-9].*)') .replace('${branch}', '(?[\\w-./]+)?')}$` ); @@ -59,12 +67,14 @@ export class PullRequestTitle { version?: Version; pullRequestTitlePattern: string; matchPattern: RegExp; + componentNoSpace?: boolean; private constructor(opts: { version?: Version; component?: string; targetBranch?: string; pullRequestTitlePattern?: string; + componentNoSpace?: boolean; logger?: Logger; }) { this.version = opts.version; @@ -72,8 +82,10 @@ export class PullRequestTitle { this.targetBranch = opts.targetBranch; this.pullRequestTitlePattern = opts.pullRequestTitlePattern || DEFAULT_PR_TITLE_PATTERN; + this.componentNoSpace = opts.componentNoSpace || COMPONENT_NO_SPACE; this.matchPattern = generateMatchPattern( this.pullRequestTitlePattern, + this.componentNoSpace, opts.logger ); } @@ -81,9 +93,14 @@ export class PullRequestTitle { static parse( title: string, pullRequestTitlePattern?: string, + componentNoSpace?: boolean, logger: Logger = defaultLogger ): PullRequestTitle | undefined { - const matchPattern = generateMatchPattern(pullRequestTitlePattern, logger); + const matchPattern = generateMatchPattern( + pullRequestTitlePattern, + componentNoSpace, + logger + ); const match = title.match(matchPattern); if (match?.groups) { return new PullRequestTitle({ @@ -93,6 +110,7 @@ export class PullRequestTitle { component: match.groups['component'], targetBranch: match.groups['branch'], pullRequestTitlePattern, + componentNoSpace, logger, }); } @@ -102,47 +120,64 @@ export class PullRequestTitle { static ofComponentVersion( component: string, version: Version, - pullRequestTitlePattern?: string + pullRequestTitlePattern?: string, + componentNoSpace?: boolean ): PullRequestTitle { - return new PullRequestTitle({version, component, pullRequestTitlePattern}); + return new PullRequestTitle({ + version, + component, + pullRequestTitlePattern, + componentNoSpace, + }); } static ofVersion( version: Version, - pullRequestTitlePattern?: string + pullRequestTitlePattern?: string, + componentNoSpace?: boolean ): PullRequestTitle { - return new PullRequestTitle({version, pullRequestTitlePattern}); + return new PullRequestTitle({ + version, + pullRequestTitlePattern, + componentNoSpace, + }); } static ofTargetBranchVersion( targetBranch: string, version: Version, - pullRequestTitlePattern?: string + pullRequestTitlePattern?: string, + componentNoSpace?: boolean ): PullRequestTitle { return new PullRequestTitle({ version, targetBranch, pullRequestTitlePattern, + componentNoSpace, }); } static ofComponentTargetBranchVersion( component?: string, targetBranch?: string, version?: Version, - pullRequestTitlePattern?: string + pullRequestTitlePattern?: string, + componentNoSpace?: boolean ): PullRequestTitle { return new PullRequestTitle({ version, component, targetBranch, pullRequestTitlePattern, + componentNoSpace, }); } static ofTargetBranch( targetBranch: string, - pullRequestTitlePattern?: string + pullRequestTitlePattern?: string, + componentNoSpace?: boolean ): PullRequestTitle { return new PullRequestTitle({ targetBranch, pullRequestTitlePattern, + componentNoSpace, }); } @@ -158,8 +193,25 @@ export class PullRequestTitle { toString(): string { const scope = this.targetBranch ? `(${this.targetBranch})` : ''; - const component = this.component ? ` ${this.component}` : ''; + const component = + this.componentNoSpace === true + ? this.component + ? `${this.component}` + : '' + : this.component + ? ` ${this.component}` + : ''; const version = this.version ?? ''; + if (this.componentNoSpace === true && !component) { + console.log( + '`component` is empty. Removing component from title pattern..' + ); + this.pullRequestTitlePattern = this.pullRequestTitlePattern.replace( + '${component} ', + '' + ); + } + return this.pullRequestTitlePattern .replace('${scope}', scope) .replace('${component}', component) diff --git a/test/manifest.ts b/test/manifest.ts index cbf259f02..f8394290a 100644 --- a/test/manifest.ts +++ b/test/manifest.ts @@ -2351,7 +2351,7 @@ describe('Manifest', () => { expect(pullRequests[0].version?.toString()).to.eql('1.0.0'); }); - it('should allow customizing pull request title with root package', async () => { + it('should allow customizing pull request title with root package with SPACE in component', async () => { mockReleases(sandbox, github, [ { id: 1, @@ -2467,7 +2467,126 @@ describe('Manifest', () => { snapshot(dateSafe(pullRequest.body.toString())); }); - it('should allow customizing pull request title without root package', async () => { + it('should allow customizing pull request title with root package without SPACE in component', async () => { + mockReleases(sandbox, github, [ + { + id: 1, + sha: 'abc123', + tagName: 'pkg1-v1.0.0', + url: 'https://github.com/fake-owner/fake-repo/releases/tag/pkg1-v1.0.0', + }, + { + id: 2, + sha: 'abc123', + tagName: 'root-v1.2.0', + url: 'https://github.com/fake-owner/fake-repo/releases/tag/root-v1.2.0', + }, + { + id: 3, + sha: 'def234', + tagName: 'pkg1-v1.0.1', + url: 'https://github.com/fake-owner/fake-repo/releases/tag/pkg1-v1.0.1', + }, + { + id: 4, + sha: 'def234', + tagName: 'pkg2-v0.2.3', + url: 'https://github.com/fake-owner/fake-repo/releases/tag/pkg2-v0.2.3', + }, + { + id: 5, + sha: 'def234', + tagName: 'root-v1.2.1', + url: 'https://github.com/fake-owner/fake-repo/releases/tag/root-v1.2.1', + }, + ]); + mockCommits(sandbox, github, [ + { + sha: 'aaaaaa', + message: 'fix: some bugfix', + files: ['path/a/foo'], + }, + { + sha: 'abc123', + message: 'chore: release main', + files: [], + pullRequest: { + headBranchName: 'release-please/branches/main', + baseBranchName: 'main', + number: 123, + title: 'chore: release v1.2.0', + body: '', + labels: [], + files: [], + sha: 'abc123', + }, + }, + { + sha: 'bbbbbb', + message: 'fix: some bugfix', + files: ['path/b/foo'], + }, + { + sha: 'cccccc', + message: 'fix: some bugfix', + files: ['path/a/foo'], + }, + { + sha: 'def234', + message: 'chore: release v1.2.1', + files: [], + pullRequest: { + headBranchName: 'release-please/branches/main', + baseBranchName: 'main', + number: 123, + title: 'chore: release v1.2.1', + body: '', + labels: [], + files: [], + sha: 'def234', + }, + }, + ]); + const manifest = new Manifest( + github, + 'main', + { + '.': { + releaseType: 'simple', + component: 'root', + componentNoSpace: true, + }, + 'path/a': { + releaseType: 'simple', + component: 'pkg1', + componentNoSpace: true, + }, + 'path/b': { + releaseType: 'simple', + component: 'pkg2', + componentNoSpace: true, + }, + }, + { + '.': Version.parse('1.2.1'), + 'path/a': Version.parse('1.0.1'), + 'path/b': Version.parse('0.2.3'), + }, + { + groupPullRequestTitlePattern: + 'chore${scope}: release ${component} v${version}', + } + ); + const pullRequests = await manifest.buildPullRequests(); + expect(pullRequests).lengthOf(1); + const pullRequest = pullRequests[0]; + expect(pullRequest.title.toString()).to.eql( + 'chore(main): release root v1.2.2' + ); + snapshot(dateSafe(pullRequest.body.toString())); + }); + + it('should allow customizing pull request title without root package with space', async () => { mockReleases(sandbox, github, [ { id: 1, @@ -2562,6 +2681,103 @@ describe('Manifest', () => { expect(pullRequests[0].title.toString()).to.eql('chore(main): release v'); }); + it('should allow customizing pull request title without root package without space', async () => { + mockReleases(sandbox, github, [ + { + id: 1, + sha: 'abc123', + tagName: 'pkg1-v1.0.0', + url: 'https://github.com/fake-owner/fake-repo/releases/tag/pkg1-v1.0.0', + }, + { + id: 2, + sha: 'def234', + tagName: 'pkg1-v1.0.1', + url: 'https://github.com/fake-owner/fake-repo/releases/tag/pkg1-v1.0.1', + }, + { + id: 3, + sha: 'def234', + tagName: 'pkg2-v0.2.3', + url: 'https://github.com/fake-owner/fake-repo/releases/tag/pkg2-v0.2.3', + }, + ]); + mockCommits(sandbox, github, [ + { + sha: 'aaaaaa', + message: 'fix: some bugfix', + files: ['path/a/foo'], + }, + { + sha: 'abc123', + message: 'chore: release main', + files: [], + pullRequest: { + headBranchName: 'release-please/branches/main', + baseBranchName: 'main', + number: 123, + title: 'chore: release v1.2.0', + body: '', + labels: [], + files: [], + sha: 'abc123', + }, + }, + { + sha: 'bbbbbb', + message: 'fix: some bugfix', + files: ['path/b/foo'], + }, + { + sha: 'cccccc', + message: 'fix: some bugfix', + files: ['path/a/foo'], + }, + { + sha: 'def234', + message: 'chore: release v1.2.1', + files: [], + pullRequest: { + headBranchName: 'release-please/branches/main', + baseBranchName: 'main', + number: 123, + title: 'chore: release v1.2.1', + body: '', + labels: [], + files: [], + sha: 'def234', + }, + }, + ]); + const manifest = new Manifest( + github, + 'main', + { + 'path/a': { + releaseType: 'simple', + component: 'pkg1', + componentNoSpace: true, + }, + 'path/b': { + releaseType: 'simple', + component: 'pkg2', + componentNoSpace: true, + }, + }, + { + 'path/a': Version.parse('1.0.1'), + 'path/b': Version.parse('0.2.3'), + }, + { + groupPullRequestTitlePattern: + 'chore${scope}: release ${component} v${version}', + } + ); + const pullRequests = await manifest.buildPullRequests(); + expect(pullRequests).lengthOf(1); + expect(pullRequests[0].title.toString()).to.eql('chore(main): release v'); + }); + it('should read latest version from manifest if no release tag found', async () => { mockReleases(sandbox, github, []); mockCommits(sandbox, github, [ diff --git a/test/util/pull-request-title.ts b/test/util/pull-request-title.ts index c44447fbc..308559d61 100644 --- a/test/util/pull-request-title.ts +++ b/test/util/pull-request-title.ts @@ -139,7 +139,7 @@ describe('PullRequestTitle', () => { }); }); -describe('PullRequestTitle with custom pullRequestTitlePattern', () => { +describe('PullRequestTitle with custom pullRequestTitlePattern with SPACE in component', () => { describe('parse', () => { describe('autorelease branch name', () => { it('parses a versioned branch name', () => { @@ -320,3 +320,181 @@ describe('PullRequestTitle with custom pullRequestTitlePattern', () => { // }); }); }); + +describe('PullRequestTitle with custom pullRequestTitlePattern without SPACE in component', () => { + describe('parse', () => { + describe('autorelease branch name', () => { + it('parses a versioned branch name', () => { + const name = 'chore: 🔖 release 1.2.3'; + const pullRequestTitle = PullRequestTitle.parse( + name, + 'chore${scope}: 🔖 release ${component} ${version}', + true + ); + expect(pullRequestTitle).to.not.be.undefined; + expect(pullRequestTitle?.getTargetBranch()).to.be.undefined; + expect(pullRequestTitle?.getComponent()).to.be.undefined; + expect(pullRequestTitle?.getVersion()?.toString()).to.eql('1.2.3'); + expect(pullRequestTitle?.toString()).to.eql(name); + }); + it('parses a versioned branch name with v', () => { + const name = 'chore: 🔖 release v1.2.3'; + const pullRequestTitle = PullRequestTitle.parse( + name, + 'chore${scope}: 🔖 release ${component} ${version}', + true + ); + expect(pullRequestTitle).to.not.be.undefined; + expect(pullRequestTitle?.getTargetBranch()).to.be.undefined; + expect(pullRequestTitle?.getComponent()).to.be.undefined; + expect(pullRequestTitle?.getVersion()?.toString()).to.eql('1.2.3'); + }); + it('parses a versioned branch name with component', () => { + const name = 'chore: 🔖 release storage v1.2.3'; + const pullRequestTitle = PullRequestTitle.parse( + name, + 'chore${scope}: 🔖 release ${component} ${version}', + true + ); + expect(pullRequestTitle).to.not.be.undefined; + expect(pullRequestTitle?.getTargetBranch()).to.be.undefined; + expect(pullRequestTitle?.getComponent()).to.eql('storage'); + expect(pullRequestTitle?.getVersion()?.toString()).to.eql('1.2.3'); + }); + }); + + it('parses a target branch', () => { + const name = 'chore(main): 🔖 release v1.2.3'; + const pullRequestTitle = PullRequestTitle.parse( + name, + 'chore${scope}: 🔖 release ${component} ${version}', + true + ); + expect(pullRequestTitle).to.not.be.undefined; + expect(pullRequestTitle?.getTargetBranch()).to.eql('main'); + expect(pullRequestTitle?.getComponent()).to.be.undefined; + expect(pullRequestTitle?.getVersion()?.toString()).to.eql('1.2.3'); + }); + it('parses a target branch and component', () => { + const name = 'chore(main): 🔖 release storage v1.2.3'; + const pullRequestTitle = PullRequestTitle.parse( + name, + 'chore${scope}: 🔖 release ${component} ${version}', + true + ); + expect(pullRequestTitle).to.not.be.undefined; + expect(pullRequestTitle?.getTargetBranch()).to.eql('main'); + expect(pullRequestTitle?.getComponent()).to.eql('storage'); + expect(pullRequestTitle?.getVersion()?.toString()).to.eql('1.2.3'); + }); + it('parses a component with @ sign prefix', () => { + const name = 'chore(main): 🔖 release @example/storage v1.2.3'; + const pullRequestTitle = PullRequestTitle.parse( + name, + 'chore${scope}: 🔖 release ${component} ${version}', + true + ); + expect(pullRequestTitle).to.not.be.undefined; + expect(pullRequestTitle?.getComponent()).to.eql('@example/storage'); + }); + it('fails to parse', () => { + const pullRequestTitle = PullRequestTitle.parse( + 'release-foo', + 'chore${scope}: 🔖 release ${component} ${version}', + true + ); + expect(pullRequestTitle).to.be.undefined; + }); + it('parses a manifest title', () => { + const name = 'chore: release main'; + const pullRequestTitle = PullRequestTitle.parse( + name, + MANIFEST_PULL_REQUEST_TITLE_PATTERN, + true + ); + expect(pullRequestTitle).to.not.be.undefined; + expect(pullRequestTitle?.getTargetBranch()).to.eql('main'); + expect(pullRequestTitle?.getComponent()).to.be.undefined; + expect(pullRequestTitle?.getVersion()).to.be.undefined; + }); + it('parses a complex title and pattern', () => { + const pullRequestTitle = PullRequestTitle.parse( + '[HOTFIX] - chore(hotfix/v3.1.0-bug): release 3.1.0-hotfix1 (@example/storage)', + '[HOTFIX] - chore${scope}: release ${version} (${component})', + true + ); + expect(pullRequestTitle).to.not.be.undefined; + expect(pullRequestTitle?.getTargetBranch()).to.eql('hotfix/v3.1.0-bug'); + expect(pullRequestTitle?.getVersion()?.toString()).to.eql( + '3.1.0-hotfix1' + ); + expect(pullRequestTitle?.getComponent()).to.eql('@example/storage'); + }); + }); + + describe('ofVersion', () => { + it('builds the autorelease versioned branch name', () => { + const pullRequestTitle = PullRequestTitle.ofVersion( + Version.parse('1.2.3'), + 'chore${scope}: 🔖 release ${component} ${version}', + true + ); + expect(pullRequestTitle.toString()).to.eql('chore: 🔖 release 1.2.3'); + }); + }); + + describe('ofComponentVersion', () => { + it('builds the autorelease versioned branch name with component', () => { + const pullRequestTitle = PullRequestTitle.ofComponentVersion( + 'storage', + Version.parse('1.2.3'), + 'chore${scope}: 🔖 release ${component} ${version}', + true + ); + expect(pullRequestTitle.toString()).to.eql( + 'chore: 🔖 release storage 1.2.3' + ); + }); + }); + + describe('ofTargetBranch', () => { + it('builds branch name with only target branch', () => { + const pullRequestTitle = PullRequestTitle.ofTargetBranchVersion( + 'main', + Version.parse('1.2.3'), + 'chore${scope}: 🔖 release ${component} ${version}', + true + ); + expect(pullRequestTitle.toString()).to.eql( + 'chore(main): 🔖 release 1.2.3' + ); + }); + }); + + describe('ofComponentTargetBranch', () => { + it('builds branch name with target branch and component', () => { + const pullRequestTitle = PullRequestTitle.ofComponentTargetBranchVersion( + 'foo', + 'main', + Version.parse('1.2.3'), + 'chore${scope}: 🔖 release ${component} ${version}', + true + ); + expect(pullRequestTitle.toString()).to.eql( + 'chore(main): 🔖 release foo 1.2.3' + ); + }); + }); + + describe('generateMatchPattern', () => { + it('return matchPattern with custom Pattern', () => { + const matchPattern = generateMatchPattern( + 'chore${scope}: 🔖 release ${component} ${version}', + true + ); + expect(matchPattern).to.eql( + /^chore(\((?[\w-./]+)\))?: 🔖 release ?(?@?[\w-./]*)? v?(?[0-9].*)$/ + ); + }); + }); +});