Skip to content

Commit

Permalink
Merge branch 'development' into fix/deadline-message
Browse files Browse the repository at this point in the history
# Conflicts:
#	dist/index.js
  • Loading branch information
gentlementlegen committed Jan 2, 2025
2 parents 96e4779 + 9ccdb93 commit 031823e
Show file tree
Hide file tree
Showing 9 changed files with 121 additions and 111 deletions.
29 changes: 20 additions & 9 deletions .cspell.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,24 +15,35 @@
"useGitignore": true,
"language": "en",
"words": [
"knip",
"mischeck",
"dataurl",
"devpool",
"knip",
"mischeck",
"mswjs",
"outdir",
"servedir",
"Supabase",
"signoff",
"sonarjs",
"supabase",
"Supabase",
"SUPABASE",
"timespans",
"typebox",
"ubiquity-os",
"signoff",
"sonarjs",
"mswjs",
"unassignment",
"unassignments"
],
"dictionaries": ["typescript", "node", "software-terms"],
"import": ["@cspell/dict-typescript/cspell-ext.json", "@cspell/dict-node/cspell-ext.json", "@cspell/dict-software-terms"],
"ignoreRegExpList": ["[0-9a-fA-F]{6}"]
"dictionaries": [
"typescript",
"node",
"software-terms"
],
"import": [
"@cspell/dict-typescript/cspell-ext.json",
"@cspell/dict-node/cspell-ext.json",
"@cspell/dict-software-terms"
],
"ignoreRegExpList": [
"[0-9a-fA-F]{6}"
]
}
Binary file modified bun.lockb
Binary file not shown.
2 changes: 1 addition & 1 deletion dist/index.js

Large diffs are not rendered by default.

96 changes: 7 additions & 89 deletions src/handlers/shared/check-assignments.ts
Original file line number Diff line number Diff line change
@@ -1,98 +1,16 @@
import { Context } from "../../types";
import { getOwnerRepoFromHtmlUrl } from "../../utils/issue";

async function getUserStopComments(context: Context, username: string): Promise<number> {
if (!("issue" in context.payload)) {
throw new Error("The context does not contain an issue.");
}
const { payload, octokit, logger } = context;
const { number, html_url } = payload.issue;
const { owner, repo } = getOwnerRepoFromHtmlUrl(html_url);

try {
const comments = await octokit.paginate(octokit.rest.issues.listComments, {
owner,
repo,
issue_number: number,
});

return comments.filter((comment) => comment.body?.includes("/stop") && comment.user?.login.toLowerCase() === username.toLowerCase()).length;
} catch (error) {
throw new Error(logger.error("Error while getting user stop comments", { error: error as Error }).logMessage.raw);
}
}
import { getAssignmentPeriods } from "./user-assigned-timespans";

export async function hasUserBeenUnassigned(context: Context, username: string): Promise<boolean> {
const {
env: { BOT_USER_ID },
} = context;
const events = await getAssignmentEvents(context);
const userAssignments = events.filter((event) => event.assignee === username);

if (userAssignments.length === 0) {
return false;
}

const unassignedEvents = userAssignments.filter((event) => event.event === "unassigned");
// all bot unassignments (/stop, disqualification, etc)
// TODO: task-xp-guard: will also prevent future assignments so we need to add a comment tracker we can use here
const botUnassigned = unassignedEvents.filter((event) => event.actorId === BOT_USER_ID);
// UI assignment
const adminUnassigned = unassignedEvents.filter((event) => event.actor !== username && event.actorId !== BOT_USER_ID);
// UI assignment
const userUnassigned = unassignedEvents.filter((event) => event.actor === username);
const userStopComments = await getUserStopComments(context, username);
/**
* Basically the bot will be the actor in most cases but if we
* remove the /stop usage which does not trigger future disqualification
* then any other bot unassignment will be considered valid
*/

const botMinusUserStopCommands = Math.max(0, botUnassigned.length - userStopComments);
const userUiMinusUserStopCommands = Math.max(0, userUnassigned.length - userStopComments);

return botMinusUserStopCommands > 0 || userUiMinusUserStopCommands > 0 || adminUnassigned.length > 0;
}

