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"]], ]); }