Skip to content

Commit

Permalink
Merge pull request #73 from gentlementlegen/feat/cron-job
Browse files Browse the repository at this point in the history
  • Loading branch information
0x4007 authored Jan 25, 2025
2 parents ce3d151 + 7d21fb4 commit a243440
Show file tree
Hide file tree
Showing 19 changed files with 321 additions and 25 deletions.
23 changes: 23 additions & 0 deletions .github/workflows/compute.yml
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,10 @@ on:
command:
description: "Command"

env:
APP_ID: ${{ secrets.APP_ID }}
APP_PRIVATE_KEY: ${{ secrets.APP_PRIVATE_KEY }}

jobs:
compute:
environment: ${{ github.ref == 'refs/heads/main' && 'main' || 'development' }}
Expand All @@ -42,3 +46,22 @@ jobs:

- name: Watch Activity
uses: ./

- name: Get GitHub App token
if: env.APP_ID != '' && env.APP_PRIVATE_KEY != ''
uses: actions/create-github-app-token@v1
id: app-token
with:
app-id: ${{ env.APP_ID }}
private-key: ${{ env.APP_PRIVATE_KEY }}

- name: Commit updated DB
uses: swinton/[email protected]
env:
GH_TOKEN: ${{ steps.app-token.outputs.token || github.token }}
with:
files: |
db.json
commit-message: "chore: [skip ci] update db.json"
# We save to the default branch since CRON cannot access any other branch
ref: ${{ github.event.repository.default_branch }}
43 changes: 43 additions & 0 deletions .github/workflows/cron.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
name: Cron

on:
workflow_dispatch:
schedule:
- cron: "1 0 * * *"

env:
APP_ID: ${{ secrets.APP_ID }}
APP_PRIVATE_KEY: ${{ secrets.APP_PRIVATE_KEY }}

jobs:
run-cron:
environment: ${{ github.ref == 'refs/heads/main' && 'main' || 'development' }}
name: Run Cron
runs-on: ubuntu-latest
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

steps:
- uses: oven-sh/setup-bun@v2

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

- name: Install packages
run: bun install --frozen-lockfile

- name: Get GitHub App token
if: env.APP_ID != '' && env.APP_PRIVATE_KEY != ''
uses: actions/create-github-app-token@v1
id: app-token
with:
app-id: ${{ env.APP_ID }}
private-key: ${{ env.APP_PRIVATE_KEY }}

- name: Run CRON job
run: |
bun src/cron/index.ts
env:
GITHUB_TOKEN: ${{ steps.app-token.outputs.token || github.token }}
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -15,3 +15,4 @@ cypress/screenshots
/tests/http/http-client.private.env.json
.wrangler
test-dashboard.md
db.json
Binary file removed bun.lockb
Binary file not shown.
6 changes: 3 additions & 3 deletions dist/index.js

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion manifest.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "Daemon Disqualifier",
"description": "Watches user activity on issues, sends reminders on disqualification threshold, and unassign inactive users.",
"ubiquity:listeners": ["issues.assigned"],
"ubiquity:listeners": ["issues.assigned", "issue_comment.edited"],
"skipBotEvents": false,
"configuration": {
"default": {},
Expand Down
7 changes: 4 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -33,9 +33,10 @@
"@octokit/graphql-schema": "^15.25.0",
"@octokit/rest": "^21.0.2",
"@sinclair/typebox": "0.34.3",
"@ubiquity-os/plugin-sdk": "^1.1.1",
"@ubiquity-os/plugin-sdk": "^2.0.3",
"@ubiquity-os/ubiquity-os-logger": "^1.3.2",
"dotenv": "16.4.5",
"lowdb": "^7.0.1",
"luxon": "3.4.4",
"ms": "2.1.3"
},
Expand All @@ -48,7 +49,7 @@
"@eslint/js": "9.15.0",
"@jest/globals": "^29.7.0",
"@mswjs/data": "0.16.1",
"@types/jest": "29.5.12",
"@types/jest": "29.5.14",
"@types/luxon": "3.4.2",
"@types/ms": "0.7.34",
"@types/node": "^22.7.7",
Expand All @@ -70,7 +71,7 @@
"msw": "2.3.1",
"prettier": "3.3.0",
"supabase": "1.176.9",
"ts-jest": "29.1.4",
"ts-jest": "^29.2.5",
"ts-node": "^10.9.2",
"typescript": "5.5.4",
"typescript-eslint": "^8.16.0"
Expand Down
15 changes: 15 additions & 0 deletions src/cron/database-handler.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { JSONFilePreset } from "lowdb/node";
import path from "node:path";

export const DB_FILE_NAME = "db.json";

