From f419e76e9ab56d883a36aa387ec0546ab231ff47 Mon Sep 17 00:00:00 2001 From: Mentlegen <9807008+gentlementlegen@users.noreply.github.com> Date: Sun, 7 Jul 2024 13:30:21 +0900 Subject: [PATCH] chore: added green CI check --- src/helpers/github.ts | 33 +++++++++++++++- src/helpers/update-pull-requests.ts | 26 ++++++------ tests/__mocks__/db.ts | 10 +++++ tests/__mocks__/handlers.ts | 13 ++++++ tests/__mocks__/seed.json | 16 ++++++++ tests/main.test.ts | 61 +++++++++++++++++++++++++++-- 6 files changed, 141 insertions(+), 18 deletions(-) create mode 100644 tests/__mocks__/seed.json diff --git a/src/helpers/github.ts b/src/helpers/github.ts index aa5011a6..57726d35 100644 --- a/src/helpers/github.ts +++ b/src/helpers/github.ts @@ -46,8 +46,37 @@ export async function getApprovalCount({ octokit, logger }: Context, { owner, re pull_number: pullNumber, }); return reviews.filter((review) => review.state === "APPROVED").length; - } catch (error) { - logger.error(`Error fetching reviews: ${error}`); + } catch (e) { + logger.error(`Error fetching reviews' approvals: ${e}`); return 0; } } + +export async function isCiGreen({ octokit, logger }: Context, sha: string, { owner, repo }: IssueParams) { + try { + const ref = sha; + + const { data: checkSuites } = await octokit.checks.listSuitesForRef({ + owner, + repo, + ref, + }); + + const checkSuitePromises = checkSuites.check_suites.map(async (suite) => { + const { data: checkRuns } = await octokit.checks.listForSuite({ + owner, + repo, + check_suite_id: suite.id, + }); + + return checkRuns.check_runs.every((run) => run.conclusion === "success"); + }); + + const checkResults = await Promise.all(checkSuitePromises); + + return checkResults.every((result) => result); + } catch (e) { + logger.error(`Error checking CI status: ${e}`); + return false; + } +} diff --git a/src/helpers/update-pull-requests.ts b/src/helpers/update-pull-requests.ts index 88920756..881db119 100644 --- a/src/helpers/update-pull-requests.ts +++ b/src/helpers/update-pull-requests.ts @@ -2,7 +2,7 @@ import ms from "ms"; import { PullRequest } from "../adapters/sqlite/entities/pull-request"; import { getAllTimelineEvents } from "../handlers/github-events"; import { Context } from "../types"; -import { getApprovalCount, getMergeTimeoutAndApprovalRequiredCount, IssueParams, parseGitHubUrl } from "./github"; +import { getApprovalCount, getMergeTimeoutAndApprovalRequiredCount, isCiGreen, IssueParams, parseGitHubUrl } from "./github"; type IssueEvent = { created_at?: string; @@ -11,17 +11,14 @@ type IssueEvent = { commented_at?: string; }; -async function isPullMerged(context: Context, { repo, owner, issue_number: pullNumber }: IssueParams) { - try { - const res = await context.octokit.pulls.checkIfMerged({ +async function getPullRequestDetails(context: Context, { repo, owner, issue_number: pullNumber }: IssueParams) { + return ( + await context.octokit.pulls.get({ repo, owner, pull_number: pullNumber, - }); - return res.status === 204; - } catch (e) { - return false; - } + }) + ).data; } export async function updatePullRequests(context: Context) { @@ -33,8 +30,9 @@ export async function updatePullRequests(context: Context) { for (const pullRequest of pullRequests) { try { const gitHubUrl = parseGitHubUrl(pullRequest.url); + const pullRequestDetails = await getPullRequestDetails(context, gitHubUrl); context.logger.debug(`Processing pull-request ${pullRequest.url}...`); - if (await isPullMerged(context, gitHubUrl)) { + if (pullRequestDetails.merged) { context.logger.info(`The pull request ${pullRequest.url} is already merged, nothing to do.`); try { await context.adapters.sqlite.pullRequest.delete(pullRequest.url); @@ -56,8 +54,12 @@ export async function updatePullRequests(context: Context) { const requirements = await getMergeTimeoutAndApprovalRequiredCount(context, gitHubUrl); if (isNaN(lastActivityDate.getTime()) || isPastOffset(lastActivityDate, requirements.mergeTimeout)) { if ((await getApprovalCount(context, gitHubUrl)) > 0) { - context.logger.info(`Pull-request ${pullRequest.url} is past its due date (${requirements} after ${lastActivityDate}), will merge.`); - await mergePullRequest(context, pullRequest, gitHubUrl); + if (await isCiGreen(context, pullRequestDetails.head.sha, gitHubUrl)) { + context.logger.info(`Pull-request ${pullRequest.url} is past its due date (${requirements} after ${lastActivityDate}), will merge.`); + await mergePullRequest(context, pullRequest, gitHubUrl); + } else { + context.logger.info(`Pull-request ${pullRequest.url} does not pass all CI tests, won't merge.`); + } } else { context.logger.info(`Pull-request ${pullRequest.url} does not have sufficient reviewer approvals to be merged.`); } diff --git a/tests/__mocks__/db.ts b/tests/__mocks__/db.ts index 7df690c0..107019aa 100644 --- a/tests/__mocks__/db.ts +++ b/tests/__mocks__/db.ts @@ -9,4 +9,14 @@ export const db = factory({ id: primaryKey(Number), name: String, }, + pullRequests: { + id: primaryKey(Number), + head: { + sha: String, + }, + }, + ci: { + id: primaryKey(Number), + conclusion: String, + }, }); diff --git a/tests/__mocks__/handlers.ts b/tests/__mocks__/handlers.ts index 67c6ce40..ec75f3e1 100644 --- a/tests/__mocks__/handlers.ts +++ b/tests/__mocks__/handlers.ts @@ -1,4 +1,5 @@ import { http, HttpResponse } from "msw"; +import { db } from "./db"; /** * Intercepts the routes and returns a custom payload @@ -10,6 +11,9 @@ export const handlers = [ http.get("https://api.github.com/repos/:org/:repo/pulls/:id/merge", () => { return HttpResponse.json(); }), + http.get("https://api.github.com/repos/:org/:repo/pulls/:id/reviews", () => { + return HttpResponse.json([]); + }), http.get("https://api.github.com/repos/:org/:repo/issues/:id/timeline", () => { return HttpResponse.json(); }), @@ -19,4 +23,13 @@ export const handlers = [ http.get("https://api.github.com/repos/:org/:repo/collaborators/:login", () => { return HttpResponse.json(); }), + http.get("https://api.github.com/repos/:org/:repo/commits/:id/check-suites", () => { + return HttpResponse.json({ check_suites: db.ci.getAll() }); + }), + http.get("https://api.github.com/repos/:org/:repo/check-suites/:id/check-runs", () => { + return HttpResponse.json({ check_runs: db.ci.getAll() }); + }), + http.get("https://api.github.com/repos/:org/:repo/pulls/:id", ({ params: { id } }) => { + return HttpResponse.json(db.pullRequests.findFirst({ where: { id: { equals: Number(id) } } })); + }), ]; diff --git a/tests/__mocks__/seed.json b/tests/__mocks__/seed.json new file mode 100644 index 00000000..e97dfa2e --- /dev/null +++ b/tests/__mocks__/seed.json @@ -0,0 +1,16 @@ +{ + "pullRequests": [ + { + "id": 1, + "head": { + "sha": "1" + } + } + ], + "ci": [ + { + "id": 1, + "conclusion": "success" + } + ] +} diff --git a/tests/main.test.ts b/tests/main.test.ts index c4e7b97d..9a117d0d 100644 --- a/tests/main.test.ts +++ b/tests/main.test.ts @@ -1,12 +1,15 @@ +import { drop } from "@mswjs/data"; import { Octokit } from "@octokit/rest"; import { http, HttpResponse } from "msw"; import * as fs from "node:fs"; import { initializeDataSource } from "../src/adapters/sqlite/data-source"; import { PullRequest } from "../src/adapters/sqlite/entities/pull-request"; -import { getMergeTimeoutAndApprovalRequiredCount } from "../src/helpers/github"; +import { getMergeTimeoutAndApprovalRequiredCount, isCiGreen } from "../src/helpers/github"; +import { db } from "./__mocks__/db"; import { server } from "./__mocks__/node"; import { expect, describe, beforeAll, beforeEach, afterAll, afterEach, it, jest } from "@jest/globals"; import { Context } from "../src/types"; +import seed from "./__mocks__/seed.json"; beforeAll(() => server.listen()); afterEach(() => server.resetHandlers()); @@ -14,6 +17,7 @@ afterAll(() => server.close()); const htmlUrl = "https://github.com/ubiquibot/automated-merging/pull/1"; const actionsGithubPackage = "@actions/github"; +const issueParams = { owner: "ubiquibot", repo: "automated-merging", issue_number: 1 }; describe("Action tests", () => { let dbName = `database/tests/test.db`; @@ -23,6 +27,13 @@ describe("Action tests", () => { jest.resetModules(); dbName = `database/tests/${expect.getState().currentTestName}.db`; fs.rmSync(dbName, { force: true }); + drop(db); + for (const table of Object.keys(seed)) { + const tableName = table as keyof typeof seed; + for (const row of seed[tableName]) { + db[tableName].create(row); + } + } }); it("Should add a pull request in the DB on PR opened", async () => { @@ -67,6 +78,15 @@ describe("Action tests", () => { pr.url = htmlUrl; pr.lastActivity = new Date(); await pr.save(); + server.use( + http.get( + "https://api.github.com/repos/:org/:repo/pulls/:id/reviews", + () => { + return HttpResponse.json([{ state: "APPROVED" }, { state: "APPROVED" }]); + }, + { once: true } + ) + ); jest.mock(actionsGithubPackage, () => ({ context: { repo: { @@ -146,7 +166,6 @@ describe("Action tests", () => { pr.url = htmlUrl; const lastActivityDate = new Date(); lastActivityDate.setDate(new Date().getDate() - 8); - console.log(lastActivityDate); pr.lastActivity = lastActivityDate; await pr.save(); server.use( @@ -209,7 +228,7 @@ describe("Action tests", () => { }, octokit: new Octokit(), } as unknown as Context; - await expect(getMergeTimeoutAndApprovalRequiredCount(context, { owner: "ubiquibot", repo: "automated-merging", issue_number: 1 })).resolves.toEqual({ + await expect(getMergeTimeoutAndApprovalRequiredCount(context, issueParams)).resolves.toEqual({ mergeTimeout: collaboratorMergeTimeout, requiredApprovalCount: collaboratorMinimumApprovalsRequired, }); @@ -222,9 +241,43 @@ describe("Action tests", () => { { once: true } ) ); - await expect(getMergeTimeoutAndApprovalRequiredCount(context, { owner: "ubiquibot", repo: "automated-merging", issue_number: 1 })).resolves.toEqual({ + await expect(getMergeTimeoutAndApprovalRequiredCount(context, issueParams)).resolves.toEqual({ mergeTimeout: contributorMergeTimeout, requiredApprovalCount: contributorMinimumApprovalsRequired, }); }); + + it("Should check if the CI tests are all passing", async () => { + server.use( + http.get( + "https://api.github.com/repos/:org/:repo/commits/:id/check-suites", + () => { + return HttpResponse.json({ check_suites: [{ id: 1 }] }); + }, + { once: true } + ) + ); + server.use( + http.get( + "https://api.github.com/repos/:org/:repo/check-suites/:id/check-runs", + () => { + return HttpResponse.json({ check_runs: [{ id: 1, conclusion: "success" }] }); + }, + { once: true } + ) + ); + const context = { + logger: { + debug: console.log, + }, + payload: { + pull_request: { + assignees: [{ login: "ubiquibot" }], + }, + }, + config: {}, + octokit: new Octokit(), + } as unknown as Context; + await expect(isCiGreen(context, "1", issueParams)).resolves.toEqual(true); + }); });