Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: task limit #91

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
b3405d7
refactor: reorder imports and optimize async operations
gentlementlegen Nov 22, 2024
5c2ec98
chore: updated generated configuration
ubiquity-os[bot] Nov 22, 2024
e26a4f6
chore: changed conditions for testing
gentlementlegen Nov 24, 2024
410a76c
chore: changed entry file
gentlementlegen Nov 24, 2024
fa5a60e
chore: changed version
gentlementlegen Nov 24, 2024
46ce5a6
chore: updated manifest.json and dist build
github-actions[bot] Nov 24, 2024
d725423
chore: changed http test and logs
gentlementlegen Nov 25, 2024
f84b7e5
chore: updated manifest.json and dist build
github-actions[bot] Nov 25, 2024
bd0aab6
chore: changed user id for test
gentlementlegen Nov 25, 2024
e5259f1
fix: logic to retrieve pull-requests pending for a user updated
gentlementlegen Nov 25, 2024
cfb54db
chore: updated manifest.json and dist build
github-actions[bot] Nov 25, 2024
8fd3029
chore: split function for readability
gentlementlegen Nov 25, 2024
e3ed8f6
chore: updated manifest.json and dist build
github-actions[bot] Nov 25, 2024
44fd708
Merge branch 'development' into fix/task-limit
gentlementlegen Nov 25, 2024
ac15c98
chore: updated manifest.json and dist build
github-actions[bot] Nov 25, 2024
d74a9f2
chore: update hono to version 4.6.12
gentlementlegen Nov 25, 2024
37e2e55
Merge branch 'development' into fix/task-limit
gentlementlegen Nov 26, 2024
f81051c
chore: updated lock file
gentlementlegen Nov 26, 2024
21d9f55
chore: fixed pull request length check
gentlementlegen Nov 26, 2024
c3540aa
chore: changed logic for opened PRs
gentlementlegen Nov 26, 2024
adca88b
chore: simplified logic for opened PRs
gentlementlegen Nov 26, 2024
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
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?" }),
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This description isn't very clear. Given the specification, I guess that this should describe the max time we will wait for a reviewer to review while holding up the task for the assignee

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe something like "Duration delay for a pull-request to count against the user's maximum assigned issues quota" ?

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