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 () {