From 0b387bbd0cfa868da1defc7bea0f0e9d53a7ba5f Mon Sep 17 00:00:00 2001 From: Fabian Meyer <3982806+meyfa@users.noreply.github.com> Date: Sun, 14 Jul 2024 15:00:31 +0200 Subject: [PATCH] feat: add "always-update" config option Fixes #1459 This patch adds a new option "always-update" that forces existing pull requests to be updated even when there are no changes to the release notes. This is useful, for example, when the repository enforces pull requests to be up-to-date with the main branch before they can be merged. Without this option set to `true`, that is, with the default behavior of release-please, if the last commit made to the main branch does not appear in the release notes, the existing pull request branch would not be rebased. --- docs/manifest-releaser.md | 7 +++ schemas/config.json | 5 ++ src/manifest.ts | 52 +++++++++++------ src/updaters/release-please-config.ts | 1 + test/manifest.ts | 83 +++++++++++++++++++++++++++ 5 files changed, 129 insertions(+), 19 deletions(-) diff --git a/docs/manifest-releaser.md b/docs/manifest-releaser.md index cfc3e0248..aaa507105 100644 --- a/docs/manifest-releaser.md +++ b/docs/manifest-releaser.md @@ -212,6 +212,13 @@ defaults (those are documented in comments) // absence defaults to false and one pull request will be raised "separate-pull-requests": false, + // if true, always update existing pull requests when changes are added, + // instead of only when the release notes change. + // This option may increase the number of API calls used, but can be useful + // if pull requests must not be out-of-date with the base branch. + // absence defaults to false + "always-update": true, + // sets the manifest pull request title for when releasing multiple packages // grouped together in the one pull request. // This option has no effect when `separate-pull-requests` is `true`. diff --git a/schemas/config.json b/schemas/config.json index 2477278a5..cb768ca24 100644 --- a/schemas/config.json +++ b/schemas/config.json @@ -111,6 +111,10 @@ "description": "Open a separate release pull request for each component. Defaults to `false`.", "type": "boolean" }, + "always-update": { + "description": "Always update the pull request with the latest changes. Defaults to `false`.", + "type": "boolean" + }, "tag-separator": { "description": "Customize the separator between the component and version in the GitHub tag.", "type": "string" @@ -438,6 +442,7 @@ "pull-request-header": true, "pull-request-footer": true, "separate-pull-requests": true, + "always-update": true, "tag-separator": true, "extra-files": true, "version-file": true, diff --git a/src/manifest.ts b/src/manifest.ts index 10ac85748..796cbd4af 100644 --- a/src/manifest.ts +++ b/src/manifest.ts @@ -117,6 +117,7 @@ export interface ReleaserConfig { pullRequestFooter?: string; tagSeparator?: string; separatePullRequests?: boolean; + alwaysUpdate?: boolean; labels?: string[]; releaseLabels?: string[]; extraLabels?: string[]; @@ -176,6 +177,7 @@ interface ReleaserConfigJson { 'pull-request-header'?: string; 'pull-request-footer'?: string; 'separate-pull-requests'?: boolean; + 'always-update'?: boolean; 'tag-separator'?: string; 'extra-files'?: ExtraFile[]; 'version-file'?: string; @@ -202,6 +204,7 @@ export interface ManifestOptions { draft?: boolean; prerelease?: boolean; draftPullRequest?: boolean; + alwaysUpdate?: boolean; groupPullRequestTitlePattern?: string; releaseSearchDepth?: number; commitSearchDepth?: number; @@ -294,6 +297,7 @@ export class Manifest { readonly releasedVersions: ReleasedVersions; private targetBranch: string; private separatePullRequests: boolean; + private alwaysUpdate: boolean; readonly fork: boolean; private signoffUser?: string; private labels: string[]; @@ -333,6 +337,8 @@ export class Manifest { * plugin * @param {boolean} manifestOptions.separatePullRequests If true, create separate pull * requests instead of a single manifest release pull request + * @param {boolean} manifestOptions.alwaysUpdate If true, always updates pull requests instead of + * only when the release notes change * @param {PluginType[]} manifestOptions.plugins Any plugins to use for this repository * @param {boolean} manifestOptions.fork If true, create pull requests from a fork. Defaults * to `false` @@ -360,6 +366,7 @@ export class Manifest { this.separatePullRequests = manifestOptions?.separatePullRequests ?? Object.keys(repositoryConfig).length === 1; + this.alwaysUpdate = manifestOptions?.alwaysUpdate || false; this.fork = manifestOptions?.fork || false; this.signoffUser = manifestOptions?.signoff; this.releaseLabels = @@ -1003,7 +1010,9 @@ export class Manifest { openPullRequest.headBranchName === pullRequest.headRefName ); if (existing) { - return await this.maybeUpdateExistingPullRequest(existing, pullRequest); + return this.alwaysUpdate + ? await this.updateExistingPullRequest(existing, pullRequest) + : await this.maybeUpdateExistingPullRequest(existing, pullRequest); } // look for closed, snoozed pull request @@ -1012,7 +1021,9 @@ export class Manifest { openPullRequest.headBranchName === pullRequest.headRefName ); if (snoozed) { - return await this.maybeUpdateSnoozedPullRequest(snoozed, pullRequest); + return this.alwaysUpdate + ? await this.updateExistingPullRequest(snoozed, pullRequest) + : await this.maybeUpdateSnoozedPullRequest(snoozed, pullRequest); } const body = await this.pullRequestOverflowHandler.handleOverflow( @@ -1055,20 +1066,10 @@ export class Manifest { ); return undefined; } - const updatedPullRequest = await this.github.updatePullRequest( - existing.number, - pullRequest, - this.targetBranch, - { - fork: this.fork, - signoffUser: this.signoffUser, - pullRequestOverflowHandler: this.pullRequestOverflowHandler, - } - ); - return updatedPullRequest; + return await this.updateExistingPullRequest(existing, pullRequest); } - /// only update an snoozed pull request if it has release note changes + /// only update a snoozed pull request if it has release note changes private async maybeUpdateSnoozedPullRequest( snoozed: PullRequest, pullRequest: ReleasePullRequest @@ -1080,8 +1081,22 @@ export class Manifest { ); return undefined; } - const updatedPullRequest = await this.github.updatePullRequest( - snoozed.number, + const updatedPullRequest = await this.updateExistingPullRequest( + snoozed, + pullRequest + ); + // TODO: consider leaving the snooze label + await this.github.removeIssueLabels([SNOOZE_LABEL], snoozed.number); + return updatedPullRequest; + } + + /// force an update to an existing pull request + private async updateExistingPullRequest( + existing: PullRequest, + pullRequest: ReleasePullRequest + ): Promise { + return await this.github.updatePullRequest( + existing.number, pullRequest, this.targetBranch, { @@ -1090,9 +1105,6 @@ export class Manifest { pullRequestOverflowHandler: this.pullRequestOverflowHandler, } ); - // TODO: consider leaving the snooze label - await this.github.removeIssueLabels([SNOOZE_LABEL], snoozed.number); - return updatedPullRequest; } private async *findMergedReleasePullRequests() { @@ -1366,6 +1378,7 @@ function extractReleaserConfig( pullRequestFooter: config['pull-request-footer'], tagSeparator: config['tag-separator'], separatePullRequests: config['separate-pull-requests'], + alwaysUpdate: config['always-update'], labels: config['label']?.split(','), releaseLabels: config['release-label']?.split(','), extraLabels: config['extra-label']?.split(','), @@ -1415,6 +1428,7 @@ async function parseConfig( lastReleaseSha: config['last-release-sha'], alwaysLinkLocal: config['always-link-local'], separatePullRequests: config['separate-pull-requests'], + alwaysUpdate: config['always-update'], groupPullRequestTitlePattern: config['group-pull-request-title-pattern'], plugins: config['plugins'], labels: configLabel?.split(','), diff --git a/src/updaters/release-please-config.ts b/src/updaters/release-please-config.ts index feedfe1d5..88d7c50df 100644 --- a/src/updaters/release-please-config.ts +++ b/src/updaters/release-please-config.ts @@ -77,6 +77,7 @@ function releaserConfigToJsonConfig( 'pull-request-header': config.pullRequestHeader, 'pull-request-footer': config.pullRequestFooter, 'separate-pull-requests': config.separatePullRequests, + 'always-update': config.alwaysUpdate, 'tag-separator': config.tagSeparator, 'extra-files': config.extraFiles, 'version-file': config.versionFile, diff --git a/test/manifest.ts b/test/manifest.ts index 7315dee0c..5acb1973d 100644 --- a/test/manifest.ts +++ b/test/manifest.ts @@ -4113,6 +4113,89 @@ describe('Manifest', () => { const pullRequestNumbers = await manifest.createPullRequests(); expect(pullRequestNumbers).lengthOf(0); }); + + it('updates an existing pull request without changes if set to always update', async function () { + sandbox + .stub(github, 'getFileContentsOnBranch') + .withArgs('README.md', 'main') + .resolves(buildGitHubFileRaw('some-content')) + .withArgs('release-notes.md', 'my-head-branch--release-notes') + .resolves(buildGitHubFileRaw(body.toString())); + stubSuggesterWithSnapshot(sandbox, this.test!.fullTitle()); + mockPullRequests( + github, + [ + { + number: 22, + title: 'pr title1', + body: pullRequestBody('release-notes/overflow.txt'), + headBranchName: 'release-please/branches/main', + baseBranchName: 'main', + labels: ['autorelease: pending'], + files: [], + }, + ], + [] + ); + sandbox + .stub(github, 'updatePullRequest') + .withArgs( + 22, + sinon.match.any, + sinon.match.any, + sinon.match.has('pullRequestOverflowHandler', sinon.match.truthy) + ) + .resolves({ + number: 22, + title: 'pr title1', + body: 'pr body1', + headBranchName: 'release-please/branches/main', + baseBranchName: 'main', + labels: [], + files: [], + }); + const manifest = new Manifest( + github, + 'main', + { + 'path/a': { + releaseType: 'node', + component: 'pkg1', + }, + 'path/b': { + releaseType: 'node', + component: 'pkg2', + }, + }, + { + 'path/a': Version.parse('1.0.0'), + 'path/b': Version.parse('0.2.3'), + }, + { + separatePullRequests: true, + alwaysUpdate: true, + plugins: ['node-workspace'], + } + ); + sandbox.stub(manifest, 'buildPullRequests').resolves([ + { + title: PullRequestTitle.ofTargetBranch('main'), + body, + updates: [ + { + path: 'README.md', + createIfMissing: false, + updater: new RawContent('some raw content'), + }, + ], + labels: [], + headRefName: 'release-please/branches/main', + draft: false, + }, + ]); + const pullRequestNumbers = await manifest.createPullRequests(); + expect(pullRequestNumbers).lengthOf(1); + }); }); it('updates an existing snapshot pull request', async function () {