export interface DbComment {
commentId: number;
issueNumber: number;
}

export interface DbIssues {
[repo: string]: DbComment[];
}

export default await JSONFilePreset<DbIssues>(path.join(process.env.GITHUB_WORKSPACE || import.meta.dirname || "", DB_FILE_NAME), {});
46 changes: 46 additions & 0 deletions src/cron/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import { Octokit } from "@octokit/rest";
import { Logs } from "@ubiquity-os/ubiquity-os-logger";
import db from "./database-handler";

async function main() {
const logger = new Logs(process.env.LOG_LEVEL ?? "info");
const octokit = new Octokit({
auth: process.env.GITHUB_TOKEN,
});
const fileContent = db.data;
for (const [key, value] of Object.entries(fileContent)) {
try {
logger.info(`Triggering update`, {
key,
value,
});
const [owner, repo] = key.split("/");
const comment = value.pop();
if (!comment) {
logger.error(`No comment was found for repository ${key}`);
continue;
}
const {
data: { body = "" },
} = await octokit.rest.issues.getComment({
owner,
repo,
comment_id: comment.commentId,
issue_number: comment.issueNumber,
});
const newBody = body + `\n<!-- daemon-disqualifier update ${Date().toLocaleString()} -->`;
logger.debug(`Update comment ${comment.commentId}`, { newBody });
await octokit.rest.issues.updateComment({
owner,
repo,
comment_id: comment.commentId,
issue_number: comment.issueNumber,
body: newBody,
});
} catch (e) {
logger.error("Failed to update the comment", { key, value, e });
}
}
}

main().catch(console.error);
36 changes: 36 additions & 0 deletions src/cron/workflow.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { ContextPlugin } from "../types/plugin-input";
import db from "./database-handler";

export async function updateCronState(context: ContextPlugin) {
await db.update((data) => {
for (const key of Object.keys(data)) {
if (!data[key].length) {
delete data[key];
}
}
return data;
});

if (!process.env.GITHUB_REPOSITORY) {
context.logger.error("Can't update the Action Workflow state as GITHUB_REPOSITORY is missing from the env.");
return;
}

const [owner, repo] = process.env.GITHUB_REPOSITORY.split("/");

if (Object.keys(db.data).length) {
context.logger.verbose("Enabling cron.yml workflow.");
await context.octokit.rest.actions.enableWorkflow({
owner,
repo,
workflow_id: "cron.yml",
});
} else {
context.logger.verbose("Disabling cron.yml workflow.");
await context.octokit.rest.actions.disableWorkflow({
owner,
repo,
workflow_id: "cron.yml",
});
}
}
36 changes: 26 additions & 10 deletions src/handlers/watch-user-activity.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
import { RestEndpointMethodTypes } from "@octokit/rest";
import { postComment } from "@ubiquity-os/plugin-sdk";
import { formatMillisecondsToHumanReadable } from "./time-format";
import db from "../cron/database-handler";
import { updateCronState } from "../cron/workflow";
import { getWatchedRepos } from "../helpers/get-watched-repos";
import { removeEntryFromDatabase } from "../helpers/remind-and-remove";
import { parsePriceLabel, parsePriorityLabel } from "../helpers/task-metadata";
import { updateTaskReminder } from "../helpers/task-update";
import { ListForOrg } from "../types/github-types";
import { ContextPlugin } from "../types/plugin-input";
import { formatMillisecondsToHumanReadable } from "./time-format";

type IssueType = RestEndpointMethodTypes["issues"]["listForRepo"]["response"]["data"]["0"];

Expand Down Expand Up @@ -35,15 +37,28 @@ export async function watchUserActivity(context: ContextPlugin) {
);
const log = logger.error(message.map((o) => `> ${o}`).join("\n"));
log.logMessage.diff = log.logMessage.raw;
await postComment(context, log);
const commentData = await postComment(context, log);
if (commentData) {
await db.update((data) => {
const dbKey = `${context.payload.repository.owner?.login}/${context.payload.repository.name}`;
if (!data[dbKey]) {
data[dbKey] = [];
}
if (!data[dbKey].some((o) => o.issueNumber === commentData.issueNumber)) {
data[dbKey].push({
commentId: commentData.id,
issueNumber: commentData.issueNumber,
});
}
return data;
});
}
}

await Promise.all(
repos.map(async (repo) => {
logger.debug(`> Watching user activity for repo: ${repo.name} (${repo.html_url})`);
await updateReminders(context, repo);
})
);
const repo = context.payload.repository;
logger.debug(`> Watching user activity for repo: ${repo.name} (${repo.html_url})`);
await updateReminders(context, repo);
await updateCronState(context);

