Skip to content

Commit b909e67

Browse files
committed
feat: add git node staging command
Add a new `git node staging` command that automates cherry-picking commits into staging branches. It works by cherry-picking all commits that have no conflicts and stopping when finding commits that have conflicts so that the releaser can either manually fix that conflict to continue or skip. Usage: Fetches a commit list using `branch-diff` and automatically cherry-picks / skips commits based on whether or not they land cleanly: git node staging By default the script will add `backport-request` labels to any skip commit, in order to run in a "dry-run" mode, avoiding the GH cli interactions, you can use the `skipGH` flag, e.g: git node staging --skipGH After fixing up manual conflicts using `git cherry-pick --continue` the releaser can resume the cherry-pick queue: git node staging --continue Similarly, after manually skipping a commit using `git cherry-pick --skip` the releaser can resume the cherry-pick queue using: git node staging --skip If you want to run the script and automatically skip any commit that is unable to land cleanly you can use the `--autoSkip` flag: git node staging --autoSkip Sets a custom reporter at the end of the automated operation: git node staging --reporter=json Limits to 10 the number of commits to be cherry-picked: git node staging --pagination=10 Automates the backport request message, this won't run any of the `branch-diff` or cherry-pick routines. Useful for when you removed a faulty commit from the branch and want to signal to PR author and collaborators that commit now needs backporting, just use its PR#: git node staging --backport=12345 More: The automate cherry-pick logic also includes local persistency of the ongoing commit list, in case a fatal error happens during the command execution, it's possible to resume after cleaning up the git repo state by running `git node staging` again.
1 parent 3afe24f commit b909e67

File tree

3 files changed

+820
-6
lines changed

3 files changed

+820
-6
lines changed

components/git/staging.js

+105
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
import CLI from '../../lib/cli.js';
2+
import { runPromise } from '../../lib/run.js';
3+
import { Staging } from '../../lib/staging.js';
4+
5+
export const command = 'staging';
6+
export const describe = 'Automatic port commits to a release line branch';
7+
8+
const stagingOptions = {
9+
autoSkip: {
10+
describe: 'Automatically skip commits with conflicts that have to be manually resolved',
11+
type: 'boolean'
12+
},
13+
backport: {
14+
describe: 'The PR ID / number to backport, skip staging commits',
15+
type: 'number'
16+
},
17+
continue: {
18+
describe: 'Continue the staging process after a conflict',
19+
type: 'boolean'
20+
},
21+
paginate: {
22+
describe: 'Sets a maximum number of commits to port',
23+
type: 'number'
24+
},
25+
releaseLine: {
26+
describe: 'The major version of the target release',
27+
type: 'number'
28+
},
29+
reportDestination: {
30+
describe: 'The destination to write the report to. Possible values are: ' +
31+
'stdout, github, or a file path, defaults to an interactive prompt.',
32+
type: 'string',
33+
default: undefined
34+
},
35+
reporter: {
36+
describe: 'The reporter to use for the output',
37+
type: 'string',
38+
default: 'markdown'
39+
},
40+
reset: {
41+
describe: 'Reset the staging process',
42+
type: 'boolean'
43+
},
44+
skip: {
45+
describe: 'Continue the staging process marking the current commit as skipped',
46+
type: 'boolean'
47+
},
48+
skipGH: {
49+
describe: 'Skip all `gh` cli actions. Will not read / add label to GitHub PRs',
50+
type: 'boolean'
51+
}
52+
};
53+
54+
export function builder(yargs) {
55+
return yargs
56+
.options(stagingOptions)
57+
.example('git node staging --releaseLine=23',
58+
'Port commits to the v1.x-staging branch');
59+
}
60+
61+
export function handler(argv) {
62+
const logStream = process.stdout.isTTY ? process.stdout : process.stderr;
63+
const cli = new CLI(logStream);
64+
const dir = process.cwd();
65+
66+
return runPromise(main(argv, cli, dir)).catch((err) => {
67+
if (cli.spinner.enabled) {
68+
cli.spinner.fail();
69+
}
70+
throw err;
71+
});
72+
}
73+
74+
async function main(argv, cli, dir) {
75+
const {
76+
autoSkip,
77+
backport,
78+
paginate,
79+
releaseLine,
80+
reportDestination,
81+
reporter,
82+
reset,
83+
skip,
84+
skipGH
85+
} = argv;
86+
const staging = new Staging({
87+
cli,
88+
dir,
89+
cont: argv.continue,
90+
autoSkip,
91+
paginate,
92+
releaseLine,
93+
reportDestination,
94+
reporter,
95+
skip,
96+
skipGH
97+
});
98+
if (backport) {
99+
await staging.requestBackport(backport);
100+
} else if (reset) {
101+
await staging.reset();
102+
} else {
103+
await staging.run();
104+
}
105+
}

lib/prepare_release.js

+18-6
Original file line numberDiff line numberDiff line change
@@ -523,12 +523,24 @@ export default class ReleasePreparation extends Session {
523523
const { newVersion } = this;
524524
const proposalBranch = `v${newVersion}-proposal`;
525525

526-
await runAsync('git', [
527-
'checkout',
528-
'-b',
529-
proposalBranch,
530-
base
531-
]);
526+
try {
527+
await forceRunAsync('git', [
528+
'checkout',
529+
'-b',
530+
proposalBranch,
531+
base
532+
], { captureStdout: true, captureStderr: true, ignoreFailures: false });
533+
} catch (err) {
534+
const branchExistsRE = /fatal: a branch named '.*' already exists/i;
535+
if (branchExistsRE.test(err.stderr)) {
536+
await runAsync('git', [
537+
'checkout',
538+
proposalBranch
539+
]);
540+
} else {
541+
throw err;
542+
}
543+
}
532544
return proposalBranch;
533545
}
534546

0 commit comments

Comments
 (0)