Skip to content

Commit

Permalink
fix: changed logic to compute deadline
Browse files Browse the repository at this point in the history
Removed the yarn.lock file to reset dependencies.
  • Loading branch information
gentlementlegen committed Oct 21, 2024
1 parent d7577a7 commit 7fef8f5
Show file tree
Hide file tree
Showing 14 changed files with 516 additions and 135 deletions.
8 changes: 4 additions & 4 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -34,10 +34,10 @@
"@actions/github": "6.0.0",
"@octokit/graphql-schema": "^15.25.0",
"@octokit/rest": "20.1.1",
"@octokit/webhooks": "13.2.7",
"@sinclair/typebox": "0.32.31",
"@ubiquity-dao/ubiquibot-logger": "^1.3.1",
"@ubiquity-os/ubiquity-os-kernel": "^2.3.0",
"@octokit/webhooks": "^13.3.0",
"@sinclair/typebox": "^0.32.35",
"@ubiquity-os/ubiquity-os-kernel": "^2.4.0",
"@ubiquity-os/ubiquity-os-logger": "^1.3.2",
"dotenv": "16.4.5",
"luxon": "3.4.4",
"ms": "2.1.3",
Expand Down
45 changes: 27 additions & 18 deletions src/handlers/watch-user-activity.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { getWatchedRepos } from "../helpers/get-watched-repos";
import { updateTaskReminder } from "../helpers/task-update";
import { updateReminder } from "../helpers/task-update";
import { ListForOrg, ListIssueForRepo } from "../types/github-types";
import { ContextPlugin } from "../types/plugin-input";

Expand All @@ -12,11 +12,13 @@ export async function watchUserActivity(context: ContextPlugin) {
return { message: logger.info("No watched repos have been found, no work to do.").logMessage.raw };
}

for (const repo of repos) {
// uusd.ubq.fi
logger.debug(`> Watching user activity for repo: ${repo.name} (${repo.html_url})`);
await updateReminders(context, repo);
}
await Promise.all(
repos.map(async (repo) => {
// uusd.ubq.fi
logger.debug(`> Watching user activity for repo: ${repo.name} (${repo.html_url})`);
await updateReminders(context, repo);
})
);

return { message: "OK" };
}
Expand All @@ -34,16 +36,23 @@ async function updateReminders(context: ContextPlugin, repo: ListForOrg["data"][
state: "open",
})) as ListIssueForRepo[];

