Skip to content

Commit

Permalink
Merge pull request #91 from gentlementlegen/fix/task-limit
Browse files Browse the repository at this point in the history
  • Loading branch information
0x4007 authored Nov 27, 2024
2 parents 2d6199a + adca88b commit 45a0726
Show file tree
Hide file tree
Showing 11 changed files with 155 additions and 65 deletions.
Binary file modified bun.lockb
Binary file not shown.
1 change: 1 addition & 0 deletions manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@
"properties": {
"reviewDelayTolerance": {
"default": "1 Day",
"description": "How long shall the wait be for a reviewer to take action?",
"type": "string"
},
"taskStaleTimeoutDuration": {
Expand Down
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"name": "@ubiquity-os/command-start-stop",
"version": "1.0.0",
"description": "Enables the assignment and graceful unassignment of tasks to contributors.",
"main": "src/worker.ts",
"main": "src/index.ts",
"author": "Ubiquity DAO",
"license": "MIT",
"engines": {
Expand Down Expand Up @@ -35,6 +35,7 @@
"@ubiquity-os/plugin-sdk": "^1.1.0",
"@ubiquity-os/ubiquity-os-logger": "^1.3.2",
"dotenv": "^16.4.4",
"hono": "^4.6.12",
"ms": "^2.1.3"
},
"devDependencies": {
Expand Down
4 changes: 2 additions & 2 deletions src/handlers/shared/start.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { AssignedIssue, Context, ISSUE_TYPE, Label } from "../../types";
import { isUserCollaborator } from "../../utils/get-user-association";
import { addAssignees, addCommentToIssue, getAssignedIssues, getAvailableOpenedPullRequests, getTimeValue, isParentIssue } from "../../utils/issue";
import { addAssignees, addCommentToIssue, getAssignedIssues, getPendingOpenedPullRequests, getTimeValue, isParentIssue } from "../../utils/issue";
import { HttpStatusCode, Result } from "../result-types";
import { hasUserBeenUnassigned } from "./check-assignments";
import { checkTaskStale } from "./check-task-stale";
Expand Down Expand Up @@ -198,7 +198,7 @@ async function fetchUserIds(context: Context, username: string[]) {
}

async function handleTaskLimitChecks(username: string, context: Context, logger: Context["logger"], sender: string) {
const openedPullRequests = await getAvailableOpenedPullRequests(context, username);
const openedPullRequests = await getPendingOpenedPullRequests(context, username);
const assignedIssues = await getAssignedIssues(context, username);
const { limit } = await getUserRoleAndTaskLimit(context, username);

Expand Down
11 changes: 6 additions & 5 deletions src/worker.ts → src/index.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
import { createPlugin } from "@ubiquity-os/plugin-sdk";
import { Manifest } from "@ubiquity-os/plugin-sdk/manifest";
import { LogLevel } from "@ubiquity-os/ubiquity-os-logger";
import type { ExecutionContext } from "hono";
import manifest from "../manifest.json";
import { createAdapters } from "./adapters";
import { startStopTask } from "./plugin";
import { Command } from "./types/command";
import { SupportedEvents } from "./types/context";
import { Env, envSchema } from "./types/env";
import { PluginSettings, pluginSettingsSchema } from "./types/plugin-input";
import manifest from "../manifest.json";
import { Command } from "./types/command";
import { startStopTask } from "./plugin";
import { Manifest } from "@ubiquity-os/plugin-sdk/manifest";
import { LogLevel } from "@ubiquity-os/ubiquity-os-logger";

export default {
async fetch(request: Request, env: Env, executionCtx?: ExecutionContext) {
Expand All @@ -27,6 +27,7 @@ export default {
settingsSchema: pluginSettingsSchema,
logLevel: env.LOG_LEVEL as LogLevel,
kernelPublicKey: env.KERNEL_PUBLIC_KEY,
bypassSignatureVerification: process.env.NODE_ENV === "local",
}
).fetch(request, env, executionCtx);
},
Expand Down
9 changes: 4 additions & 5 deletions src/plugin.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,13 @@
import { createClient } from "@supabase/supabase-js";
import { createAdapters } from "./adapters";
import { HttpStatusCode } from "./handlers/result-types";
import { commandHandler, userPullRequest, userSelfAssign, userStartStop, userUnassigned } from "./handlers/user-start-stop";
import { Context } from "./types";
import { listOrganizations } from "./utils/list-organizations";
import { HttpStatusCode } from "./handlers/result-types";
import { createAdapters } from "./adapters";
import { createClient } from "@supabase/supabase-js";

export async function startStopTask(context: Context) {
context.adapters = createAdapters(createClient(context.env.SUPABASE_URL, context.env.SUPABASE_KEY), context as Context);
const organizations = await listOrganizations(context);
context.organizations = organizations;
context.organizations = await listOrganizations(context);

if (context.command) {
return await commandHandler(context);
Expand Down
2 changes: 1 addition & 1 deletion src/types/plugin-input.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ function maxConcurrentTasks() {

export const pluginSettingsSchema = T.Object(
{
reviewDelayTolerance: T.String({ default: "1 Day" }),
reviewDelayTolerance: T.String({ default: "1 Day", description: "How long shall the wait be for a reviewer to take action?" }),
taskStaleTimeoutDuration: T.String({ default: "30 Days" }),
startRequiresWallet: T.Boolean({ default: true }),
maxConcurrentTasks: maxConcurrentTasks(),
Expand Down
78 changes: 66 additions & 12 deletions src/utils/issue.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import { RestEndpointMethodTypes } from "@octokit/plugin-rest-endpoint-methods";
import { Endpoints } from "@octokit/types";
import ms from "ms";
import { AssignedIssueScope, Role } from "../types";
import { Context } from "../types/context";
import { GitHubIssueSearch, RepoIssues, Review } from "../types/payload";
import { getLinkedPullRequests, GetLinkedResults } from "./get-linked-prs";
import { getAllPullRequestsFallback, getAssignedIssuesFallback } from "./get-pull-requests-fallback";
import { AssignedIssueScope, Role } from "../types";

export function isParentIssue(body: string) {
const parentPattern = /-\s+\[( |x)\]\s+#\d+/;
Expand Down Expand Up @@ -248,7 +248,62 @@ export function getOwnerRepoFromHtmlUrl(url: string) {
};
}

export async function getAvailableOpenedPullRequests(context: Context, username: string) {
async function getReviewByUser(context: Context, pullRequest: Awaited<ReturnType<typeof getOpenedPullRequestsForUser>>[0]) {
const { owner, repo } = getOwnerRepoFromHtmlUrl(pullRequest.html_url);
const reviews = (await getAllPullRequestReviews(context, pullRequest.number, owner, repo)).sort((a, b) => {
if (!a?.submitted_at || !b?.submitted_at) {
return 0;
}
return new Date(b.submitted_at).getTime() - new Date(a.submitted_at).getTime();
});
const latestReviewsByUser: Map<number, Review> = new Map();
for (const review of reviews) {
const isReviewRequestedForUser =
"requested_reviewers" in pullRequest && pullRequest.requested_reviewers && pullRequest.requested_reviewers.some((o) => o.id === review.user?.id);
if (!isReviewRequestedForUser && review.user?.id && !latestReviewsByUser.has(review.user?.id)) {
latestReviewsByUser.set(review.user?.id, review);
}
}

return latestReviewsByUser;
}

async function shouldSkipPullRequest(
context: Context,
pullRequest: Awaited<ReturnType<typeof getOpenedPullRequestsForUser>>[0],
reviews: Awaited<ReturnType<typeof getReviewByUser>>,
{ owner, repo, issueNumber }: { owner: string; repo: string; issueNumber: number },
reviewDelayTolerance: string
) {
const timeline = await context.octokit.paginate(context.octokit.rest.issues.listEventsForTimeline, {
owner,
repo,
issue_number: issueNumber,
});
const reviewEvent = timeline.filter((o) => o.event === "review_requested").pop();
const referenceTime = reviewEvent && "created_at" in reviewEvent ? new Date(reviewEvent.created_at).getTime() : new Date(pullRequest.created_at).getTime();

// If no reviews exist, check time reference
if (reviews.size === 0) {
return new Date().getTime() - referenceTime >= getTimeValue(reviewDelayTolerance);
}

// If changes are requested, do not skip
if (Array.from(reviews.values()).some((review) => review.state === "CHANGES_REQUESTED")) {
return true;
}

// If no approvals exist or time reference has exceeded review delay tolerance
const hasApproval = Array.from(reviews.values()).some((review) => review.state === "APPROVED");
const isTimePassed = new Date().getTime() - referenceTime >= getTimeValue(reviewDelayTolerance);

return hasApproval || !isTimePassed;
}

/**
* Returns all the pull-requests pending to be approved, counting as a malus against the PR user's quota.
*/
export async function getPendingOpenedPullRequests(context: Context, username: string) {
const { reviewDelayTolerance } = context.config;
if (!reviewDelayTolerance) return [];

Expand All @@ -259,16 +314,15 @@ export async function getAvailableOpenedPullRequests(context: Context, username:
const openedPullRequest = openedPullRequests[i];
if (!openedPullRequest) continue;
const { owner, repo } = getOwnerRepoFromHtmlUrl(openedPullRequest.html_url);
const reviews = await getAllPullRequestReviews(context, openedPullRequest.number, owner, repo);

if (reviews.length > 0) {
const approvedReviews = reviews.find((review) => review.state === "APPROVED");
if (approvedReviews) {
result.push(openedPullRequest);
}
}

if (reviews.length === 0 && new Date().getTime() - new Date(openedPullRequest.created_at).getTime() >= getTimeValue(reviewDelayTolerance)) {
const latestReviewsByUser = await getReviewByUser(context, openedPullRequest);
const shouldSkipPr = await shouldSkipPullRequest(
context,
openedPullRequest,
latestReviewsByUser,
{ owner, repo, issueNumber: openedPullRequest.number },
reviewDelayTolerance
);
if (!shouldSkipPr) {
result.push(openedPullRequest);
}
}
Expand Down
104 changes: 69 additions & 35 deletions tests/http/run.http
Original file line number Diff line number Diff line change
Expand Up @@ -8,25 +8,59 @@ X-GitHub-Delivery: mock-delivery-id
"action": "created",
"eventName": "issue_comment.created",
"authToken": "{{GITHUB_TOKEN}}",
"ref": "1234",
"signature": "1234",
"settings": {},
"stateId": "1234",
"command": null,
"eventPayload": {
"issue": {
"url": "https://api.github.com/repos/ubiquity/work.ubq.fi/issues/119",
"repository_url": "https://api.github.com/repos/ubiquity/work.ubq.fi",
"labels_url": "https://api.github.com/repos/ubiquity/work.ubq.fi/issues/119/labels{/name}",
"comments_url": "https://api.github.com/repos/ubiquity/work.ubq.fi/issues/119/comments",
"events_url": "https://api.github.com/repos/ubiquity/work.ubq.fi/issues/119/events",
"html_url": "https://github.com/ubiquity/work.ubq.fi/issues/119",
"url": "https://api.github.com/repos/{{owner}}/{{repo}}/issues/{{issueId}}",
"repository_url": "https://api.github.com/repos/{{owner}}/{{repo}}",
"labels_url": "https://api.github.com/repos/{{owner}}/{{repo}}/issues/{{issueId}}/labels{/name}",
"comments_url": "https://api.github.com/repos/{{owner}}/{{repo}}/issues/{{issueId}}/comments",
"events_url": "https://api.github.com/repos/{{owner}}/{{repo}}/issues/{{issueId}}/events",
"html_url": "https://github.com/{{owner}}/{{repo}}/issues/{{issueId}}",
"id": 12345678,
"node_id": "I_kwDOA1234567",
"number": 119,
"number": {{issueId}},
"title": "Sample Issue Title",
"labels": [
{
"id": 7215029067,
"node_id": "LA_kwDOMIXMk88AAAABrgybSw",
"url": "https://api.github.com/repos/Meniole/command-start-stop/labels/Time:%20%3C1%20Day",
"name": "Time: <1 Day",
"color": "ededed",
"default": false,
"description": ""
},
{
"id": 7215029076,
"node_id": "LA_kwDOMIXMk88AAAABrgybVA",
"url": "https://api.github.com/repos/Meniole/command-start-stop/labels/Priority:%204%20(Urgent)",
"name": "Priority: 4 (Urgent)",
"color": "ededed",
"default": false,
"description": ""
},
{
"id": 7671693611,
"node_id": "LA_kwDOMIXMk88AAAAByUTBKw",
"url": "https://api.github.com/repos/Meniole/command-start-stop/labels/Price:%2037.5%20USD",
"name": "Price: 37.5 USD",
"color": "1f883d",
"default": false,
"description": null
}
],
"user": {
"login": "sshivaditya2019",
"login": "ubiquity-ubiquibot",
"id": 12345678,
"node_id": "MDQ6VXNlcjEyMzQ1Njc4",
"avatar_url": "https://avatars.githubusercontent.com/u/12345678?v=4",
"url": "https://api.github.com/users/sshivaditya2019",
"html_url": "https://github.com/sshivaditya2019",
"url": "https://api.github.com/users/ubiquity-ubiquibot",
"html_url": "https://github.com/ubiquity-ubiquibot",
"type": "User",
"site_admin": false
},
Expand All @@ -42,18 +76,18 @@ X-GitHub-Delivery: mock-delivery-id
"body": "Original issue description"
},
"comment": {
"url": "https://api.github.com/repos/ubiquity/work.ubq.fi/issues/comments/1234567890",
"html_url": "https://github.com/ubiquity/work.ubq.fi/issues/119#issuecomment-1234567890",
"issue_url": "https://api.github.com/repos/ubiquity/work.ubq.fi/issues/119",
"url": "https://api.github.com/repos/{{owner}}/{{repo}}/issues/comments/1234567890",
"html_url": "https://github.com/{{owner}}/{{repo}}/issues/{{issueId}}#issuecomment-1234567890",
"issue_url": "https://api.github.com/repos/{{owner}}/{{repo}}/issues/{{issueId}}",
"id": 1234567890,
"node_id": "IC_kwDOA1234567",
"user": {
"login": "sshivaditya2019",
"id": 12345678,
"login": "ubiquity-ubiquibot",
"id": 163369652,
"node_id": "MDQ6VXNlcjEyMzQ1Njc4",
"avatar_url": "https://avatars.githubusercontent.com/u/12345678?v=4",
"url": "https://api.github.com/users/sshivaditya2019",
"html_url": "https://github.com/sshivaditya2019",
"url": "https://api.github.com/users/ubiquity-ubiquibot",
"html_url": "https://github.com/ubiquity-ubiquibot",
"type": "User",
"site_admin": false
},
Expand All @@ -64,20 +98,20 @@ X-GitHub-Delivery: mock-delivery-id
"repository": {
"id": 98765432,
"node_id": "R_kgDOBcDEFG",
"name": "work.ubq.fi",
"full_name": "ubiquity/work.ubq.fi",
"name": "{{repo}}",
"full_name": "{{owner}}/{{repo}}",
"private": false,
"owner": {
"login": "ubiquity",
"login": "{{owner}}",
"id": 87654321,
"node_id": "MDEyOk9yZ2FuaXphdGlvbjg3NjU0MzIx",
"avatar_url": "https://avatars.githubusercontent.com/u/87654321?v=4",
"url": "https://api.github.com/users/ubiquity",
"html_url": "https://github.com/ubiquity",
"url": "https://api.github.com/users/{{owner}}",
"html_url": "https://github.com/{{owner}}",
"type": "Organization",
"site_admin": false
},
"html_url": "https://github.com/ubiquity/work.ubq.fi",
"html_url": "https://github.com/{{owner}}/{{repo}}",
"description": "Work portal for Ubiquity DAO",
"fork": false,
"created_at": "2024-01-01T00:00:00Z",
Expand All @@ -86,26 +120,26 @@ X-GitHub-Delivery: mock-delivery-id
"default_branch": "development"
},
"organization": {
"login": "ubiquity",
"login": "{{owner}}",
"id": 87654321,
"node_id": "MDEyOk9yZ2FuaXphdGlvbjg3NjU0MzIx",
"url": "https://api.github.com/orgs/ubiquity",
"repos_url": "https://api.github.com/orgs/ubiquity/repos",
"events_url": "https://api.github.com/orgs/ubiquity/events",
"hooks_url": "https://api.github.com/orgs/ubiquity/hooks",
"issues_url": "https://api.github.com/orgs/ubiquity/issues",
"members_url": "https://api.github.com/orgs/ubiquity/members{/member}",
"public_members_url": "https://api.github.com/orgs/ubiquity/public_members{/member}",
"url": "https://api.github.com/orgs/{{owner}}",
"repos_url": "https://api.github.com/orgs/{{owner}}/repos",
"events_url": "https://api.github.com/orgs/{{owner}}/events",
"hooks_url": "https://api.github.com/orgs/{{owner}}/hooks",
"issues_url": "https://api.github.com/orgs/{{owner}}/issues",
"members_url": "https://api.github.com/orgs/{{owner}}/members{/member}",
"public_members_url": "https://api.github.com/orgs/{{owner}}/public_members{/member}",
"avatar_url": "https://avatars.githubusercontent.com/u/87654321?v=4",
"description": "Ubiquity Organization"
},
"sender": {
"login": "sshivaditya2019",
"id": 12345678,
"login": "ubiquity-ubiquibot",
"id": 163369652,
"node_id": "MDQ6VXNlcjEyMzQ1Njc4",
"avatar_url": "https://avatars.githubusercontent.com/u/12345678?v=4",
"url": "https://api.github.com/users/sshivaditya2019",
"html_url": "https://github.com/sshivaditya2019",
"url": "https://api.github.com/users/ubiquity-ubiquibot",
"html_url": "https://github.com/ubiquity-ubiquibot",
"type": "User",
"site_admin": false
}
Expand Down
6 changes: 3 additions & 3 deletions tests/main.test.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,17 @@
import { afterAll, afterEach, beforeAll, beforeEach, describe, expect } from "@jest/globals";
import { drop } from "@mswjs/data";
import { TransformDecodeError, Value } from "@sinclair/typebox/value";
import { createClient } from "@supabase/supabase-js";
import { cleanLogString, Logs } from "@ubiquity-os/ubiquity-os-logger";
import dotenv from "dotenv";
import { createAdapters } from "../src/adapters";
import { HttpStatusCode } from "../src/handlers/result-types";
import { userStartStop, userUnassigned } from "../src/handlers/user-start-stop";
import { AssignedIssueScope, Context, envSchema, Role, Sender, SupportedEvents } from "../src/types";
import { db } from "./__mocks__/db";
import issueTemplate from "./__mocks__/issue-template";
import { server } from "./__mocks__/node";
import usersGet from "./__mocks__/users-get.json";
import { HttpStatusCode } from "../src/handlers/result-types";
import { TransformDecodeError, Value } from "@sinclair/typebox/value";

dotenv.config();

Expand Down Expand Up @@ -700,7 +700,7 @@ export function createContext(
BOT_USER_ID: appId as unknown as number,
},
command: null,
};
} as unknown as Context;
}

export function getSupabase(withData = true) {
Expand Down
2 changes: 1 addition & 1 deletion wrangler.toml
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
name = "ubiquity-os-command-start-stop"
main = "src/worker.ts"
main = "src/index.ts"
compatibility_date = "2024-09-23"
compatibility_flags = [ "nodejs_compat" ]
[env.dev]
Expand Down

0 comments on commit 45a0726

Please sign in to comment.