Skip to content

Commit

Permalink
Merge branch 'development' into feature/new-text-config-param-for-emp…
Browse files Browse the repository at this point in the history
…ty-wallet
  • Loading branch information
whck6 authored Aug 31, 2024
2 parents d2313c0 + a094a8c commit 8241c00
Show file tree
Hide file tree
Showing 16 changed files with 523 additions and 195 deletions.
4 changes: 3 additions & 1 deletion .cspell.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,9 @@
"ubiquibot",
"signoff",
"sonarjs",
"mswjs"
"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"],
Expand Down
44 changes: 44 additions & 0 deletions .github/workflows/worker-delete.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
name: Delete Deployment

on:
delete:

jobs:
delete:
runs-on: ubuntu-latest
name: Delete Deployment
steps:
- name: Setup Node
uses: actions/setup-node@v4
with:
node-version: "20.10.0"

- name: Enable corepack
run: corepack enable

- uses: actions/checkout@v4

- name: Get Deleted Branch Name
id: get_branch
run: |
branch_name=$(echo '${{ github.event.ref }}' | sed 's#refs/heads/##' | sed 's#[^a-zA-Z0-9]#-#g')
echo "branch_name=$branch_name" >> $GITHUB_ENV
- name: Retrieve and Construct Full Worker Name
id: construct_worker_name
run: |
base_name=$(grep '^name = ' wrangler.toml | sed 's/^name = "\(.*\)"$/\1/')
full_worker_name="${base_name}-${{ env.branch_name }}"
# Make sure that it doesnt exceed 63 characters or it will break RFC 1035
full_worker_name=$(echo "${full_worker_name}" | cut -c 1-63)
echo "full_worker_name=$full_worker_name" >> $GITHUB_ENV
- name: Delete Deployment with Wrangler
uses: cloudflare/wrangler-action@v3
with:
apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
command: delete --name ${{ env.full_worker_name }}

- name: Output Deletion Result
run: |
echo "### Deployment URL" >> $GITHUB_STEP_SUMMARY
echo 'Deployment `${{ env.full_worker_name }}` has been deleted.' >> $GITHUB_STEP_SUMMARY
27 changes: 23 additions & 4 deletions .github/workflows/worker-deploy.yml
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
name: Deploy
name: Deploy Worker

on:
push:
branches:
- main
workflow_dispatch:

jobs:
deploy:
Expand All @@ -19,7 +18,22 @@ jobs:
run: corepack enable

- uses: actions/checkout@v4
- uses: cloudflare/wrangler-action@v3

- name: Update wrangler.toml Name Field
run: |
branch_name=$(echo '${{ github.event.ref }}' | sed 's#refs/heads/##' | sed 's#[^a-zA-Z0-9]#-#g')
# Extract base name from wrangler.toml
base_name=$(grep '^name = ' wrangler.toml | sed 's/^name = "\(.*\)"$/\1/')
# Concatenate branch name with base name
new_name="${base_name}-${branch_name}"
# Truncate the new name to 63 characters for RFC 1035
new_name=$(echo "$new_name" | cut -c 1-63)
# Update the wrangler.toml file
sed -i "s/^name = .*/name = \"$new_name\"/" wrangler.toml
echo "Updated wrangler.toml name to: $new_name"
- name: Deploy with Wrangler
id: wrangler_deploy
uses: cloudflare/wrangler-action@v3
with:
wranglerVersion: "3.57.0"
apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
Expand All @@ -30,3 +44,8 @@ jobs:
env:
SUPABASE_URL: ${{ secrets.SUPABASE_URL }}
SUPABASE_KEY: ${{ secrets.SUPABASE_KEY }}

- name: Write Deployment URL to Summary
run: |
echo "### Deployment URL" >> $GITHUB_STEP_SUMMARY
echo "${{ steps.wrangler_deploy.outputs.deployment-url }}" >> $GITHUB_STEP_SUMMARY
92 changes: 92 additions & 0 deletions src/handlers/shared/check-assignments.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
import { Context } from "../../types";
import { getOwnerRepoFromHtmlUrl } from "../../utils/issue";

