Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions apps/bot/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import { ApplicationFunctionOptions, Probot } from "probot";

import { registerMergeableHandlers } from "./mergeable.js";

export default (app: Probot, { getRouter }: ApplicationFunctionOptions) => {
if (getRouter) {
const router = getRouter("/");
Expand All @@ -9,6 +11,8 @@ export default (app: Probot, { getRouter }: ApplicationFunctionOptions) => {
});
}

registerMergeableHandlers(app);

app.on("issues.opened", async (context) => {
const issueComment = context.issue({
body: "Thanks for opening this issue!",
Expand Down
185 changes: 185 additions & 0 deletions apps/bot/src/mergeable.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,185 @@
import { Probot } from "probot";

const BOT_CI_CHECK_NAME = "bot_ci";
const MERGEABLE_CHECK_NAME = "Mergeable: bot_ci";

interface CheckRun {
id: number;
name: string;
status: string;
conclusion: string | null;
}

type ProbotContext = Parameters<Parameters<Probot["on"]>[1]>[0];

async function getBotCiCheckRun(
context: ProbotContext,
owner: string,
repo: string,
ref: string,
): Promise<CheckRun | null> {
const { data } = await context.octokit.checks.listForRef({
owner,
repo,
ref,
check_name: BOT_CI_CHECK_NAME,
});

if (data.check_runs.length === 0) {
return null;
}

const checkRun = data.check_runs[0];
return {
id: checkRun.id,
name: checkRun.name,
status: checkRun.status,
conclusion: checkRun.conclusion,
};
}

async function createOrUpdateMergeableCheck(
context: ProbotContext,
owner: string,
repo: string,
headSha: string,
botCiCheck: CheckRun | null,
): Promise<void> {
const existingChecks = await context.octokit.checks.listForRef({
owner,
repo,
ref: headSha,
check_name: MERGEABLE_CHECK_NAME,
});

let status: "queued" | "in_progress" | "completed";
let conclusion:
| "success"
| "failure"
| "neutral"
| "cancelled"
| "skipped"
| "timed_out"
| "action_required"
| undefined;
let title: string;
let summary: string;

if (botCiCheck === null) {
status = "completed";
conclusion = "success";
title = "bot_ci not triggered";
summary =
"The bot_ci check was not triggered for this PR, so merging is allowed.";
} else if (botCiCheck.status !== "completed") {
status = "in_progress";
conclusion = undefined;
title = "Waiting for bot_ci to complete";
summary = `The bot_ci check is currently ${botCiCheck.status}. Merging is blocked until it completes successfully.`;
} else if (botCiCheck.conclusion === "success") {
status = "completed";
conclusion = "success";
title = "bot_ci passed";
summary = "The bot_ci check has passed. Merging is allowed.";
} else {
status = "completed";
conclusion = "failure";
title = `bot_ci ${botCiCheck.conclusion || "failed"}`;
summary = `The bot_ci check has ${botCiCheck.conclusion || "failed"}. Merging is blocked.`;
}

if (existingChecks.data.check_runs.length > 0) {
const existingCheck = existingChecks.data.check_runs[0];
await context.octokit.checks.update({
owner,
repo,
check_run_id: existingCheck.id,
status,
conclusion,
output: {
title,
summary,
},
});
} else {
await context.octokit.checks.create({
owner,
repo,
name: MERGEABLE_CHECK_NAME,
head_sha: headSha,
status,
conclusion,
output: {
title,
summary,
},
});
}
}

async function handleBotCiCheck(
context: ProbotContext,
owner: string,
repo: string,
headSha: string,
): Promise<void> {
const botCiCheck = await getBotCiCheckRun(context, owner, repo, headSha);
await createOrUpdateMergeableCheck(context, owner, repo, headSha, botCiCheck);
}

export function registerMergeableHandlers(app: Probot): void {
app.on(
["check_run.created", "check_run.completed", "check_run.rerequested"],
async (context) => {
const checkRun = context.payload.check_run;

if (checkRun.name !== BOT_CI_CHECK_NAME) {
return;
}

const pullRequests = checkRun.pull_requests;
if (pullRequests.length === 0) {
context.log.info("No pull requests associated with this check run");
return;
}

const owner = context.payload.repository.owner.login;
const repo = context.payload.repository.name;
const headSha = checkRun.head_sha;

context.log.info(
`bot_ci check ${context.payload.action} for ${owner}/${repo}@${headSha}`,
);

try {
await handleBotCiCheck(context, owner, repo, headSha);
} catch (error) {
context.log.error(`Failed to handle bot_ci check: ${error}`);
}
},
);

app.on(
[
"pull_request.opened",
"pull_request.synchronize",
"pull_request.reopened",
],
async (context) => {
const pr = context.payload.pull_request;
const owner = context.payload.repository.owner.login;
const repo = context.payload.repository.name;
const headSha = pr.head.sha;

context.log.info(
`PR ${context.payload.action} for ${owner}/${repo}#${pr.number}`,
);

try {
await handleBotCiCheck(context, owner, repo, headSha);
} catch (error) {
context.log.error(`Failed to handle PR event: ${error}`);
}
},
);
}
27 changes: 27 additions & 0 deletions apps/bot/test/fixtures/check_run.completed.failure.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
{
"action": "completed",
"check_run": {
"id": 1,
"name": "bot_ci",
"head_sha": "abc123",
"status": "completed",
"conclusion": "failure",
"pull_requests": [
{
"number": 123
}
]
},
"repository": {
"id": 1,
"node_id": "MDEwOlJlcG9zaXRvcnkx",
"name": "testing-things",
"full_name": "hiimbex/testing-things",
"owner": {
"login": "hiimbex"
}
},
"installation": {
"id": 2
}
}
27 changes: 27 additions & 0 deletions apps/bot/test/fixtures/check_run.completed.success.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
{
"action": "completed",
"check_run": {
"id": 1,
"name": "bot_ci",
"head_sha": "abc123",
"status": "completed",
"conclusion": "success",
"pull_requests": [
{
"number": 123
}
]
},
"repository": {
"id": 1,
"node_id": "MDEwOlJlcG9zaXRvcnkx",
"name": "testing-things",
"full_name": "hiimbex/testing-things",
"owner": {
"login": "hiimbex"
}
},
"installation": {
"id": 2
}
}
27 changes: 27 additions & 0 deletions apps/bot/test/fixtures/check_run.created.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
{
"action": "created",
"check_run": {
"id": 1,
"name": "bot_ci",
"head_sha": "abc123",
"status": "in_progress",
"conclusion": null,
"pull_requests": [
{
"number": 123
}
]
},
"repository": {
"id": 1,
"node_id": "MDEwOlJlcG9zaXRvcnkx",
"name": "testing-things",
"full_name": "hiimbex/testing-things",
"owner": {
"login": "hiimbex"
}
},
"installation": {
"id": 2
}
}
24 changes: 24 additions & 0 deletions apps/bot/test/fixtures/pull_request.opened.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
{
"action": "opened",
"number": 123,
"pull_request": {
"number": 123,
"html_url": "https://github.com/example/repo/pull/123",
"created_at": "2024-01-01T00:00:00Z",
"head": {
"sha": "abc123"
}
},
"repository": {
"id": 1,
"node_id": "MDEwOlJlcG9zaXRvcnkx",
"name": "testing-things",
"full_name": "hiimbex/testing-things",
"owner": {
"login": "hiimbex"
}
},
"installation": {
"id": 2
}
}
Loading