return { message: "OK" };
}
Expand All @@ -60,7 +75,7 @@ function shouldIgnoreIssue(issue: IssueType) {
return issue.draft || !!issue.pull_request || issue.locked || issue.state !== "open" || parsePriceLabel(issue.labels) === null;
}

async function updateReminders(context: ContextPlugin, repo: ListForOrg["data"][0]) {
async function updateReminders(context: ContextPlugin, repo: ContextPlugin["payload"]["repository"]) {
const { logger, octokit, payload } = context;
const owner = payload.repository.owner?.login;
if (!owner) {
Expand Down Expand Up @@ -91,6 +106,7 @@ async function updateReminders(context: ContextPlugin, repo: ListForOrg["data"][
await updateTaskReminder(context, repo, issue);
} else {
logger.info(`Skipping issue ${issue.html_url} because no user is assigned.`);
await removeEntryFromDatabase(issue);
}
})
);
Expand Down
13 changes: 13 additions & 0 deletions src/helpers/remind-and-remove.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import db from "../cron/database-handler";
import { FOLLOWUP_HEADER, UNASSIGN_HEADER } from "../types/constants";
import { ListIssueForRepo } from "../types/github-types";
import { ContextPlugin } from "../types/plugin-input";
Expand Down Expand Up @@ -97,6 +98,17 @@ async function remindAssignees(context: ContextPlugin, issue: ListIssueForRepo)
return true;
}

export async function removeEntryFromDatabase(issue: ListIssueForRepo) {
const { owner, repo, issue_number } = parseIssueUrl(issue.html_url);
await db.update((data) => {
const key = `${owner}/${repo}`;
if (data[key]) {
data[key] = data[key].filter((o) => o.issueNumber !== issue_number);
}
return data;
});
}

async function removeAllAssignees(context: ContextPlugin, issue: ListIssueForRepo) {
const { octokit, logger } = context;
const { repo, owner, issue_number } = parseIssueUrl(issue.html_url);
Expand Down Expand Up @@ -126,6 +138,7 @@ async function removeAllAssignees(context: ContextPlugin, issue: ListIssueForRep
issue_number,
assignees: logins,
});
await removeEntryFromDatabase(issue);
return true;
}

Expand Down
12 changes: 8 additions & 4 deletions src/helpers/task-metadata.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import { RestEndpointMethodTypes } from "@octokit/rest";
import { DateTime } from "luxon";
import ms from "ms";
import { ListForOrg, ListIssueForRepo } from "../types/github-types";
import { ListIssueForRepo } from "../types/github-types";
import { ContextPlugin } from "../types/plugin-input";
import { RestEndpointMethodTypes } from "@octokit/rest";

type IssueLabel = Partial<Omit<RestEndpointMethodTypes["issues"]["listLabelsForRepo"]["response"]["data"][0], "color">> & {
color?: string | null;
Expand All @@ -17,10 +17,14 @@ type IssueLabel = Partial<Omit<RestEndpointMethodTypes["issues"]["listLabelsForR
*/
export async function getTaskAssignmentDetails(
context: ContextPlugin,
repo: ListForOrg["data"][0],
repo: ContextPlugin["payload"]["repository"],
issue: ListIssueForRepo
): Promise<{ startPlusLabelDuration: string; taskAssignees: number[] } | false> {
const { logger, octokit } = context;
const { logger, octokit, payload } = context;

if (!repo.owner) {
throw logger.error("No owner was found in the payload", { payload });
}

const assignmentEvents = await octokit.paginate(octokit.rest.issues.listEvents, {
owner: repo.owner.login,
Expand Down
9 changes: 7 additions & 2 deletions src/helpers/task-update.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { RestEndpointMethodTypes } from "@octokit/rest";
import { DateTime } from "luxon";
import { FOLLOWUP_HEADER } from "../types/constants";
import { ListForOrg, ListIssueForRepo } from "../types/github-types";
import { ListIssueForRepo } from "../types/github-types";
import { ContextPlugin, TimelineEvent } from "../types/plugin-input";
import { collectLinkedPullRequests } from "./collect-linked-pulls";
import { getAssigneesActivityForIssue } from "./get-assignee-activity";
Expand All @@ -14,17 +14,22 @@ function getMostRecentActivityDate(assignedEventDate: DateTime, activityEventDat
return activityEventDate && activityEventDate > assignedEventDate ? activityEventDate : assignedEventDate;
}

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

if (!handledMetadata) return;

if (!repo.owner) {
throw logger.error("No owner was found in the payload", { payload });
}

const assignmentEvents = await octokit.paginate(octokit.rest.issues.listEvents, {
owner: repo.owner.login,
repo: repo.name,
Expand Down
Loading

0 comments on commit a243440

Please sign in to comment.