1// @ts-check 2/// <reference lib="esnext.asynciterable" /> 3const { Octokit } = require("@octokit/rest"); 4const { runSequence } = require("./run-sequence"); 5 6// The first is used by bot-based kickoffs, the second by automatic triggers 7const triggeredPR = process.env.SOURCE_ISSUE || process.env.SYSTEM_PULLREQUEST_PULLREQUESTNUMBER; 8 9/** 10 * This program should be invoked as `node ./scripts/update-experimental-branches <GithubAccessToken>` 11 * TODO: the following is racey - if two experiment-enlisted PRs trigger simultaneously and witness one another in an unupdated state, they'll both produce 12 * 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 13 * of this task to 1 (so only one job is allowed to update experiments at a time). 14 */ 15async function main() { 16 const gh = new Octokit({ 17 auth: process.argv[2] 18 }); 19 const prnums = (await gh.issues.listForRepo({ 20 labels: "typescript@experimental", 21 sort: "created", 22 state: "open", 23 owner: "Microsoft", 24 repo: "TypeScript", 25 })).data.filter(i => !!i.pull_request).map(i => i.number); 26 if (triggeredPR && !prnums.some(n => n === +triggeredPR)) { 27 return; // Only have work to do for enlisted PRs 28 } 29 console.log(`Performing experimental branch updating and merging for pull requests ${prnums.join(", ")}`); 30 31 const userName = process.env.GH_USERNAME; 32 const remoteUrl = `https://${process.argv[2]}@github.com/${userName}/TypeScript.git`; 33 34 // Forcibly cleanup workspace 35 runSequence([ 36 ["git", ["checkout", "."]], 37 ["git", ["fetch", "-fu", "origin", "master:master"]], 38 ["git", ["checkout", "master"]], 39 ["git", ["remote", "add", "fork", remoteUrl]], // Add the remote fork 40 ]); 41 42 for (const numRaw of prnums) { 43 const num = +numRaw; 44 if (num) { 45 // PR number rather than branch name - lookup info 46 const inputPR = await gh.pulls.get({ owner: "Microsoft", repo: "TypeScript", pull_number: num }); 47 // GH calculates the rebaseable-ness of a PR into its target, so we can just use that here 48 if (!inputPR.data.rebaseable) { 49 if (+triggeredPR === num) { 50 await gh.issues.createComment({ 51 owner: "Microsoft", 52 repo: "TypeScript", 53 issue_number: num, 54 body: `This PR is configured as an experiment, and currently has rebase conflicts with master - please rebase onto master and fix the conflicts.` 55 }); 56 } 57 throw new Error(`Rebase conflict detected in PR ${num} with master`); // A PR is currently in conflict, give up 58 } 59 runSequence([ 60 ["git", ["fetch", "origin", `pull/${num}/head:${num}`]], 61 ["git", ["checkout", `${num}`]], 62 ["git", ["rebase", "master"]], 63 ["git", ["push", "-f", "-u", "fork", `${num}`]], // Keep a rebased copy of this branch in our fork 64 ]); 65 66 } 67 else { 68 throw new Error(`Invalid PR number: ${numRaw}`); 69 } 70 } 71 72 // Return to `master` and make a new `experimental` branch 73 runSequence([ 74 ["git", ["checkout", "master"]], 75 ["git", ["checkout", "-b", "experimental"]], 76 ]); 77 78 // Merge each branch into `experimental` (which, if there is a conflict, we now know is from inter-experiment conflict) 79 for (const branchnum of prnums) { 80 const branch = "" + branchnum; 81 // Find the merge base 82 const mergeBase = runSequence([ 83 ["git", ["merge-base", branch, "experimental"]], 84 ]); 85 // Simulate the merge and abort if there are conflicts 86 const mergeTree = runSequence([ 87 ["git", ["merge-tree", mergeBase.trim(), branch, "experimental"]] 88 ]); 89 if (mergeTree.indexOf(`===${"="}===`) >= 0) { // 7 equals is the center of the merge conflict marker 90 throw new Error(`Merge conflict detected involving PR ${branch} with other experiment`); 91 } 92 // Merge (always producing a merge commit) 93 runSequence([ 94 ["git", ["merge", branch, "--no-ff"]], 95 ]); 96 } 97 // Every branch merged OK, force push the replacement `experimental` branch 98 runSequence([ 99 ["git", ["push", "-f", "-u", "fork", "experimental"]], 100 ]); 101} 102 103main().catch(e => (console.error(e), process.exitCode = 2)); 104