Skip to content
This repository was archived by the owner on Jul 1, 2024. It is now read-only.

Commit df1a976

Browse files
committed
New run script
There's a bunch of stuff in `commands` and `scripts`, but it's a mess to update all of them. Instead, this is a single `run` script that can be used as a drop-in replacement for `daily`, which can do a dry run, show various info (including the new `ExtendedPrInfo`) in multiple formats, and can also operate on a selection of PRs.
1 parent e2a0e67 commit df1a976

File tree

3 files changed

+144
-3
lines changed

3 files changed

+144
-3
lines changed

package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,8 @@
3737
"prettyjson": "^1.2.1",
3838
"request": "^2.88.2",
3939
"tslib": "^1.13.0",
40-
"typescript": "^3.9.5"
40+
"typescript": "^3.9.5",
41+
"yargs": "^16.1.1"
4142
},
4243
"devDependencies": {
4344
"@types/jest": "^25.1.0",

src/compute-pr-actions.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -100,7 +100,7 @@ const enum Staleness {
100100
type ApproverKind = "maintainer" | "owner" | "other";
101101

102102
// used to pass around pr info with additional values
103-
interface ExtendedPrInfo extends PrInfo {
103+
export interface ExtendedPrInfo extends PrInfo {
104104
readonly orig: PrInfo;
105105
readonly reviewLink: string;
106106
readonly editsInfra: boolean;
@@ -233,7 +233,8 @@ function extendPrInfo(info: PrInfo): ExtendedPrInfo {
233233

234234
}
235235

236-
export function process(prInfo: PrInfo | BotEnsureRemovedFromProject | BotNoPackages | BotError ): Actions {
236+
export function process(prInfo: PrInfo | BotEnsureRemovedFromProject | BotNoPackages | BotError,
237+
extendedCallback: (info: ExtendedPrInfo) => void = _i => {}): Actions {
237238
if (prInfo.type === "remove") {
238239
if (prInfo.isDraft) {
239240
return {
@@ -271,6 +272,7 @@ export function process(prInfo: PrInfo | BotEnsureRemovedFromProject | BotNoPack
271272

272273
// Collect some additional info
273274
const info = extendPrInfo(prInfo);
275+
extendedCallback(info);
274276

275277
// General labelling and housekeeping
276278
const label = (label: LabelName, cond: unknown = true) => {

src/run.ts

Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,138 @@
1+
#!/usr/bin/env node
2+
3+
import * as yargs from "yargs";
4+
import { process as computeActions, ExtendedPrInfo } from "./compute-pr-actions";
5+
import { getAllOpenPRsAndCardIDs } from "./queries/all-open-prs-query";
6+
import { queryPRInfo, deriveStateForPR } from "./pr-info";
7+
import { executePrActions, deleteProjectCard } from "./execute-pr-actions";
8+
import { getProjectBoardCards } from "./queries/projectboard-cards";
9+
import { runQueryToGetPRForCardId } from "./queries/card-id-to-pr-query";
10+
import { createMutation, mutate } from "./graphql-client";
11+
import { render } from "prettyjson";
12+
import { inspect } from "util";
13+
14+
const args = yargs(process.argv.slice(2))
15+
.usage("Usage: $0 [options] pr...")
16+
.usage(" Run over specified PRs, or all if no PRs specified")
17+
.usage(" Each pr is either a number or a N-M number range")
18+
.options({
19+
"dry": { alias: ["d"], type: "boolean", default: false, desc: "don't execute actions" },
20+
"cleanup": { alias: ["c"], type: "boolean", default: true, desc: "cleanup columns when done" },
21+
"format": { alias: ["f"], choices: ["json", "yaml", "node"], desc: "format for information display" },
22+
"show-raw": { alias: ["s1"], type: "boolean", desc: "display raw query result" },
23+
"show-basic": { alias: ["s2"], type: "boolean", desc: "display basic pr info" },
24+
"show-extended": { alias: ["s3"], type: "boolean", desc: "display extended info" },
25+
"show-actions": { alias: ["s4"], type: "boolean", desc: "display actions" },
26+
"show-mutations": { alias: ["s5"], type: "boolean", desc: "display mutations" },
27+
})
28+
.coerce("_", (prs: (number|string)[]) => prs.map(pr => {
29+
if (typeof pr === "number") return (n: number) => n === pr;
30+
if (pr.match(/^\d+$/)) return (n: number) => n === +pr;
31+
const m = pr.match(/^(\d+)-(\d+)$/);
32+
if (!m) throw new Error(`bad PR or PR range argument: "${pr}"`);
33+
const lo = +m[1], hi = +m[2];
34+
return (n: number) => lo <= n && n <= hi;
35+
}))
36+
.help("h").alias("h", "help")
37+
.strict()
38+
.argv;
39+
40+
const shouldRunOn: (n: number) => boolean =
41+
args._.length === 0 ? _n => true : n => args._.some(p => p(n));
42+
43+
const xform = (x: unknown, xlate: (s: string) => string): unknown => {
44+
if (typeof x === "string") return xlate(x);
45+
if (typeof x !== "object" || x === null) return x;
46+
if (Array.isArray(x)) return x.map(e => xform(e, xlate));
47+
const o = x as { [k: string]: unknown };
48+
return Object.fromEntries(Object.keys(o).map(k => [k, xform(o[k], xlate)]));
49+
}
50+
51+
const show = (name: string, value: unknown) => {
52+
console.log(` === ${name} ===`);
53+
value = xform(value, (s: string) =>
54+
s.replace(/\n---+\s*<details><summary>(Diagnostic Information)[^]*?<\/details>/g, "...$1..."));
55+
let str = args.format === "json" ? JSON.stringify(value, undefined, 2)
56+
: args.format === "yaml" ? render(value)
57+
: inspect(value, { depth: null, colors: true });
58+
str = str.replace(/^/mg, " ");
59+
console.log(str);
60+
};
61+
62+
const start = async function () {
63+
console.log(`Getting open PRs.`);
64+
const { prNumbers: prs, cardIDs } = await getAllOpenPRsAndCardIDs();
65+
//
66+
for (const pr of prs) {
67+
if (!shouldRunOn(pr)) continue;
68+
console.log(`Processing #${pr} (${prs.indexOf(pr) + 1} of ${prs.length})...`);
69+
// Generate the info for the PR from scratch
70+
const info = await queryPRInfo(pr);
71+
if (args["show-raw"]) show("Raw Query Result", info);
72+
const state = await deriveStateForPR(info);
73+
if (args["show-basic"]) show("Basic PR Info", state);
74+
// If it didn't work, bail early
75+
if (state.type === "fail") {
76+
console.error(` Failed because of: ${state.message}`);
77+
continue;
78+
}
79+
// Show errors in log but keep processing to show in a comment too
80+
if (state.type === "error") console.error(` Error: ${state.message}`);
81+
// Show other messages too
82+
if ("message" in state) console.log(` ... ${state.message}`);
83+
// Convert the info to a set of actions for the bot
84+
const actions = computeActions(state,
85+
args["show-extended"] ? i => show("Extended Info", i) : undefined);
86+
if (args["show-actions"]) show("Actions", actions);
87+
// Act on the actions
88+
const mutations = await executePrActions(actions, info.data, args.dry);
89+
if (args["show-mutations"] ?? args.dry) show("Mutations", mutations.map(m => JSON.parse(m)));
90+
}
91+
if (args.dry || !args.cleanup) return;
92+
//
93+
console.log("Cleaning up cards");
94+
const columns = await getProjectBoardCards();
95+
const deleteObject = async (id: string, shoulda?: string) => {
96+
if (shoulda) {
97+
// don't automatically delete these, eg, PRs that were created
98+
// during the scan would end up here.
99+
return console.log(` Should delete "${id}" (${shoulda})`);
100+
}
101+
const mutation = createMutation(deleteProjectCard, { input: { cardId: id }});
102+
await mutate(mutation);
103+
}
104+
// Reduce "Recently Merged"
105+
{
106+
const recentlyMerged = columns.find(c => c.name === "Recently Merged");
107+
if (!recentlyMerged) {
108+
throw new Error(`Could not find the 'Recently Merged' column in ${columns.map(n => n.name)}`);
109+
}
110+
const { cards, totalCount } = recentlyMerged;
111+
const afterFirst50 = cards.sort((l, r) => l.updatedAt.localeCompare(r.updatedAt))
112+
.slice(50);
113+
if (afterFirst50.length > 0) {
114+
console.log(`Cutting "Recently Merged" projects to the last 50`);
115+
if (cards.length < totalCount) {
116+
console.log(` *** Note: ${totalCount - cards.length} were not seen by this query!`);
117+
}
118+
for (const card of afterFirst50) await deleteObject(card.id);
119+
}
120+
}
121+
// Handle other columns
122+
for (const column of columns) {
123+
if (column.name === "Recently Merged") continue;
124+
const ids = column.cards.map(c => c.id).filter(c => !cardIDs.includes(c));
125+
if (ids.length === 0) continue;
126+
console.log(`Cleaning up closed PRs in "${column.name}"`);
127+
// don't actually do the deletions, until I follow this and make sure that it's working fine
128+
for (const id of ids) {
129+
const info = await runQueryToGetPRForCardId(id);
130+
await deleteObject(id, info === undefined ? "???"
131+
: info.state === "CLOSED" ? undefined
132+
: "#" + info.number);
133+
}
134+
}
135+
console.log("Done");
136+
};
137+
138+
start();

0 commit comments

Comments
 (0)