Skip to content

Commit

Permalink
Add config file for tweaking behavior (#6)
Browse files Browse the repository at this point in the history
  • Loading branch information
Holi0317 authored Feb 19, 2024
1 parent 5177d63 commit 9fecd50
Show file tree
Hide file tree
Showing 7 changed files with 298 additions and 121 deletions.
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@
"dependencies": {
"@cfworker/sentry": "^2.0.0",
"@octokit/auth-app": "^6.0.3",
"js-toml": "^1.0.0",
"octokit": "^3.1.2",
"parse-git-diff": "^0.0.15",
"zod": "^3.22.4"
Expand Down
75 changes: 75 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

81 changes: 76 additions & 5 deletions src/config.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,80 @@
export const Config = {
import { RequestError } from "octokit";
import { z } from "zod";
import { load } from "js-toml";
import type { Context } from "./context";

const ConfigSchema = z.object({
labels: z
.array(
z.object({
name: z.string(),
color: z.string(),
threshold: z.number().gte(0),
}),
)
.refine(
(labels) => {
const set = new Set(labels.map((label) => label.name));
return set.size === labels.length;
},
{ message: "Label name should be unique" },
)
.transform((labels) => {
// Make sure all labels are ordered in descending order of threshold.
// Size logic depends on this property.
const clone = [...labels];
clone.sort((a, b) => b.threshold - a.threshold);
return clone;
}),
});

export type ConfigType = z.infer<typeof ConfigSchema>;

const CONFIG_PATH = ".github/prsize.toml";

const DEFAULT_CONFIG: ConfigType = {
labels: [
{ name: "size/x-small", color: "008000", threshold: 0 },
{ name: "size/small", color: "008000", threshold: 100 },
{ name: "size/medium", color: "FFFF00", threshold: 200 },
{ name: "size/large", color: "FF0000", threshold: 500 },
{ name: "size/x-large", color: "FF0000", threshold: 1000 },
{ name: "size/large", color: "FF0000", threshold: 500 },
{ name: "size/medium", color: "FFFF00", threshold: 200 },
{ name: "size/small", color: "008000", threshold: 100 },
{ name: "size/x-small", color: "008000", threshold: 0 },
],
};

export async function readConfig(ctx: Context): Promise<ConfigType> {
let content: unknown;

try {
const resp = await ctx.octo.request(
"GET /repos/{owner}/{repo}/contents/{path}",
{
owner: ctx.owner,
repo: ctx.repo,
path: CONFIG_PATH,
ref: ctx.pr.base.ref,
mediaType: {
format: "raw",
},
},
);

content = resp.data as unknown;
} catch (error) {
if (error instanceof RequestError && error.status === 404) {
console.info("Config not found. Using default config");
return DEFAULT_CONFIG;
}

throw error;
}

if (typeof content !== "string") {
throw new Error(
`Config path ${CONFIG_PATH} seems to be a directory. Cannot read config content`,
);
}

const tom = load(content);
return ConfigSchema.parse(tom);
}
21 changes: 21 additions & 0 deletions src/context.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import type { Octokit } from "octokit";
import type { PullRequest } from "@octokit/webhooks-types";

export class Context {
public constructor(
public readonly octo: Octokit,
public readonly pr: PullRequest,
) {}

public get owner() {
return this.pr.base.repo.owner.login;
}

public get repo() {
return this.pr.base.repo.name;
}

public get prNum() {
return this.pr.number;
}
}
26 changes: 9 additions & 17 deletions src/handler.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import parseGitDiff, { type AnyChunk } from "parse-git-diff";
import { ConfigType } from "./config";

export interface HandlerResponse {
label: string | null;
Expand Down Expand Up @@ -33,6 +34,8 @@ function summarizeChunk(chunk: AnyChunk): Summary {
}

export class Handler {
public constructor(private readonly config: ConfigType) {}

public async run(diffFile: string): Promise<HandlerResponse> {
const summaryMap = this.genDiffSummary(diffFile);
const summary: Summary = { add: 0, del: 0 };
Expand Down Expand Up @@ -74,26 +77,15 @@ export class Handler {
return summary;
}

private getSize(summary: Summary): string {
// TODO: Respect config settings
private getSize(summary: Summary): string | null {
const size = summary.add + summary.del;

if (size >= 1000) {
return "size/x-large";
}

if (size >= 500) {
return "size/large";
}

if (size >= 200) {
return "size/medium";
}

if (size >= 100) {
return "size/small";
for (const preset of this.config.labels) {
if (size >= preset.threshold) {
return preset.name;
}
}

return "size/x-small";
return null;
}
}
36 changes: 28 additions & 8 deletions src/octo.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,22 +2,25 @@ import { App } from "octokit";
import type { PullRequest } from "@octokit/webhooks-types";
import { Handler } from "./handler";
import { unwrap } from "./utils";
import { Updater } from "./updater";
import { CommentUpdater, LabelUpdater } from "./updater";
import { ConfigType, readConfig } from "./config";
import { Context } from "./context";

async function handle(app: App, installation_id: number, pr: PullRequest) {
console.log(
`Handling PR ${pr.base.repo.full_name}#${pr.number}. Installation ID = ${installation_id}`,
);

const octo = await app.getInstallationOctokit(installation_id);
const ctx = new Context(octo, pr);

if (pr.state === "closed") {
if (ctx.pr.state === "closed") {
console.info("PR is closed. Ignoring the webhook");
return;
}

console.log("Fetching diff for the PR");
const response = await octo.request(
const response = await ctx.octo.request(
"GET /repos/{owner}/{repo}/pulls/{pull_number}",
{
owner: pr.base.repo.owner.login,
Expand All @@ -33,15 +36,32 @@ async function handle(app: App, installation_id: number, pr: PullRequest) {
// https://github.com/octokit/request.js/issues/463#issuecomment-1164800888
const diffFile = response.data as unknown as string;

const handler = new Handler();
const commentUpdater = new CommentUpdater(ctx);

let config: ConfigType;
try {
config = await readConfig(ctx);
} catch (error) {
console.error("Failed to read config", error);
await commentUpdater.updateComment(`Failed to read config. Pullsize bot is not operating!
Exception detail:
\`\`\`
${error}
\`\`\`
`);
return;
}

const handler = new Handler(config);
const resp = await handler.run(diffFile);

console.log("Ran handler. Updating PR comment and label", resp);

const updater = new Updater(octo, pr);
await updater.updateComment(resp.comment);
await updater.createLabels();
await updater.updateLabel(resp.label);
const labelUpdater = new LabelUpdater(ctx, config);
await commentUpdater.updateComment(resp.comment);
await labelUpdater.createLabels();
await labelUpdater.updateLabel(resp.label);
}

export function useOctoApp(env: Env) {
Expand Down
Loading

0 comments on commit 9fecd50

Please sign in to comment.