diff --git a/lib/promote_release.js b/lib/promote_release.js index f76349fa..da32302c 100644 --- a/lib/promote_release.js +++ b/lib/promote_release.js @@ -142,6 +142,12 @@ export default class ReleasePromotion extends Session { const workingOnNewReleaseCommit = await this.setupForNextRelease(); cli.stopSpinner('Successfully set up for next release'); + const shouldRebaseStagingBranch = await cli.prompt( + 'Rebase staging branch on top of the release commit?', { defaultAnswer: true }); + const tipOfStagingBranch = shouldRebaseStagingBranch + ? await this.rebaseStagingBranch(workingOnNewReleaseCommit) + : workingOnNewReleaseCommit; + // Cherry pick release commit to master. const shouldCherryPick = await cli.prompt( 'Cherry-pick release commit to the default branch?', { defaultAnswer: true }); @@ -200,7 +206,7 @@ export default class ReleasePromotion extends Session { } // Push to the remote the release tag, and default, release, and staging branch. - await this.pushToRemote(workingOnNewReleaseCommit); + await this.pushToRemote(workingOnNewReleaseCommit, tipOfStagingBranch); // Promote and sign the release builds. await this.promoteAndSignRelease(); @@ -440,7 +446,7 @@ export default class ReleasePromotion extends Session { return workingOnNewReleaseCommit.trim(); } - async pushToRemote(workingOnNewReleaseCommit) { + async pushToRemote(workingOnNewReleaseCommit, tipOfStagingBranch) { const { cli, dryRun, version, versionComponents, stagingBranch } = this; const releaseBranch = `v${versionComponents.major}.x`; const tagVersion = `v${version}`; @@ -454,8 +460,8 @@ export default class ReleasePromotion extends Session { cli.info(`git push ${this.upstream} ${ this.defaultBranch} ${ tagVersion} ${ - workingOnNewReleaseCommit}:refs/heads/${releaseBranch} ${ - workingOnNewReleaseCommit}:refs/heads/${stagingBranch}`); + workingOnNewReleaseCommit}:refs/heads/${releaseBranch} +${ + tipOfStagingBranch}:refs/heads/${stagingBranch}`); cli.warn('Once pushed, you must not delete the local tag'); prompt = 'Ready to continue?'; } @@ -471,7 +477,7 @@ export default class ReleasePromotion extends Session { cli.startSpinner('Pushing to remote'); await forceRunAsync('git', ['push', this.upstream, this.defaultBranch, tagVersion, `${workingOnNewReleaseCommit}:refs/heads/${releaseBranch}`, - `${workingOnNewReleaseCommit}:refs/heads/${stagingBranch}`], + `+${tipOfStagingBranch}:refs/heads/${stagingBranch}`], { ignoreFailure: false }); cli.stopSpinner(`Pushed ${tagVersion}, ${this.defaultBranch}, ${ releaseBranch}, and ${stagingBranch} to remote`); @@ -507,6 +513,21 @@ export default class ReleasePromotion extends Session { cli.stopSpinner('Release has been signed and promoted'); } + async rebaseStagingBranch(workingOnNewReleaseCommit) { + const { cli, stagingBranch, upstream } = this; + cli.startSpinner('Fetch staging branch'); + await forceRunAsync('git', ['fetch', upstream, stagingBranch], { ignoreFailure: false }); + cli.updateSpinner('Reset and rebase'); + await forceRunAsync('git', ['reset', 'FETCH_HEAD', '--hard'], { ignoreFailure: false }); + await forceRunAsync('git', + ['rebase', workingOnNewReleaseCommit, ...this.gpgSign], { ignoreFailure: false }); + const tipOfStagingBranch = await forceRunAsync('git', ['rev-parse', 'HEAD'], + { ignoreFailure: false, captureStdout: true }); + cli.stopSpinner('Rebased successfully'); + + return tipOfStagingBranch.trim(); + } + async cherryPickToDefaultBranch() { this.defaultBranch ??= await this.getDefaultBranch(); const releaseCommitSha = this.releaseCommitSha;