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