• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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