async function getAssignmentEvents(context: Context) {
if (!("issue" in context.payload)) {
throw new Error("The context does not contain an issue.");
if ("issue" in context.payload) {
const { number, html_url } = context.payload.issue;
const { owner, repo } = getOwnerRepoFromHtmlUrl(html_url);
const assignmentPeriods = await getAssignmentPeriods(context.octokit, { owner, repo, issue_number: number });
return assignmentPeriods[username]?.some((period) => period.reason === "bot" || period.reason === "admin");
}
const { repository, issue } = context.payload;
try {
const data = await context.octokit.paginate(context.octokit.rest.issues.listEventsForTimeline, {
owner: repository.owner.login,
repo: repository.name,
issue_number: issue.number,
});

const events = data
.filter((event) => event.event === "assigned" || event.event === "unassigned")
.map((event) => {
let actor, assignee, createdAt, actorId;

if ((event.event === "unassigned" || event.event === "assigned") && "actor" in event && event.actor && "assignee" in event && event.assignee) {
actor = event.actor.login;
assignee = event.assignee.login;
createdAt = event.created_at;
actorId = event.actor.id;
}

return {
event: event.event,
actor,
actorId,
assignee,
createdAt,
};
});

return events
.filter((event) => event !== undefined)
.sort((a, b) => {
return new Date(a.createdAt || "").getTime() - new Date(b.createdAt || "").getTime();
});
} catch (error) {
throw new Error(context.logger.error("Error while getting assignment events", { error: error as Error }).logMessage.raw);
}
return false;
}
17 changes: 7 additions & 10 deletions src/handlers/shared/start.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,15 +33,12 @@ async function checkRequirements(context: Context, issue: Context<"issue_comment
);
} else if (!currentLabelConfiguration.roles.includes(userAssociation.role.toLowerCase() as (typeof currentLabelConfiguration.roles)[number])) {
// If we found the label in the allowed list, but the user role does not match the allowed roles, then the user cannot start this task.
throw logger.error(
`You do not have the adequate role to start this task (your role is: ${userAssociation.role}). Allowed roles are: ${currentLabelConfiguration.roles.join(", ")}.`,
{
currentLabelConfiguration,
issueLabels,
issue: issue.html_url,
userAssociation,
}
);
throw logger.error("You must be a core team member to start this task", {
currentLabelConfiguration,
issueLabels,
issue: issue.html_url,
userAssociation,
});
}
}
}
Expand Down Expand Up @@ -139,7 +136,7 @@ export async function start(
await addCommentToIssue(
context,
`
> [!WARNING]
> ${error}
Expand Down
83 changes: 83 additions & 0 deletions src/handlers/shared/user-assigned-timespans.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
import { Context } from "../../types";

interface IssueParams {
owner: string;
repo: string;
issue_number: number;
}

interface UserAssignments {
[username: string]: AssignmentPeriod[];
}

interface AssignmentPeriod {
assignedAt: string;
unassignedAt: string | null;
reason: "user" | "bot" | "admin";
}

/*
* Returns all the assignment periods by user, with the reason of the un-assignments. If it is instigated by the user,
* (e.g. GitHub UI or using /stop), the reason will be "user", otherwise "bot", or "admin" if the admin is the
* instigator.
*/
export async function getAssignmentPeriods(octokit: Context["octokit"], issueParams: IssueParams) {
const [events, comments] = await Promise.all([
octokit.paginate(octokit.rest.issues.listEvents, {
...issueParams,
per_page: 100,
}),
octokit.paginate(octokit.rest.issues.listComments, {
...issueParams,
per_page: 100,
}),
]);
const stopComments = comments
.filter((comment) => comment.body?.trim() === "/stop")
.sort((a, b) => new Date(a.created_at).getTime() - new Date(b.created_at).getTime());
const userAssignments: UserAssignments = {};
const sortedEvents = events
.filter((event) => ["assigned", "unassigned"].includes(event.event))
.sort((a, b) => new Date(a.created_at).getTime() - new Date(b.created_at).getTime());

sortedEvents.forEach((event) => {
const username = "assignee" in event ? event.assignee?.login : null;
if (!username) return;

if (!userAssignments[username]) {
userAssignments[username] = [];
}

const lastPeriod = userAssignments[username][userAssignments[username].length - 1];

if (event.event === "assigned") {
const newPeriod: AssignmentPeriod = {
assignedAt: event.created_at,
unassignedAt: null,
reason: "bot",
};
userAssignments[username].push(newPeriod);
} else if (event.event === "unassigned" && lastPeriod && lastPeriod.unassignedAt === null) {
lastPeriod.unassignedAt = event.created_at;
const periodStart = new Date(lastPeriod.assignedAt).getTime();
const periodEnd = new Date(event.created_at).getTime();

if ("assigner" in event && event.assigner.type !== "Bot" && event.assigner.login !== username) {
lastPeriod.reason = "admin";
} else {
const hasStopCommand =
stopComments.some((comment) => {
const commentTime = new Date(comment.created_at).getTime();
return commentTime >= periodStart && commentTime <= periodEnd;
}) ||
("assigner" in event && event.assigner.type !== "Bot");

if (hasStopCommand) {
lastPeriod.reason = "user";
}
}
}
});

return userAssignments;
}
2 changes: 1 addition & 1 deletion src/utils/issue.ts
Original file line number Diff line number Diff line change
Expand Up @@ -301,7 +301,7 @@ async function shouldSkipPullRequest(
}

/**
* Returns all the pull-requests pending to be approved, counting as a malus against the PR user's quota.
* Returns all the pull-requests pending approval, which count negatively against the PR author's quota.
*/
export async function getPendingOpenedPullRequests(context: Context, username: string) {
const { reviewDelayTolerance } = context.config;
Expand Down
1 change: 1 addition & 0 deletions tests/__mocks__/handlers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ export const handlers = [
),
// list events for an issue timeline
http.get("https://api.github.com/repos/:owner/:repo/issues/:issue_number/timeline", () => HttpResponse.json(db.event.getAll())),
http.get("https://api.github.com/repos/:owner/:repo/issues/:issue_number/events", () => HttpResponse.json(db.event.getAll())),
// update a pull request
http.patch("https://api.github.com/repos/:owner/:repo/pulls/:pull_number", ({ params: { owner, repo, pull_number: pullNumber } }) =>
HttpResponse.json({ owner, repo, pullNumber })
Expand Down
2 changes: 1 addition & 1 deletion tests/main.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -339,7 +339,7 @@ describe("User start/stop", () => {

await expect(userStartStop(context)).rejects.toMatchObject({
logMessage: {
raw: "You do not have the adequate role to start this task (your role is: admin). Allowed roles are: contributor.",
raw: "You must be a core team member to start this task",
},
});
});
Expand Down

0 comments on commit 031823e

Please sign in to comment.