async function getUserStopComments(context: Context, username: string): Promise<number> {
const { payload, octokit, logger } = context;
const { number, html_url } = payload.issue;
const { owner, repo } = getOwnerRepoFromHtmlUrl(html_url);

try {
const comments = await octokit.paginate(octokit.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);
}
}

export async function hasUserBeenUnassigned(context: Context, username: string): Promise<boolean> {
const {
env: { APP_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 === APP_ID);
// UI assignment
const adminUnassigned = unassignedEvents.filter((event) => event.actor !== username && event.actorId !== APP_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) {
const { repository, issue } = context.payload;
try {
const data = await context.octokit.paginate(context.octokit.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);
}
}
78 changes: 60 additions & 18 deletions src/handlers/shared/start.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { Context, ISSUE_TYPE, Label } from "../../types";
import { isParentIssue, getAvailableOpenedPullRequests, getAssignedIssues, addAssignees, addCommentToIssue, getTimeValue } from "../../utils/issue";
import { calculateDurations } from "../../utils/shared";
import { checkTaskStale } from "./check-task-stale";
import { hasUserBeenUnassigned } from "./check-assignments";
import { generateAssignmentComment } from "./generate-assignment-comment";
import structuredMetadata from "./structured-metadata";
import { assignTableComment } from "./table";
Expand All @@ -16,8 +17,7 @@ export async function start(context: Context, issue: Context["payload"]["issue"]
context,
"```diff\n# Please select a child issue from the specification checklist to work on. The '/start' command is disabled on parent issues.\n```"
);
logger.error(`Skipping '/start' since the issue is a parent issue`);
return { output: "Parent issue detected" };
throw new Error(logger.error(`Skipping '/start' since the issue is a parent issue`).logMessage.raw);
}

let commitHash: string | null = null;
Expand All @@ -36,51 +36,66 @@ export async function start(context: Context, issue: Context["payload"]["issue"]
// is it assignable?

if (issue.state === ISSUE_TYPE.CLOSED) {
throw logger.error("This issue is closed, please choose another.", { issueNumber: issue.number });
throw new Error(logger.error("This issue is closed, please choose another.", { issueNumber: issue.number }).logMessage.raw);
}

const assignees = issue?.assignees ?? [];

// find out if the issue is already assigned
if (assignees.length !== 0) {
const isCurrentUserAssigned = !!assignees.find((assignee) => assignee?.login === sender.login);
throw logger.error(
isCurrentUserAssigned ? "You are already assigned to this task." : "This issue is already assigned. Please choose another unassigned task.",
{ issueNumber: issue.number }
throw new Error(
logger.error(
isCurrentUserAssigned ? "You are already assigned to this task." : "This issue is already assigned. Please choose another unassigned task.",
{ issueNumber: issue.number }
).logMessage.raw
);
}

teammates.push(sender.login);

const toAssign = [];
// check max assigned issues
for (const user of teammates) {
await handleTaskLimitChecks(user, context, maxConcurrentTasks, logger, sender.login);
if (await handleTaskLimitChecks(user, context, maxConcurrentTasks, logger, sender.login)) {
toAssign.push(user);
}
}

let error: string | null = null;

if (toAssign.length === 0 && teammates.length > 1) {
error = "All teammates have reached their max task limit. Please close out some tasks before assigning new ones.";
} else if (toAssign.length === 0) {
error = "You have reached your max task limit. Please close out some tasks before assigning new ones.";
}

if (error) {
throw new Error(logger.error(error, { issueNumber: issue.number }).logMessage.raw);
}

// get labels
const labels = issue.labels;
const priceLabel = labels.find((label: Label) => label.name.startsWith("Price: "));

if (!priceLabel) {
throw logger.error("No price label is set to calculate the duration", { issueNumber: issue.number });
throw new Error(logger.error("No price label is set to calculate the duration", { issueNumber: issue.number }).logMessage.raw);
}

const duration: number = calculateDurations(labels).shift() ?? 0;
const toAssignIds = await fetchUserIds(context, toAssign);

const { id } = sender;
const assignmentComment = await generateAssignmentComment(context, issue.created_at, issue.number, sender.id, duration);
const logMessage = logger.info("Task assigned successfully", {
duration,
taskDeadline: assignmentComment.deadline,
taskAssignees: toAssignIds,
priceLabel,
revision: commitHash?.substring(0, 7),
assignees: teammates,
issue: issue.number,
});

const assignmentComment = await generateAssignmentComment(context, issue.created_at, issue.number, id, duration);
const metadata = structuredMetadata.create("Assignment", logMessage);

// assign the issue
await addAssignees(context, issue.number, teammates);
// add assignee
await addAssignees(context, issue.number, toAssign);

const isTaskStale = checkTaskStale(getTimeValue(taskStaleTimeoutDuration), issue.created_at);

Expand All @@ -101,16 +116,43 @@ export async function start(context: Context, issue: Context["payload"]["issue"]
return { output: "Task assigned successfully" };
}

async function fetchUserIds(context: Context, username: string[]) {
const ids = [];

for (const user of username) {
const { data } = await context.octokit.rest.users.getByUsername({
username: user,
});

ids.push(data.id);
}

if (ids.filter((id) => !id).length > 0) {
throw new Error("Error while fetching user ids");
}

return ids;
}

async function handleTaskLimitChecks(username: string, context: Context, maxConcurrentTasks: number, logger: Context["logger"], sender: string) {
const openedPullRequests = await getAvailableOpenedPullRequests(context, username);
const assignedIssues = await getAssignedIssues(context, username);

// check for max and enforce max
if (assignedIssues.length - openedPullRequests.length >= maxConcurrentTasks) {
throw logger.error(username === sender ? "You have reached your max task limit" : `${username} has reached their max task limit`, {

if (Math.abs(assignedIssues.length - openedPullRequests.length) >= maxConcurrentTasks) {
const log = logger.error(username === sender ? "You have reached your max task limit" : `${username} has reached their max task limit`, {
assignedIssues: assignedIssues.length,
openedPullRequests: openedPullRequests.length,
maxConcurrentTasks,
});
await addCommentToIssue(context, log?.logMessage.diff as string);
return false;
}

if (await hasUserBeenUnassigned(context, username)) {
throw new Error(logger.error(`${username} you were previously unassigned from this task. You cannot be reassigned.`, { username }).logMessage.raw);
}

return true;
}
16 changes: 9 additions & 7 deletions src/handlers/shared/stop.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { Assignee, Context } from "../../types";
import { Assignee, Context, Sender } from "../../types";
import { addCommentToIssue, closePullRequestForAnIssue } from "../../utils/issue";

export async function stop(context: Context, issue: Context["payload"]["issue"], sender: Context["payload"]["sender"], repo: Context["payload"]["repository"]) {
export async function stop(context: Context, issue: Context["payload"]["issue"], sender: Sender, repo: Context["payload"]["repository"]) {
const { logger } = context;
const issueNumber = issue.number;

Expand Down Expand Up @@ -32,11 +32,13 @@ export async function stop(context: Context, issue: Context["payload"]["issue"],
assignees: [userToUnassign.login],
});
} catch (err) {
throw logger.error(`Error while removing ${userToUnassign.login} from the issue: `, {
err,
issueNumber,
user: userToUnassign.login,
});
throw new Error(
logger.error(`Error while removing ${userToUnassign.login} from the issue: `, {
err,
issueNumber,
user: userToUnassign.login,
}).logMessage.raw
);
}

const unassignedLog = logger.info("You have been unassigned from the task", {
Expand Down
4 changes: 3 additions & 1 deletion src/handlers/user-start-stop.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,10 @@ export async function userStartStop(context: Context): Promise<{ output: string
.slice(1)
.map((teamMate) => teamMate.split(" ")[0]);

const user = comment.user?.login ? { login: comment.user.login, id: comment.user.id } : { login: sender.login, id: sender.id };

if (slashCommand === "stop") {
return await stop(context, issue, sender, repository);
return await stop(context, issue, user, repository);
} else if (slashCommand === "start") {
return await start(context, issue, sender, teamMates);
}
Expand Down
Loading

0 comments on commit 8241c00

Please sign in to comment.