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