for (const issue of issues.filter((o) => o.html_url === "https://github.com/ubiquity/uusd.ubq.fi/issues/1")) {
// I think we can safely ignore the following
if (issue.draft || issue.pull_request || issue.locked || issue.state !== "open") {
continue;
}

if (issue.assignees?.length || issue.assignee) {
// uusd-ubq-fi
logger.debug(`Checking assigned issue: ${issue.html_url}`);
await updateTaskReminder(context, repo, issue);
}
}
await Promise.all(
issues
.filter((o) => o.html_url === "https://github.com/ubiquity/uusd.ubq.fi/issues/1")
.map(async (issue) => {
// I think we can safely ignore the following
if (issue.draft || issue.pull_request || issue.locked || issue.state !== "open") {
logger.debug("Skipping issue due to the issue state.", { issue });
return;
}

if (issue.assignees?.length || issue.assignee) {
// uusd-ubq-fi
logger.debug(`Checking assigned issue: ${issue.html_url}`);
await updateReminder(context, repo, issue);
} else {
logger.info("Skipping issue because no user is assigned.", { issue });
}
})
);
}
File renamed without changes.
8 changes: 5 additions & 3 deletions src/helpers/get-assignee-activity.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { DateTime } from "luxon";
import { collectLinkedPullRequests } from "../handlers/collect-linked-pulls";
import { collectLinkedPullRequests } from "./collect-linked-pulls";
import { GitHubTimelineEvents, ListIssueForRepo } from "../types/github-types";
import { ContextPlugin } from "../types/plugin-input";
import { parseIssueUrl } from "./github-url";
Expand Down Expand Up @@ -48,14 +48,15 @@ function filterEvents(issueEvents: GitHubTimelineEvents[], assigneeIds: number[]
}
actorId = userIdMap.get(actorLogin);
createdAt = event.created_at;
} else if (event.event === "committed") {
} else if ((event.event === "committed" || event.event === "commented") && "author" in event) {
const commitAuthor = "author" in event ? event.author : null;
const commitCommitter = "committer" in event ? event.committer : null;

if (commitAuthor || commitCommitter) {
assigneeEvents.push({
event: eventName,
created_at: createdAt,
created_at: event.author.date,
author: event.author.email,
});

continue;
Expand All @@ -66,6 +67,7 @@ function filterEvents(issueEvents: GitHubTimelineEvents[], assigneeIds: number[]
assigneeEvents.push({
event: eventName,
created_at: createdAt,
author: actorLogin,
});
}
}
Expand Down
15 changes: 8 additions & 7 deletions src/helpers/remind-and-remove.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import { Context } from "../types/context";
import { parseIssueUrl } from "./github-url";
import { FOLLOWUP_HEADER } from "../types/context";
import { ListIssueForRepo } from "../types/github-types";
import { ContextPlugin } from "../types/plugin-input";
import { parseIssueUrl } from "./github-url";
import { createStructuredMetadata } from "./structured-metadata";

export async function unassignUserFromIssue(context: Context, issue: ListIssueForRepo) {
export async function unassignUserFromIssue(context: ContextPlugin, issue: ListIssueForRepo) {
const { logger, config } = context;

if (config.disqualification <= 0) {
Expand All @@ -14,7 +15,7 @@ export async function unassignUserFromIssue(context: Context, issue: ListIssueFo
}
}

export async function remindAssigneesForIssue(context: Context, issue: ListIssueForRepo) {
export async function remindAssigneesForIssue(context: ContextPlugin, issue: ListIssueForRepo) {
const { logger, config } = context;
if (config.warning <= 0) {
logger.info("The reminder threshold is <= 0, won't send any reminder.");
Expand All @@ -24,7 +25,7 @@ export async function remindAssigneesForIssue(context: Context, issue: ListIssue
}
}

async function remindAssignees(context: Context, issue: ListIssueForRepo) {
async function remindAssignees(context: ContextPlugin, issue: ListIssueForRepo) {
const { octokit, logger } = context;
const { repo, owner, issue_number } = parseIssueUrl(issue.html_url);

Expand All @@ -41,7 +42,7 @@ async function remindAssignees(context: Context, issue: ListIssueForRepo) {
taskAssignees: issue.assignees.map((o) => o?.id),
});

const metadata = createStructuredMetadata("Followup", logMessage);
const metadata = createStructuredMetadata(FOLLOWUP_HEADER, logMessage);

await octokit.rest.issues.createComment({
owner,
Expand All @@ -52,7 +53,7 @@ async function remindAssignees(context: Context, issue: ListIssueForRepo) {
return true;
}

async function removeAllAssignees(context: Context, issue: ListIssueForRepo) {
async function removeAllAssignees(context: ContextPlugin, issue: ListIssueForRepo) {
const { octokit, logger } = context;
const { repo, owner, issue_number } = parseIssueUrl(issue.html_url);

Expand Down
24 changes: 22 additions & 2 deletions src/helpers/structured-metadata.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
import { LogReturn } from "@ubiquity-dao/ubiquibot-logger";
import { LogReturn } from "@ubiquity-os/ubiquity-os-logger";
import { ContextPlugin } from "../types/plugin-input";

const HEADER_NAME = "Ubiquity";

export function createStructuredMetadata(className: string, logReturn: LogReturn | null) {
let logMessage, metadata;
Expand All @@ -10,7 +13,7 @@ export function createStructuredMetadata(className: string, logReturn: LogReturn
const jsonPretty = JSON.stringify(metadata, null, 2);
const stackLine = new Error().stack?.split("\n")[2] ?? "";
const caller = stackLine.match(/at (\S+)/)?.[1] ?? "";
const ubiquityMetadataHeader = `<!-- Ubiquity - ${className} - ${caller} - ${metadata?.revision}`;
const ubiquityMetadataHeader = `<!-- ${HEADER_NAME} - ${className} - ${caller} - ${metadata?.revision}`;

let metadataSerialized: string;
const metadataSerializedVisible = ["```json", jsonPretty, "```"].join("\n");
Expand All @@ -26,3 +29,20 @@ export function createStructuredMetadata(className: string, logReturn: LogReturn

return metadataSerialized;
}

export async function getCommentsFromMetadata(context: ContextPlugin, issueNumber: number, repoOwner: string, repoName: string, className: string) {
const { octokit } = context;
const ubiquityMetadataHeaderPattern = new RegExp(`<!-- ${HEADER_NAME} - ${className} - \\S+ - [\\s\\S]*?-->`);
return await octokit.paginate(
octokit.rest.issues.listComments,
{
owner: repoOwner,
repo: repoName,
issue_number: issueNumber,
},
(response) =>
response.data.filter(
(comment) => comment.performed_via_github_app && comment.body && comment.user?.type === "Bot" && ubiquityMetadataHeaderPattern.test(comment.body)
)
);
}
74 changes: 73 additions & 1 deletion src/helpers/task-update.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
import { DateTime } from "luxon";
import { FOLLOWUP_HEADER } from "../types/context";
import { ListForOrg, ListIssueForRepo } from "../types/github-types";
import { ContextPlugin } from "../types/plugin-input";
import { ContextPlugin, TimelineEvent } from "../types/plugin-input";
import { getAssigneesActivityForIssue } from "./get-assignee-activity";
import { remindAssigneesForIssue, unassignUserFromIssue } from "./remind-and-remove";
import { getCommentsFromMetadata } from "./structured-metadata";
import { getDeadlineWithThreshold } from "./task-deadline";
import { getTaskAssignmentDetails } from "./task-metadata";

Expand Down Expand Up @@ -42,3 +46,71 @@ export async function updateTaskReminder(context: ContextPlugin, repo: ListForOr
logger.info(`Nothing to do for ${issue.html_url}, still within due-time.`);
}
}

export async function updateReminder(context: ContextPlugin, repo: ListForOrg["data"][0], issue: ListIssueForRepo) {
const {
octokit,
logger,
config: { eventWhitelist, warning, disqualification },
} = context;
const handledMetadata = await getTaskAssignmentDetails(context, repo, issue);
const now = DateTime.local();

if (handledMetadata) {
const assignmentEvents = await octokit.paginate(
octokit.rest.issues.listEvents,
{
owner: repo.owner.login,
repo: repo.name,
issue_number: issue.number,
},
(response) =>
response.data
.filter((o) => o.event === "assigned" && handledMetadata.taskAssignees.includes(o.actor.id))
.sort((a, b) => DateTime.fromISO(b.created_at).toMillis() - DateTime.fromISO(a.created_at).toMillis())
);

const assignedEvent = assignmentEvents.pop();
const activityEvent = (await getAssigneesActivityForIssue(context, issue, handledMetadata.taskAssignees))
.filter((o) => eventWhitelist.includes(o.event as TimelineEvent))
.pop();

if (!assignedEvent) {
throw new Error(`Failed to update activity for ${issue.html_url}, there is no assigned event.`);
}

let mostRecentActivityDate: DateTime;

if (assignedEvent?.created_at && activityEvent?.created_at) {
const assignedDate = DateTime.fromISO(assignedEvent.created_at);
const activityDate = DateTime.fromISO(activityEvent.created_at);
mostRecentActivityDate = assignedDate > activityDate ? assignedDate : activityDate;
} else {
mostRecentActivityDate = DateTime.fromISO(assignedEvent.created_at);
}

const lastReminderComment = (await getCommentsFromMetadata(context, issue.number, repo.owner.login, repo.name, FOLLOWUP_HEADER)).pop();
const disqualificationDifference = disqualification - warning;

logger.info(`Handling metadata and deadline for ${issue.html_url}`, {
now: now.toLocaleString(DateTime.DATETIME_MED),
lastReminderComment: lastReminderComment ? DateTime.fromISO(lastReminderComment.created_at).toLocaleString(DateTime.DATETIME_MED) : "none",
mostRecentActivityDate: mostRecentActivityDate.toLocaleString(DateTime.DATETIME_MED),
});

if (lastReminderComment) {
const lastReminderTime = DateTime.fromISO(lastReminderComment.created_at);
if (lastReminderTime.plus(disqualificationDifference) >= now) {
await unassignUserFromIssue(context, issue);
} else {
logger.info(`Reminder was sent for ${issue.html_url} already, not beyond disqualification deadline yet.`);
}
} else {
if (mostRecentActivityDate.plus({ milliseconds: warning }) >= now) {
await remindAssigneesForIssue(context, issue);
} else {
logger.info(`No reminder to send for ${issue.html_url}, still within due time.`);
}
}
}
}
2 changes: 1 addition & 1 deletion src/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { LOG_LEVEL } from "@ubiquity-dao/ubiquibot-logger";
import { LOG_LEVEL } from "@ubiquity-os/ubiquity-os-logger";
import { createActionsPlugin } from "@ubiquity-os/ubiquity-os-kernel";
import { run } from "./run";
import { Env, envSchema, PluginSettings, pluginSettingsSchema, SupportedEvents } from "./types/plugin-input";
Expand Down
8 changes: 5 additions & 3 deletions src/types/context.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { EmitterWebhookEvent as WebhookEvent, EmitterWebhookEventName as WebhookEventName } from "@octokit/webhooks";
import { Octokit } from "@octokit/rest";
import { SupportedEvents, PluginSettings } from "./plugin-input";
import { Logs } from "@ubiquity-dao/ubiquibot-logger";
import { EmitterWebhookEvent as WebhookEvent } from "@octokit/webhooks";
import { Logs } from "@ubiquity-os/ubiquity-os-logger";
import { PluginSettings, SupportedEvents } from "./plugin-input";

export interface Context<T extends SupportedEvents = SupportedEvents> {
eventName: T;
Expand All @@ -10,3 +10,5 @@ export interface Context<T extends SupportedEvents = SupportedEvents> {
config: PluginSettings;
logger: Logs;
}

export const FOLLOWUP_HEADER = "Followup";
4 changes: 2 additions & 2 deletions src/types/env.d.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import { LOG_LEVEL } from "@ubiquity-dao/ubiquibot-logger";
import { LogLevel } from "@ubiquity-os/ubiquity-os-logger";

export {};

declare global {
namespace NodeJS {
interface ProcessEnv {
LOG_LEVEL?: LOG_LEVEL;
LOG_LEVEL?: LogLevel;
}
}
}
7 changes: 4 additions & 3 deletions tests/__mocks__/http/action.http
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
### POST request to run the plugin
POST http://localhost:4000
POST http://localhost:3000
Content-Type: application/json

{
"stateId": "some-state-id",
"eventName": "some-event-name",
"eventName": "issues.closed",
"eventPayload": {
"repository": {
"owner": {
Expand All @@ -20,5 +20,6 @@ Content-Type: application/json
"disqualification": "7 days"
},
"authToken": "{{GITHUB_TOKEN}}",
"ref": "some-ref"
"ref": "development",
"signature": "1234"
}
47 changes: 20 additions & 27 deletions tests/__mocks__/http/express_plugin.ts
Original file line number Diff line number Diff line change
@@ -1,31 +1,24 @@
import express from "express";
import { validateAndDecodeSchemas } from "../../../src/helpers/validator";
import { run } from "../../../src/run";
import { PluginInputs } from "../../../src/types/plugin-input";
import { serve } from "@hono/node-server";
import { createPlugin } from "@ubiquity-os/ubiquity-os-kernel";
import { LOG_LEVEL } from "@ubiquity-os/ubiquity-os-logger";
import manifest from "../../../manifest.json";
import { run } from "../../../src/run";
import { Env, envSchema, PluginSettings, pluginSettingsSchema, SupportedEvents } from "../../../src/types/plugin-input";

const app = express();
const port = 4000;

app.use(express.json());

app.post("/", async (req, res) => {
try {
const inputs = req.body;
const { decodedSettings } = validateAndDecodeSchemas(inputs.settings, process.env);
inputs.settings = decodedSettings;
const result = await run(inputs);
res.json(result);
} catch (error) {
console.error("Error running plugin:", error);
res.status(500).send("Internal Server Error");
createPlugin<PluginSettings, Env, SupportedEvents>(
async (context) => {
const result = await run(context);
console.log(JSON.stringify(result));
return result;
},
//@ts-expect-error err
manifest,
{
envSchema: envSchema,
settingsSchema: pluginSettingsSchema,
logLevel: LOG_LEVEL.DEBUG,
}
});

app.get("/manifest.json", (req, res) => {
res.json(manifest);
});

app.listen(port, () => {
console.log(`Server is running at http://localhost:${port}`);
).then((server) => {
console.log("Server starting...");
return serve(server);
});
Loading

0 comments on commit 7fef8f5

Please sign in to comment.