From b5040a6d9dc4621cee8f9f8dbe5d2278e67b22a9 Mon Sep 17 00:00:00 2001 From: Antoine du Hamel Date: Tue, 20 May 2025 01:34:41 +0200 Subject: [PATCH] feat(git-node): rebase staging commits after promoting a release If there are commits on the staging branch, we probably want to keep them on top of the release commit. Staging branches and release proposal are usually in sync, however it's never the case for security releases. --- lib/promote_release.js | 31 ++++++++++++++++++++++++++----- 1 file changed, 26 insertions(+), 5 deletions(-) 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;