-
Notifications
You must be signed in to change notification settings - Fork 13
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #15 from ubq-testing/chore/testing
Chore/testing
- Loading branch information
Showing
30 changed files
with
1,486 additions
and
384 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,6 @@ | ||
schema: | ||
- https://api.github.com/graphql: | ||
headers: | ||
Authorization: Bearer ${GITHUB_TOKEN} | ||
documents: src/handlers/collect-linked-pulls.ts | ||
projects: {} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -5,6 +5,7 @@ | |
"main": "src/worker.ts", | ||
"author": "Ubiquity DAO", | ||
"license": "MIT", | ||
"type": "module", | ||
"engines": { | ||
"node": ">=20.10.0" | ||
}, | ||
|
@@ -31,10 +32,11 @@ | |
"dependencies": { | ||
"@actions/core": "1.10.1", | ||
"@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.0", | ||
"@ubiquity-dao/ubiquibot-logger": "^1.3.1", | ||
"dotenv": "16.4.5", | ||
"luxon": "3.4.4", | ||
"ms": "2.1.3", | ||
|
@@ -46,6 +48,7 @@ | |
"@cspell/dict-node": "5.0.1", | ||
"@cspell/dict-software-terms": "3.4.0", | ||
"@cspell/dict-typescript": "3.1.5", | ||
"@jest/globals": "^29.7.0", | ||
"@mswjs/data": "0.16.1", | ||
"@types/jest": "29.5.12", | ||
"@types/luxon": "3.4.2", | ||
|
@@ -86,4 +89,4 @@ | |
] | ||
}, | ||
"packageManager": "[email protected]" | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,19 +1,66 @@ | ||
import { parseIssueUrl } from "../helpers/github-url"; | ||
import { Context } from "../types/context"; | ||
import { IssuesSearch } from "../types/github-types"; | ||
import { PullRequest, User, validate } from "@octokit/graphql-schema"; | ||
|
||
export type IssueParams = ReturnType<typeof parseIssueUrl>; | ||
type closedByPullRequestsReferences = { | ||
node: Pick<PullRequest, "url" | "title" | "number" | "state" | "body"> & Pick<User, "login" | "id">; | ||
} | ||
|
||
function additionalBooleanFilters(issueNumber: number) { | ||
return `linked:${issueNumber} in:body "closes #${issueNumber}" OR "closes #${issueNumber}" OR "fixes #${issueNumber}" OR "fix #${issueNumber}" OR "resolves #${issueNumber}"`; | ||
type IssueWithClosedByPRs = { | ||
repository: { | ||
issue: { | ||
closedByPullRequestsReferences: { | ||
edges: closedByPullRequestsReferences[]; | ||
}; | ||
}; | ||
}; | ||
} | ||
|
||
export async function collectLinkedPullRequests(context: Context, issue: IssueParams) { | ||
return await context.octokit.paginate( | ||
context.octokit.rest.search.issuesAndPullRequests, | ||
{ | ||
q: `repo:${issue.owner}/${issue.repo} is:pr is:open ${additionalBooleanFilters(issue.issue_number)}`, | ||
per_page: 100, | ||
const query = /* GraphQL */ ` | ||
query collectLinkedPullRequests($owner: String!, $repo: String!, $issue_number: Int!) { | ||
repository(owner: $owner, name: $repo) { | ||
issue(number: $issue_number) { | ||
closedByPullRequestsReferences(first: 100, includeClosedPrs: true) { | ||
edges { | ||
node { | ||
url | ||
title | ||
body | ||
state | ||
number | ||
author { | ||
login | ||
... on User { | ||
id: databaseId | ||
} | ||
} | ||
} | ||
} | ||
} | ||
} | ||
) as IssuesSearch[]; | ||
} | ||
}` | ||
|
||
const queryErrors = validate(query); | ||
|
||
/** | ||
* > 1 because the schema package is slightly out of date and does not include the | ||
* `closedByPullRequestsReferences` object in the schema as it is a recent addition to the GitHub API. | ||
*/ | ||
if (queryErrors.length > 1) { | ||
throw new Error(`Invalid query: ${queryErrors.join(", ")}`); | ||
} | ||
|
||
export async function collectLinkedPullRequests(context: Context, issue: { | ||
owner: string; | ||
repo: string; | ||
issue_number: number; | ||
}) { | ||
const { owner, repo, issue_number } = issue; | ||
const result = await context.octokit.graphql<IssueWithClosedByPRs>(query, { | ||
owner, | ||
repo, | ||
issue_number, | ||
}); | ||
|
||
return result.repository.issue.closedByPullRequestsReferences.edges.map((edge) => edge.node); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,47 @@ | ||
import { getWatchedRepos } from "../helpers/get-watched-repos"; | ||
import { updateTaskReminder } from "../helpers/task-update"; | ||
import { Context } from "../types/context"; | ||
import { ListForOrg, ListIssueForRepo } from "../types/github-types"; | ||
|
||
export async function watchUserActivity(context: Context) { | ||
const { logger } = context; | ||
|
||
const repos = await getWatchedRepos(context); | ||
|
||
if (!repos?.length) { | ||
logger.info("No watched repos have been found, no work to do."); | ||
return false; | ||
} | ||
|
||
for (const repo of repos) { | ||
await updateReminders(context, repo); | ||
} | ||
|
||
return true; | ||
} | ||
|
||
async function updateReminders(context: Context, repo: ListForOrg["data"][0]) { | ||
const { logger, octokit, payload } = context; | ||
const owner = payload.repository.owner?.login; | ||
if (!owner) { | ||
throw new Error("No owner found in the payload"); | ||
} | ||
const issues = (await octokit.paginate(octokit.rest.issues.listForRepo, { | ||
owner, | ||
repo: repo.name, | ||
per_page: 100, | ||
state: "open", | ||
})) as ListIssueForRepo[]; | ||
|
||
for (const issue of issues) { | ||
// 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) { | ||
logger.debug(`Checking assigned issue: ${issue.html_url}`); | ||
await updateTaskReminder(context, repo, issue); | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,32 @@ | ||
import { DateTime } from "luxon"; | ||
import { collectLinkedPullRequests } from "../handlers/collect-linked-pulls"; | ||
import { Context } from "../types/context"; | ||
import { parseIssueUrl } from "./github-url"; | ||
import { GitHubListEvents, ListIssueForRepo } from "../types/github-types"; | ||
|
||
/** | ||
* Retrieves all the activity for users that are assigned to the issue. Also takes into account linked pull requests. | ||
*/ | ||
export async function getAssigneesActivityForIssue(context: Context, issue: ListIssueForRepo, assigneeIds: number[]) { | ||
const gitHubUrl = parseIssueUrl(issue.html_url); | ||
const issueEvents: GitHubListEvents[] = await context.octokit.paginate(context.octokit.rest.issues.listEvents, { | ||
owner: gitHubUrl.owner, | ||
repo: gitHubUrl.repo, | ||
issue_number: gitHubUrl.issue_number, | ||
per_page: 100, | ||
}); | ||
const linkedPullRequests = await collectLinkedPullRequests(context, gitHubUrl); | ||
for (const linkedPullRequest of linkedPullRequests) { | ||
const { owner, repo, issue_number } = parseIssueUrl(linkedPullRequest.url || ""); | ||
const events = await context.octokit.paginate(context.octokit.rest.issues.listEvents, { | ||
owner, | ||
repo, | ||
issue_number, | ||
per_page: 100, | ||
}); | ||
issueEvents.push(...events); | ||
} | ||
|
||
return issueEvents.filter((o) => o.actor && o.actor.id && assigneeIds.includes(o.actor.id)) | ||
.sort((a, b) => DateTime.fromISO(b.created_at).toMillis() - DateTime.fromISO(a.created_at).toMillis()); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,71 @@ | ||
import { Context } from "../types/context"; | ||
import { parseIssueUrl } from "./github-url"; | ||
import { ListIssueForRepo } from "../types/github-types"; | ||
import { createStructuredMetadata } from "./structured-metadata"; | ||
|
||
export async function unassignUserFromIssue(context: Context, issue: ListIssueForRepo) { | ||
const { logger, config } = context; | ||
|
||
if (config.disqualification <= 0) { | ||
logger.info("The unassign threshold is <= 0, won't unassign users."); | ||
} else { | ||
logger.info(`Passed the deadline on ${issue.html_url} and no activity is detected, removing assignees.`); | ||
await removeAllAssignees(context, issue); | ||
} | ||
} | ||
|
||
export async function remindAssigneesForIssue(context: Context, issue: ListIssueForRepo) { | ||
const { logger, config } = context; | ||
if (config.warning <= 0) { | ||
logger.info("The reminder threshold is <= 0, won't send any reminder."); | ||
} else { | ||
logger.info(`Passed the reminder threshold on ${issue.html_url}, sending a reminder.`); | ||
await remindAssignees(context, issue); | ||
} | ||
} | ||
|
||
async function remindAssignees(context: Context, issue: ListIssueForRepo) { | ||
const { octokit, logger } = context; | ||
const { repo, owner, issue_number } = parseIssueUrl(issue.html_url); | ||
|
||
if (!issue?.assignees?.length) { | ||
logger.error(`Missing Assignees from ${issue.html_url}`); | ||
return false; | ||
} | ||
const logins = issue.assignees | ||
.map((o) => o?.login) | ||
.filter((o) => !!o) | ||
.join(", @"); | ||
|
||
const logMessage = logger.info(`@${logins}, this task has been idle for a while. Please provide an update.\n\n`, { | ||
taskAssignees: issue.assignees.map((o) => o?.id), | ||
}); | ||
|
||
const metadata = createStructuredMetadata("Followup", logMessage); | ||
|
||
await octokit.rest.issues.createComment({ | ||
owner, | ||
repo, | ||
issue_number, | ||
body: [logMessage.logMessage.raw, metadata].join("\n"), | ||
}); | ||
return true; | ||
} | ||
|
||
async function removeAllAssignees(context: Context, issue: ListIssueForRepo) { | ||
const { octokit, logger } = context; | ||
const { repo, owner, issue_number } = parseIssueUrl(issue.html_url); | ||
|
||
if (!issue?.assignees?.length) { | ||
logger.error(`Missing Assignees from ${issue.html_url}`); | ||
return false; | ||
} | ||
const logins = issue.assignees.map((o) => o?.login).filter((o) => !!o) as string[]; | ||
await octokit.rest.issues.removeAssignees({ | ||
owner, | ||
repo, | ||
issue_number, | ||
assignees: logins, | ||
}); | ||
return true; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,28 @@ | ||
import { LogReturn } from "@ubiquity-dao/ubiquibot-logger"; | ||
|
||
export function createStructuredMetadata(className: string, logReturn: LogReturn | null) { | ||
let logMessage, metadata; | ||
if (logReturn) { | ||
logMessage = logReturn.logMessage; | ||
metadata = logReturn.metadata; | ||
} | ||
|
||
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}`; | ||
|
||
let metadataSerialized: string; | ||
const metadataSerializedVisible = ["```json", jsonPretty, "```"].join("\n"); | ||
const metadataSerializedHidden = [ubiquityMetadataHeader, jsonPretty, "-->"].join("\n"); | ||
|
||
if (logMessage?.type === "fatal") { | ||
// if the log message is fatal, then we want to show the metadata | ||
metadataSerialized = [metadataSerializedVisible, metadataSerializedHidden].join("\n"); | ||
} else { | ||
// otherwise we want to hide it | ||
metadataSerialized = metadataSerializedHidden; | ||
} | ||
|
||
return metadataSerialized; | ||
} |
Oops, something went wrong.