File: update-experimental-branches.mjs

package info (click to toggle)
node-typescript 4.9.5%2Bds1-2
  • links: PTS, VCS
  • area: main
  • in suites: sid, trixie
  • size: 533,908 kB
  • sloc: javascript: 2,018,330; makefile: 7; sh: 1
file content (101 lines) | stat: -rw-r--r-- 4,600 bytes parent folder | download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
import { Octokit } from "@octokit/rest";
import { runSequence } from "./run-sequence.mjs";

// 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 <GithubAccessToken>`
 * 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 gh = new Octokit({
        auth: process.argv[2]
    });
    const prnums = (await gh.issues.listForRepo({
        labels: "typescript@experimental",
        sort: "created",
        state: "open",
        owner: "Microsoft",
        repo: "TypeScript",
    })).data.filter(i => !!i.pull_request).map(i => i.number);
    if (triggeredPR && !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", ["checkout", "."]],
        ["git", ["fetch", "-fu", "origin", "main:main"]],
        ["git", ["checkout", "main"]],
        ["git", ["remote", "add", "fork", remoteUrl]], // Add the remote fork
    ]);

    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", pull_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 ?? 0) === num) {
                    await gh.issues.createComment({
                        owner: "Microsoft",
                        repo: "TypeScript",
                        issue_number: num,
                        body: `This PR is configured as an experiment, and currently has rebase conflicts with main - please rebase onto main and fix the conflicts.`
                    });
                }
                throw new Error(`Rebase conflict detected in PR ${num} with main`); // A PR is currently in conflict, give up
            }
            runSequence([
                ["git", ["fetch", "origin", `pull/${num}/head:${num}`]],
                ["git", ["checkout", `${num}`]],
                ["git", ["rebase", "main"]],
                ["git", ["push", "-f", "-u", "fork", `${num}`]], // Keep a rebased copy of this branch in our fork
            ]);

        }
        else {
            throw new Error(`Invalid PR number: ${numRaw}`);
        }
    }

    // Return to `master` and make a new `experimental` branch
    runSequence([
        ["git", ["checkout", "main"]],
        ["git", ["checkout", "-b", "experimental"]],
    ]);

    // Merge each branch into `experimental` (which, if there is a conflict, we now know is from inter-experiment conflict)
    for (const branchnum of prnums) {
        const branch = "" + branchnum;
        // 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.trim(), branch, "experimental"]]
        ]);
        if (mergeTree.indexOf(`===${"="}===`) >= 0) { // 7 equals is the center of the merge conflict marker
            throw new Error(`Merge conflict detected involving PR ${branch} with other experiment`);
        }
        // Merge (always producing a merge commit)
        runSequence([
            ["git", ["merge", branch, "--no-ff"]],
        ]);
    }
    // Every branch merged OK, force push the replacement `experimental` branch
    runSequence([
        ["git", ["push", "-f", "-u", "fork", "experimental"]],
    ]);
}

main().catch(e => (console.error(e), process.exitCode = 2));