2022-10-07 19:50:46 +03:00
|
|
|
import { Octokit } from "@octokit/rest";
|
2023-02-03 01:36:21 +03:00
|
|
|
|
2022-10-07 19:50:46 +03:00
|
|
|
import { runSequence } from "./run-sequence.mjs";
|
2019-06-04 02:06:35 +03:00
|
|
|
|
|
|
|
// The first is used by bot-based kickoffs, the second by automatic triggers
|
|
|
|
const triggeredPR = process.env.SOURCE_ISSUE || process.env.SYSTEM_PULLREQUEST_PULLREQUESTNUMBER;
|
2019-04-23 23:52:23 +03:00
|
|
|
|
|
|
|
/**
|
2019-06-28 23:34:46 +03:00
|
|
|
* This program should be invoked as `node ./scripts/update-experimental-branches <GithubAccessToken>`
|
2019-06-04 02:06:35 +03:00
|
|
|
* 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
|
2022-09-07 02:05:47 +03:00
|
|
|
* of this task to 1 (so only one job is allowed to update experiments at a time).
|
2019-04-23 23:52:23 +03:00
|
|
|
*/
|
|
|
|
async function main() {
|
2019-06-28 23:34:46 +03:00
|
|
|
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)) {
|
2019-06-04 02:06:35 +03:00
|
|
|
return; // Only have work to do for enlisted PRs
|
2019-04-23 23:52:23 +03:00
|
|
|
}
|
2019-06-04 02:06:35 +03:00
|
|
|
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`;
|
2019-04-23 23:52:23 +03:00
|
|
|
|
|
|
|
// Forcibly cleanup workspace
|
|
|
|
runSequence([
|
|
|
|
["git", ["checkout", "."]],
|
2021-06-01 22:44:18 +03:00
|
|
|
["git", ["fetch", "-fu", "origin", "main:main"]],
|
|
|
|
["git", ["checkout", "main"]],
|
2019-06-04 02:06:35 +03:00
|
|
|
["git", ["remote", "add", "fork", remoteUrl]], // Add the remote fork
|
2019-04-23 23:52:23 +03:00
|
|
|
]);
|
2019-06-04 02:06:35 +03:00
|
|
|
|
|
|
|
for (const numRaw of prnums) {
|
|
|
|
const num = +numRaw;
|
|
|
|
if (num) {
|
|
|
|
// PR number rather than branch name - lookup info
|
2019-06-06 01:25:33 +03:00
|
|
|
const inputPR = await gh.pulls.get({ owner: "Microsoft", repo: "TypeScript", pull_number: num });
|
2019-06-04 02:06:35 +03:00
|
|
|
// GH calculates the rebaseable-ness of a PR into its target, so we can just use that here
|
|
|
|
if (!inputPR.data.rebaseable) {
|
2022-10-07 19:50:46 +03:00
|
|
|
if (+(triggeredPR ?? 0) === num) {
|
2019-06-04 02:06:35 +03:00
|
|
|
await gh.issues.createComment({
|
|
|
|
owner: "Microsoft",
|
|
|
|
repo: "TypeScript",
|
2019-06-06 01:25:33 +03:00
|
|
|
issue_number: num,
|
2021-06-01 22:44:18 +03:00
|
|
|
body: `This PR is configured as an experiment, and currently has rebase conflicts with main - please rebase onto main and fix the conflicts.`
|
2019-06-04 02:06:35 +03:00
|
|
|
});
|
|
|
|
}
|
2021-06-01 22:44:18 +03:00
|
|
|
throw new Error(`Rebase conflict detected in PR ${num} with main`); // A PR is currently in conflict, give up
|
2019-04-23 23:52:23 +03:00
|
|
|
}
|
2019-06-04 02:06:35 +03:00
|
|
|
runSequence([
|
|
|
|
["git", ["fetch", "origin", `pull/${num}/head:${num}`]],
|
|
|
|
["git", ["checkout", `${num}`]],
|
2021-06-01 22:44:18 +03:00
|
|
|
["git", ["rebase", "main"]],
|
2019-06-04 02:06:35 +03:00
|
|
|
["git", ["push", "-f", "-u", "fork", `${num}`]], // Keep a rebased copy of this branch in our fork
|
|
|
|
]);
|
|
|
|
|
|
|
|
}
|
|
|
|
else {
|
|
|
|
throw new Error(`Invalid PR number: ${numRaw}`);
|
2019-04-23 23:52:23 +03:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// Return to `master` and make a new `experimental` branch
|
|
|
|
runSequence([
|
2021-06-01 22:44:18 +03:00
|
|
|
["git", ["checkout", "main"]],
|
2019-04-23 23:52:23 +03:00
|
|
|
["git", ["checkout", "-b", "experimental"]],
|
|
|
|
]);
|
|
|
|
|
|
|
|
// Merge each branch into `experimental` (which, if there is a conflict, we now know is from inter-experiment conflict)
|
2020-02-21 20:49:02 +03:00
|
|
|
for (const branchnum of prnums) {
|
|
|
|
const branch = "" + branchnum;
|
2019-04-23 23:52:23 +03:00
|
|
|
// Find the merge base
|
|
|
|
const mergeBase = runSequence([
|
|
|
|
["git", ["merge-base", branch, "experimental"]],
|
|
|
|
]);
|
|
|
|
// Simulate the merge and abort if there are conflicts
|
|
|
|
const mergeTree = runSequence([
|
2019-06-04 02:06:35 +03:00
|
|
|
["git", ["merge-tree", mergeBase.trim(), branch, "experimental"]]
|
2019-04-23 23:52:23 +03:00
|
|
|
]);
|
2019-06-06 02:24:02 +03:00
|
|
|
if (mergeTree.indexOf(`===${"="}===`) >= 0) { // 7 equals is the center of the merge conflict marker
|
2019-06-04 02:06:35 +03:00
|
|
|
throw new Error(`Merge conflict detected involving PR ${branch} with other experiment`);
|
2019-04-23 23:52:23 +03:00
|
|
|
}
|
|
|
|
// Merge (always producing a merge commit)
|
|
|
|
runSequence([
|
|
|
|
["git", ["merge", branch, "--no-ff"]],
|
|
|
|
]);
|
|
|
|
}
|
|
|
|
// Every branch merged OK, force push the replacement `experimental` branch
|
|
|
|
runSequence([
|
2019-06-04 02:06:35 +03:00
|
|
|
["git", ["push", "-f", "-u", "fork", "experimental"]],
|
2019-04-23 23:52:23 +03:00
|
|
|
]);
|
|
|
|
}
|
|
|
|
|
|
|
|
main().catch(e => (console.error(e), process.exitCode = 2));
|