Skip to content

Commit

Permalink
Merge pull request #134 from ubiquity-whilefoo/fix
Browse files Browse the repository at this point in the history
  • Loading branch information
whilefoo authored Feb 3, 2025
2 parents 53f000a + 84e5617 commit 2bcb99c
Show file tree
Hide file tree
Showing 12 changed files with 355 additions and 111 deletions.
6 changes: 6 additions & 0 deletions .github/workflows/worker-deploy.yml
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ jobs:

steps:
- uses: actions/checkout@v4
with:
ref: ${{ github.event.workflow_run.head_branch || github.ref }}

- name: Setup Bun
uses: oven-sh/setup-bun@v2
Expand All @@ -46,12 +48,16 @@ jobs:
apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
secrets: |
APP_ID
APP_PRIVATE_KEY
SUPABASE_URL
SUPABASE_KEY
BOT_USER_ID
CLOUDFLARE_ACCOUNT_ID
${{ secrets.KERNEL_PUBLIC_KEY && secrets.KERNEL_PUBLIC_KEY != '' && 'KERNEL_PUBLIC_KEY' || '' }}
env:
APP_ID: ${{ secrets.APP_ID }}
APP_PRIVATE_KEY: ${{ secrets.APP_PRIVATE_KEY }}
SUPABASE_URL: ${{ secrets.SUPABASE_URL }}
SUPABASE_KEY: ${{ secrets.SUPABASE_KEY }}
BOT_USER_ID: ${{ secrets.BOT_USER_ID }}
Expand Down
101 changes: 40 additions & 61 deletions bun.lock

Large diffs are not rendered by default.

