diff --git a/scripts/update-experimental-branches.js b/scripts/update-experimental-branches.js
index c112cf1a367..8af4df95cb9 100644
--- a/scripts/update-experimental-branches.js
+++ b/scripts/update-experimental-branches.js
@@ -1,66 +1,74 @@
// @ts-check
///
const Octokit = require("@octokit/rest");
-const {runSequence} = require("./run-sequence");
+const { runSequence } = require("./run-sequence");
+
+// The first is used by bot-based kickoffs, the second by automatic triggers
+const triggeredPR = process.env.SOURCE_ISSUE || process.env.SYSTEM_PULLREQUEST_PULLREQUESTNUMBER;
/**
- * This program should be invoked as `node ./scripts/update-experimental-branches [Branch2] [...]`
+ * This program should be invoked as `node ./scripts/update-experimental-branches [PR2] [...]`
+ * The order PR numbers are passed controls the order in which they are merged together.
+ * TODO: the following is racey - if two experiment-enlisted PRs trigger simultaneously and witness one another in an unupdated state, they'll both produce
+ * a new experimental branch, but each will be missing a change from the other. There's no _great_ way to fix this beyond setting the maximum concurrency
+ * of this task to 1 (so only one job is allowed to update experiments at a time).
*/
async function main() {
- const branchesRaw = process.argv[3];
- const branches = process.argv.slice(3);
- if (!branches.length) {
- throw new Error(`No experimental branches, aborting...`);
+ const prnums = process.argv.slice(3);
+ if (!prnums.length) {
+ return; // No enlisted PRs, nothing to update
}
- console.log(`Performing experimental branch updating and merging for branches ${branchesRaw}`);
+ if (!prnums.some(n => n === triggeredPR)) {
+ return; // Only have work to do for enlisted PRs
+ }
+ console.log(`Performing experimental branch updating and merging for pull requests ${prnums.join(", ")}`);
+
+ const userName = process.env.GH_USERNAME;
+ const remoteUrl = `https://${process.argv[2]}@github.com/${userName}/TypeScript.git`;
+
+ // Forcibly cleanup workspace
+ runSequence([
+ ["git", ["clean", "-fdx"]],
+ ["git", ["checkout", "."]],
+ ["git", ["checkout", "master"]],
+ ["git", ["remote", "add", "fork", remoteUrl]], // Add the remote fork
+ ["git", ["fetch", "origin", "master:master"]],
+ ]);
const gh = new Octokit();
gh.authenticate({
type: "token",
token: process.argv[2]
});
-
- // Fetch all relevant refs
- runSequence([
- ["git", ["fetch", "origin", "master:master", ...branches.map(b => `${b}:${b}`)]]
- ])
-
- // Forcibly cleanup workspace
- runSequence([
- ["git", ["clean", "-fdx"]],
- ["git", ["checkout", "."]],
- ["git", ["checkout", "master"]],
- ]);
-
- // Update branches
- for (const branch of branches) {
- // Checkout, then get the merge base
- const mergeBase = runSequence([
- ["git", ["checkout", branch]],
- ["git", ["merge-base", branch, "master"]],
- ]);
- // Simulate the merge and abort if there are conflicts
- const mergeTree = runSequence([
- ["git", ["merge-tree", mergeBase, branch, "master"]]
- ]);
- if (mergeTree.indexOf(`===${"="}===`)) { // 7 equals is the center of the merge conflict marker
- const res = await gh.pulls.list({owner: "Microsoft", repo: "TypeScript", base: branch});
- if (res && res.data && res.data[0]) {
- const pr = res.data[0];
- await gh.issues.createComment({
- owner: "Microsoft",
- repo: "TypeScript",
- number: pr.number,
- body: `This PR is configured as an experiment, and currently has merge conflicts with master - please rebase onto master and fix the conflicts.`
- });
+ for (const numRaw of prnums) {
+ const num = +numRaw;
+ if (num) {
+ // PR number rather than branch name - lookup info
+ const inputPR = await gh.pulls.get({ owner: "Microsoft", repo: "TypeScript", number: num });
+ // GH calculates the rebaseable-ness of a PR into its target, so we can just use that here
+ if (!inputPR.data.rebaseable) {
+ if (+triggeredPR === num) {
+ await gh.issues.createComment({
+ owner: "Microsoft",
+ repo: "TypeScript",
+ number: num,
+ body: `This PR is configured as an experiment, and currently has merge conflicts with master - please rebase onto master and fix the conflicts.`
+ });
+ throw new Error(`Merge conflict detected in PR ${num} with master`);
+ }
+ return; // A PR is currently in conflict, give up
}
- throw new Error(`Merge conflict detected on branch ${branch} with master`);
+ runSequence([
+ ["git", ["fetch", "origin", `pull/${num}/head:${num}`]],
+ ["git", ["checkout", `${num}`]],
+ ["git", ["rebase", "master"]],
+ ["git", ["push", "-f", "-u", "fork", `${num}`]], // Keep a rebased copy of this branch in our fork
+ ]);
+
+ }
+ else {
+ throw new Error(`Invalid PR number: ${numRaw}`);
}
- // Merge is good - apply a rebase and (force) push
- runSequence([
- ["git", ["rebase", "master"]],
- ["git", ["push", "-f", "-u", "origin", branch]],
- ]);
}
// Return to `master` and make a new `experimental` branch
@@ -71,17 +79,17 @@ async function main() {
]);
// Merge each branch into `experimental` (which, if there is a conflict, we now know is from inter-experiment conflict)
- for (const branch of branches) {
+ for (const branch of prnums) {
// Find the merge base
const mergeBase = runSequence([
["git", ["merge-base", branch, "experimental"]],
]);
// Simulate the merge and abort if there are conflicts
const mergeTree = runSequence([
- ["git", ["merge-tree", mergeBase, branch, "experimental"]]
+ ["git", ["merge-tree", mergeBase.trim(), branch, "experimental"]]
]);
if (mergeTree.indexOf(`===${"="}===`)) { // 7 equals is the center of the merge conflict marker
- throw new Error(`Merge conflict detected on branch ${branch} with other experiment`);
+ throw new Error(`Merge conflict detected involving PR ${branch} with other experiment`);
}
// Merge (always producing a merge commit)
runSequence([
@@ -90,7 +98,7 @@ async function main() {
}
// Every branch merged OK, force push the replacement `experimental` branch
runSequence([
- ["git", ["push", "-f", "-u", "origin", "experimental"]],
+ ["git", ["push", "-f", "-u", "fork", "experimental"]],
]);
}