Skip to content

Commit bb1ac81

Browse files
authored
Experimental management scripts (microsoft#31067)
* Add configure-experimental * Add script for synchronizing branches with master and creating an experimental branch with the result of merging those
1 parent 885d4d6 commit bb1ac81

File tree

3 files changed

+117
-10
lines changed

3 files changed

+117
-10
lines changed

scripts/open-user-pr.ts

Lines changed: 1 addition & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,7 @@
11
/// <reference lib="esnext.asynciterable" />
22
// Must reference esnext.asynciterable lib, since octokit uses AsyncIterable internally
3-
import cp = require("child_process");
43
import Octokit = require("@octokit/rest");
5-
6-
const opts = { timeout: 100_000, shell: true, stdio: "inherit" }
7-
function runSequence(tasks: [string, string[]][]) {
8-
for (const task of tasks) {
9-
console.log(`${task[0]} ${task[1].join(" ")}`);
10-
const result = cp.spawnSync(task[0], task[1], opts);
11-
if (result.status !== 0) throw new Error(`${task[0]} ${task[1].join(" ")} failed: ${result.stderr && result.stderr.toString()}`);
12-
}
13-
}
4+
import {runSequence} from "./run-sequence";
145

156
function padNum(number: number) {
167
const str = "" + number;

scripts/run-sequence.js

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
// @ts-check
2+
const cp = require("child_process");
3+
/**
4+
*
5+
* @param {[string, string[]][]} tasks
6+
* @param {cp.SpawnSyncOptions} opts
7+
*/
8+
function runSequence(tasks, opts = { timeout: 100000, shell: true, stdio: "inherit" }) {
9+
let lastResult;
10+
for (const task of tasks) {
11+
console.log(`${task[0]} ${task[1].join(" ")}`);
12+
const result = cp.spawnSync(task[0], task[1], opts);
13+
if (result.status !== 0) throw new Error(`${task[0]} ${task[1].join(" ")} failed: ${result.stderr && result.stderr.toString()}`);
14+
lastResult = result;
15+
}
16+
return lastResult && lastResult.stdout && lastResult.stdout.toString();
17+
}
18+
19+
exports.runSequence = runSequence;
Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
// @ts-check
2+
/// <reference lib="esnext.asynciterable" />
3+
const Octokit = require("@octokit/rest");
4+
const {runSequence} = require("./run-sequence");
5+
6+
/**
7+
* This program should be invoked as `node ./scripts/update-experimental-branches <GithubAccessToken> <Branch1> [Branch2] [...]`
8+
*/
9+
async function main() {
10+
const branchesRaw = process.argv[3];
11+
const branches = process.argv.slice(3);
12+
if (!branches.length) {
13+
throw new Error(`No experimental branches, aborting...`);
14+
}
15+
console.log(`Performing experimental branch updating and merging for branches ${branchesRaw}`);
16+
17+
const gh = new Octokit();
18+
gh.authenticate({
19+
type: "token",
20+
token: process.argv[2]
21+
});
22+
23+
// Fetch all relevant refs
24+
runSequence([
25+
["git", ["fetch", "origin", "master:master", ...branches.map(b => `${b}:${b}`)]]
26+
])
27+
28+
// Forcibly cleanup workspace
29+
runSequence([
30+
["git", ["clean", "-fdx"]],
31+
["git", ["checkout", "."]],
32+
["git", ["checkout", "master"]],
33+
]);
34+
35+
// Update branches
36+
for (const branch of branches) {
37+
// Checkout, then get the merge base
38+
const mergeBase = runSequence([
39+
["git", ["checkout", branch]],
40+
["git", ["merge-base", branch, "master"]],
41+
]);
42+
// Simulate the merge and abort if there are conflicts
43+
const mergeTree = runSequence([
44+
["git", ["merge-tree", mergeBase, branch, "master"]]
45+
]);
46+
if (mergeTree.indexOf(`===${"="}===`)) { // 7 equals is the center of the merge conflict marker
47+
const res = await gh.pulls.list({owner: "Microsoft", repo: "TypeScript", base: branch});
48+
if (res && res.data && res.data[0]) {
49+
const pr = res.data[0];
50+
await gh.issues.createComment({
51+
owner: "Microsoft",
52+
repo: "TypeScript",
53+
number: pr.number,
54+
body: `This PR is configured as an experiment, and currently has merge conflicts with master - please rebase onto master and fix the conflicts.`
55+
});
56+
}
57+
throw new Error(`Merge conflict detected on branch ${branch} with master`);
58+
}
59+
// Merge is good - apply a rebase and (force) push
60+
runSequence([
61+
["git", ["rebase", "master"]],
62+
["git", ["push", "-f", "-u", "origin", branch]],
63+
]);
64+
}
65+
66+
// Return to `master` and make a new `experimental` branch
67+
runSequence([
68+
["git", ["checkout", "master"]],
69+
["git", ["branch", "-D", "experimental"]],
70+
["git", ["checkout", "-b", "experimental"]],
71+
]);
72+
73+
// Merge each branch into `experimental` (which, if there is a conflict, we now know is from inter-experiment conflict)
74+
for (const branch of branches) {
75+
// Find the merge base
76+
const mergeBase = runSequence([
77+
["git", ["merge-base", branch, "experimental"]],
78+
]);
79+
// Simulate the merge and abort if there are conflicts
80+
const mergeTree = runSequence([
81+
["git", ["merge-tree", mergeBase, branch, "experimental"]]
82+
]);
83+
if (mergeTree.indexOf(`===${"="}===`)) { // 7 equals is the center of the merge conflict marker
84+
throw new Error(`Merge conflict detected on branch ${branch} with other experiment`);
85+
}
86+
// Merge (always producing a merge commit)
87+
runSequence([
88+
["git", ["merge", branch, "--no-ff"]],
89+
]);
90+
}
91+
// Every branch merged OK, force push the replacement `experimental` branch
92+
runSequence([
93+
["git", ["push", "-f", "-u", "origin", "experimental"]],
94+
]);
95+
}
96+
97+
main().catch(e => (console.error(e), process.exitCode = 2));

0 commit comments

Comments
 (0)