14 changes: 11 additions & 3 deletions dist/index.js

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
"open-source"
],
"dependencies": {
"@octokit/auth-app": "^7.1.4",
"@octokit/graphql-schema": "15.25.0",
"@octokit/plugin-rest-endpoint-methods": "^13.2.6",
"@octokit/types": "^13.6.2",
Expand Down
107 changes: 72 additions & 35 deletions src/handlers/user-start-stop.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import { createAppAuth } from "@octokit/auth-app";
import { Repository } from "@octokit/graphql-schema";
import { Context, isIssueCommentEvent, Label } from "../types";
import { customOctokit } from "@ubiquity-os/plugin-sdk/octokit";
import { Context, isIssueCommentEvent } from "../types";
import { QUERY_CLOSING_ISSUE_REFERENCES } from "../utils/get-closing-issue-references";
import { closePullRequest, closePullRequestForAnIssue, getOwnerRepoFromHtmlUrl } from "../utils/issue";
import { HttpStatusCode, Result } from "./result-types";
Expand Down Expand Up @@ -77,41 +79,76 @@ export async function userPullRequest(context: Context<"pull_request.opened" | "
context.logger.info("No linked issues were found, nothing to do.");
return { status: HttpStatusCode.NOT_MODIFIED };
}

const appOctokit = new customOctokit({
authStrategy: createAppAuth,
auth: {
appId: context.env.APP_ID,
privateKey: context.env.APP_PRIVATE_KEY,
},
});

for (const issue of issues) {
if (issue && !issue.assignees.nodes?.length) {
const labels =
issue.labels?.nodes?.reduce<Label[]>((acc, curr) => {
if (curr) {
acc.push({
...curr,
id: Number(curr.id),
node_id: curr.id,
default: true,
description: curr.description ?? null,
});
}
return acc;
}, []) ?? [];
const deadline = getDeadline(labels);
if (!deadline) {
context.logger.debug("Skipping deadline posting message because no deadline has been set.");
return { status: HttpStatusCode.NOT_MODIFIED };
} else {
const issueWithComment: Context<"issue_comment.created">["payload"]["issue"] = {
...issue,
assignees: issue.assignees.nodes as Context<"issue_comment.created">["payload"]["issue"]["assignees"],
labels,
html_url: issue.url,
} as unknown as Context<"issue_comment.created">["payload"]["issue"];
context.payload = Object.assign({ issue: issueWithComment }, context.payload);
try {
return await start(context, issueWithComment, pull_request.user ?? payload.sender, []);
} catch (error) {
await closePullRequest(context, { number: pull_request.number });
// Makes sure to concatenate error messages on AggregateError for proper display
throw error instanceof AggregateError ? context.logger.error(error.errors.map(String).join("\n"), { error }) : error;
}
}
if (!issue || issue.assignees.nodes?.length) {
continue;
}

const installation = await appOctokit.rest.apps.getRepoInstallation({
owner: issue.repository.owner.login,
repo: issue.repository.name,
});
const repoOctokit = new customOctokit({
authStrategy: createAppAuth,
auth: {
appId: Number(context.env.APP_ID),
privateKey: context.env.APP_PRIVATE_KEY,
installationId: installation.data.id,
},
});

const linkedIssue = (
await repoOctokit.rest.issues.get({
owner: issue.repository.owner.login,
repo: issue.repository.name,
issue_number: issue.number,
})
).data as Context<"issue_comment.created">["payload"]["issue"];
const deadline = getDeadline(linkedIssue.labels);
if (!deadline) {
context.logger.debug("Skipping deadline posting message because no deadline has been set.");
return { status: HttpStatusCode.NOT_MODIFIED };
}

const repository = (
await repoOctokit.rest.repos.get({
owner: issue.repository.owner.login,
repo: issue.repository.name,
})
).data as Context<"issue_comment.created">["payload"]["repository"];
let organization: Context<"issue_comment.created">["payload"]["organization"] | undefined = undefined;
if (repository.owner.type === "Organization") {
organization = (
await repoOctokit.rest.orgs.get({
org: issue.repository.owner.login,
})
).data;
}
const newContext = {
...context,
octokit: repoOctokit,
payload: {
...context.payload,
issue: linkedIssue,
repository,
organization,
},
};
try {
return await start(newContext, linkedIssue, pull_request.user ?? payload.sender, []);
} catch (error) {
await closePullRequest(context, { number: pull_request.number });
// Makes sure to concatenate error messages on AggregateError for proper display
throw error instanceof AggregateError ? context.logger.error(error.errors.map(String).join("\n"), { error }) : error;
}
}
return { status: HttpStatusCode.NOT_MODIFIED };
Expand Down
2 changes: 2 additions & 0 deletions src/types/env.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ import { StaticDecode } from "@sinclair/typebox";

const ERROR_MSG = "Invalid BOT_USER_ID";
export const envSchema = T.Object({
APP_ID: T.String({ minLength: 1 }),
APP_PRIVATE_KEY: T.String({ minLength: 1 }),
SUPABASE_URL: T.String(),
SUPABASE_KEY: T.String(),
BOT_USER_ID: T.Transform(T.Union([T.String(), T.Number()], { examples: 123456 }))
Expand Down
9 changes: 9 additions & 0 deletions src/utils/get-closing-issue-references.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ export const QUERY_CLOSING_ISSUE_REFERENCES = /* GraphQL */ `
id
url
number
state
labels(first: 100) {
nodes {
id
Expand All @@ -21,6 +22,14 @@ export const QUERY_CLOSING_ISSUE_REFERENCES = /* GraphQL */ `
login
}
}
repository {
id
name
owner {
id
login
}
}
}
pageInfo {
hasNextPage
Expand Down
1 change: 1 addition & 0 deletions tests/__mocks__/db.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ export const db = factory({
owner: {
login: String,
id: Number,
type: String,
},
issues: Array,
},
Expand Down
2 changes: 1 addition & 1 deletion tests/__mocks__/issue-template.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ export default {
name: "Price: 200 USD",
},
{
name: "Time: 1h",
name: "Time: <1 Hour",
},
{
name: "Priority: 1 (Normal)",
Expand Down
2 changes: 2 additions & 0 deletions tests/main.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -745,6 +745,8 @@ export function createContext(
eventName: "issue_comment.created" as SupportedEvents,
organizations: ["ubiquity"],
env: {
APP_ID: appId,
APP_PRIVATE_KEY: "private_key",
SUPABASE_KEY: "key",
SUPABASE_URL: "url",
BOT_USER_ID: appId as unknown as number,
Expand Down
101 changes: 90 additions & 11 deletions tests/pull-request.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, jest } from "@jest/globals";
import { drop } from "@mswjs/data";
import { Repository } from "@octokit/graphql-schema";
import dotenv from "dotenv";
import { Context } from "../src/types";
import { db } from "./__mocks__/db";
Expand Down Expand Up @@ -41,6 +42,7 @@ async function setupTests() {
owner: {
login: "ubiquity",
id: 1,
type: "Organization",
},
issues: [],
});
Expand All @@ -57,6 +59,7 @@ describe("Pull-request tests", () => {

it("Should properly update the close status of a linked pull-request", async () => {
const issue = db.issue.findFirst({ where: { id: { equals: 1 } } }) as unknown as Issue;
const repo = db.repo.findFirst({ where: { id: { equals: 1 } } }) as unknown as Repository;
issue.labels = [];
const sender = db.users.findFirst({ where: { id: { equals: 1 } } }) as unknown as PayloadSender;

Expand Down Expand Up @@ -87,9 +90,7 @@ describe("Pull-request tests", () => {
assignees: {
nodes: [],
},
labels: {
nodes: [{ name: "Time: <1 Hour" }],
},
repository: repo,
},
],
},
Expand All @@ -105,24 +106,102 @@ describe("Pull-request tests", () => {
jest.unstable_mockModule("../src/adapters", () => ({
createAdapters: jest.fn(),
}));
jest.unstable_mockModule("@ubiquity-os/plugin-sdk/octokit", () => ({
customOctokit: jest.fn().mockReturnValue({
rest: {
apps: {
getRepoInstallation: jest.fn(() => Promise.resolve({ data: { id: 1 } })),
},
issues: {
get: jest.fn(() => Promise.resolve({ data: { ...issue, labels: [{ name: "Time: <1 Hour" }] } })),
},
repos: {
get: jest.fn(() => Promise.resolve({ data: repo })),
},
orgs: {
get: jest.fn(() => Promise.resolve({ data: repo?.owner })),
},
},
}),
}));
const { startStopTask } = await import("../src/plugin");
await expect(startStopTask(context)).rejects.toMatchObject({
logMessage: {
raw: expect.stringContaining("No price label is set to calculate the duration"),
},
});
});

it("Should properly update the close status of a linked pull-request", async () => {
const issue = db.issue.findFirst({ where: { id: { equals: 1 } } }) as unknown as Issue;
const repo = db.repo.findFirst({ where: { id: { equals: 1 } } }) as unknown as Repository;
issue.labels = [];
const sender = db.users.findFirst({ where: { id: { equals: 1 } } }) as unknown as PayloadSender;

const context = createContext(issue, sender, "") as Context<"pull_request.opened">;
context.eventName = "pull_request.opened";
context.payload.pull_request = {
html_url: "https://github.com/ubiquity-os-marketplace/command-start-stop",
number: 1,
user: {
id: 1,
login: userLogin,
},
} as unknown as Context<"pull_request.edited">["payload"]["pull_request"];
context.octokit = {
...context.octokit,
//@ts-expect-error partial mock of the endpoint
paginate: jest.fn(() => []),
rest: {
...context.octokit.rest,
orgs: {
//@ts-expect-error partial mock of the endpoint
getMembershipForUser: jest.fn(() => ({ data: { role: "member" } })),
pulls: {
update: jest.fn(),
},
},
};
graphql: {
paginate: jest.fn(() =>
Promise.resolve({
repository: {
pullRequest: {
closingIssuesReferences: {
nodes: [
{
assignees: {
nodes: [],
},
repository: repo,
},
],
},
},
},
})
),
},
} as unknown as Context<"pull_request.edited">["octokit"];
jest.unstable_mockModule("@supabase/supabase-js", () => ({
createClient: jest.fn(),
}));
jest.unstable_mockModule("../src/adapters", () => ({
createAdapters: jest.fn(),
}));
jest.unstable_mockModule("@ubiquity-os/plugin-sdk/octokit", () => ({
customOctokit: jest.fn().mockReturnValue({
rest: {
apps: {
getRepoInstallation: jest.fn(() => Promise.resolve({ data: { id: 1 } })),
},
issues: {
get: jest.fn(() => Promise.resolve({ data: { ...issue, labels: [{ name: "Time: <1 Hour" }] } })),
},
repos: {
get: jest.fn(() => Promise.resolve({ data: repo })),
getCollaboratorPermissionLevel: jest.fn(() => Promise.resolve({ data: { role_name: "admin" } })),
},
orgs: {
get: jest.fn(() => Promise.resolve({ data: repo?.owner })),
getMembershipForUser: jest.fn(() => ({ data: { role: "member" } })),
},
},
}),
}));
const { startStopTask } = await import("../src/plugin");
await expect(startStopTask(context)).rejects.toMatchObject({
logMessage: {
raw: "Error: This task does not reflect a business priority at the moment. You may start tasks with one of the following labels: Priority: 1 (Normal), Priority: 2 (Medium), Priority: 3 (High), Priority: 4 (Urgent), Priority: 5 (Emergency)",
Expand Down
Loading

0 comments on commit 2bcb99c

Please sign in to comment.