Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: close only assignee PR #10

Merged
Merged
Show file tree
Hide file tree
Changes from 10 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .github/knip.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,10 @@ import type { KnipConfig } from "knip";
const config: KnipConfig = {
entry: ["build/index.ts"],
project: ["src/**/*.ts"],
ignore: ["src/types/config.ts", "**/__mocks__/**", "**/__fixtures__/**", "src/worker.ts"],
ignore: ["src/types/config.ts", "**/__mocks__/**", "**/__fixtures__/**", "src/worker.ts", "src/types/plugin-input.ts", "src/main.ts", "src/plugin.ts"],
Keyrxng marked this conversation as resolved.
Show resolved Hide resolved
ignoreExportsUsedInFile: true,
// eslint can also be safely ignored as per the docs: https://knip.dev/guides/handling-issues#eslint--jest
ignoreDependencies: ["eslint-config-prettier", "eslint-plugin-prettier", "@types/jest"],
ignoreDependencies: ["eslint-config-prettier", "eslint-plugin-prettier", "@types/jest", "@actions/core", "@actions/github"],
gentlementlegen marked this conversation as resolved.
Show resolved Hide resolved
eslint: true,
};

Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/jest-testing.yml
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ jobs:
- uses: actions/checkout@master
with:
fetch-depth: 0

- name: Enable corepack
run: corepack enable

Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/knip.yml
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ jobs:
uses: actions/setup-node@v4
with:
node-version: 20.10.0

- name: Enable corepack
run: corepack enable

Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/worker-deploy.yml
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ jobs:

- name: Enable corepack
run: corepack enable

- uses: actions/checkout@v4
- uses: cloudflare/wrangler-action@v3
with:
Expand Down
2 changes: 1 addition & 1 deletion src/adapters/supabase/helpers/user.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ export class User extends Super {
}

async getWalletByUserId(userId: number, issueNumber: number) {
const { data, error } = await this.supabase.from("users").select("wallets(*)").eq("id", userId).single() as { data: { wallets: Wallet }, error: unknown };
const { data, error } = (await this.supabase.from("users").select("wallets(*)").eq("id", userId).single()) as { data: { wallets: Wallet }; error: unknown };
if ((error && !data) || !data.wallets?.address) {
this.context.logger.error("No wallet address found", { userId, issueNumber });
if (this.context.config.miscellaneous.startRequiresWallet) {
Expand Down
4 changes: 2 additions & 2 deletions src/handlers/shared/check-task-stale.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@ export function checkTaskStale(staleTask: number, createdAt: string) {
if (staleTask !== 0) {
const days = Math.floor((new Date().getTime() - new Date(createdAt).getTime()) / (1000 * 60 * 60 * 24));
const staleToDays = Math.floor(staleTask / (1000 * 60 * 60 * 24));
return days >= staleToDays;
return days >= staleToDays && staleToDays > 0;
}

return false;
}
}
16 changes: 5 additions & 11 deletions src/handlers/shared/stop.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,23 +7,17 @@ export async function stop(context: Context, issue: Context["payload"]["issue"],

// is there an assignee?
const assignees = issue.assignees ?? [];
if (assignees.length == 0) {
logger.error("No assignees found for issue", { issueNumber });
await addCommentToIssue(context, "````diff\n! You are not assigned to this task.\n````");
return { output: "No assignees found for this task" };
}

// should unassign?
const shouldUnassign = assignees[0]?.login.toLowerCase() == sender.login.toLowerCase();
const whoToUnassign = assignees.find((assignee) => assignee?.login.toLowerCase() === sender.login.toLowerCase());
Keyrxng marked this conversation as resolved.
Show resolved Hide resolved

if (!shouldUnassign) {
if (!whoToUnassign) {
logger.error("You are not assigned to this task", { issueNumber, user: sender.login });
await addCommentToIssue(context, "```diff\n! You are not assigned to this task.\n```");
Keyrxng marked this conversation as resolved.
Show resolved Hide resolved
return { output: "You are not assigned to this task" };
}

// close PR

await closePullRequestForAnIssue(context, issueNumber, repo);
await closePullRequestForAnIssue(context, issueNumber, repo, whoToUnassign.login);

const {
name,
Expand All @@ -44,6 +38,6 @@ export async function stop(context: Context, issue: Context["payload"]["issue"],
user: sender.login,
});

addCommentToIssue(context, "```diff\n+ You have been unassigned from this task.\n````").catch(logger.error);
addCommentToIssue(context, "```diff\n+ You have been unassigned from this task.\n```").catch(logger.error);
return { output: "Task unassigned successfully" };
}
2 changes: 1 addition & 1 deletion src/handlers/shared/structured-metadata.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { LogReturn } from "@ubiquity-dao/ubiquibot-logger";

function createStructuredMetadata(className: string, logReturn: LogReturn | null) {
let logMessage, metadata
let logMessage, metadata;
if (logReturn) {
logMessage = logReturn.logMessage;
metadata = logReturn.metadata;
Expand Down
2 changes: 1 addition & 1 deletion src/types/context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,5 +18,5 @@ export interface Context<T extends SupportedEventsU = SupportedEventsU, TU exten
adapters: ReturnType<typeof createAdapters>;
config: StartStopSettings;
env: Env;
logger: Logs
logger: Logs;
}
33 changes: 19 additions & 14 deletions src/utils/get-linked-prs.ts
Original file line number Diff line number Diff line change
@@ -1,43 +1,48 @@
import { Context } from "../types/context";
import { Issue } from "../types";
import { Context } from "../types/context";

interface GetLinkedParams {
owner: string;
repository: string;
issue: number;
issue?: number;
pull?: number;
}

interface GetLinkedResults {
export interface GetLinkedResults {
organization: string;
repository: string;
number: number;
href: string;
author: string;
body: string | null;
}

export async function getLinkedPullRequests(context: Context, { owner, repository, issue }: GetLinkedParams): Promise<GetLinkedResults[]> {
if (!issue) {
throw new Error("Issue is not defined");
}

const { data: timeline } = await context.octokit.issues.listEventsForTimeline({
owner,
repo: repository,
issue_number: issue,
});

const LINKED_PRS = timeline
.filter(
(event) =>
event.event === "cross-referenced" &&
"source" in event &&
!!event.source.issue &&
"repository" in event.source.issue &&
"pull_request" in event.source.issue
)
.filter((event) => event.event === "cross-referenced" && "source" in event && !!event.source.issue && "pull_request" in event.source.issue)
.map((event) => (event as { source: { issue: Issue } }).source.issue);

return LINKED_PRS.map((pr) => {
return {
organization: pr.repository?.full_name.split("/")[0],
repository: pr.repository?.full_name.split("/")[1],
organization: pr.repository?.full_name.split("/")[0] as string,
repository: pr.repository?.full_name.split("/")[1] as string,
number: pr.number,
href: pr.html_url,
author: pr.user?.login,
state: pr.state,
body: pr.body,
};
}).filter((pr) => pr !== null) as GetLinkedResults[];
})
.filter((pr) => pr !== null)
.filter((pr) => pr.state === "open") as GetLinkedResults[];
}
92 changes: 76 additions & 16 deletions src/utils/issue.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { Context } from "../types/context";
import { Issue, ISSUE_TYPE } from "../types/payload";
import { getLinkedPullRequests } from "./get-linked-prs";
import { getLinkedPullRequests, GetLinkedResults } from "./get-linked-prs";

export function isParentIssue(body: string) {
const parentPattern = /-\s+\[( |x)\]\s+#\d+/;
Expand Down Expand Up @@ -40,31 +40,32 @@ export async function addCommentToIssue(context: Context, message: string | null
issue_number: issueNumber,
body: comment,
});
} catch (e: unknown) {
context.logger.error("Adding a comment failed!", { error: e as Error });
} catch (err: unknown) {
context.logger.error("Adding a comment failed!", { error: err as Error });
}
}

// Pull requests
// Pull Requests

export async function closePullRequest(context: Context, pullNumber: number) {
const { repository } = context.payload;
export async function closePullRequest(context: Context, results: GetLinkedResults) {
const { payload } = context;
try {
await context.octokit.rest.pulls.update({
owner: repository.owner.login,
repo: repository.name,
pull_number: pullNumber,
owner: payload.repository.owner.login,
repo: payload.repository.name,
pull_number: results.number,
state: "closed",
});
} catch (err: unknown) {
context.logger.error("Closing pull requests failed!", { error: err as Error });
}
}

export async function closePullRequestForAnIssue(context: Context, issueNumber: number, repository: Context["payload"]["repository"]) {
const logger = context.logger;
export async function closePullRequestForAnIssue(context: Context, issueNumber: number, repository: Context["payload"]["repository"], author: string) {
const { logger } = context;
if (!issueNumber) {
throw logger.error("Issue is not defined");
logger.error("Issue is not defined");
return;
}

const linkedPullRequests = await getLinkedPullRequests(context, {
Expand All @@ -77,12 +78,36 @@ export async function closePullRequestForAnIssue(context: Context, issueNumber:
return logger.info(`No linked pull requests to close`);
}

logger.info(`Opened prs`, { message: JSON.stringify(linkedPullRequests) });
logger.info(`Opened prs`, { author, linkedPullRequests });
let comment = "```diff\n# These linked pull requests are closed: ";
for (let i = 0; i < linkedPullRequests.length; i++) {
await closePullRequest(context, linkedPullRequests[i].number);
comment += ` ${linkedPullRequests[i].href} `;

let isClosed = false;

for (const pr of linkedPullRequests) {
/**
* If the PR author is not the same as the issue author, skip the PR
* If the PR organization is not the same as the issue organization, skip the PR
*
* Same organization and author, close the PR
*/
if (pr.author !== author || pr.organization !== repository.owner.login) {
continue;
} else {
const isLinked = issueLinkedViaPrBody(pr.body, issueNumber);
if (!isLinked) {
logger.info(`Issue is not linked to the PR`, { issueNumber, prNumber: pr.number });
continue;
}
await closePullRequest(context, pr);
comment += ` ${pr.href} `;
isClosed = true;
}
}

if (!isClosed) {
return logger.info(`No PRs were closed`);
}

await addCommentToIssue(context, comment);
return logger.info(comment);
}
Expand Down Expand Up @@ -169,3 +194,38 @@ async function getOpenedPullRequests(context: Context, username: string): Promis
const prs = await getAllPullRequests(context, "open");
return prs.filter((pr) => !pr.draft && (pr.user?.login === username || !username));
}

/**
* Extracts the task id from the PR body. The format is:
* `Resolves #123`
* `Requires https://www.github.com/.../issue/123`
* `Fixes https://github.com/.../issues/123`
* `Closes #123`
* `Depends on #123`
* `Related to #123`
*/
export function issueLinkedViaPrBody(prBody: string | null, issueNumber: number): boolean {
const regex = // eslint-disable-next-line no-useless-escape
/(?:Resolves|Fixes|Closes|Depends on|Related to) #(\d+)|https:\/\/(?:www\.)?github.com\/([^\/]+)\/([^\/]+)\/(issue|issues)\/(\d+)|#(\d+)/gi;
const matches = prBody?.match(regex);
Keyrxng marked this conversation as resolved.
Show resolved Hide resolved

if (!matches) {
return false;
}

let issueId;

matches.map((match) => {
if (match.startsWith("http")) {
// Extract the issue number from the URL
const urlParts = match.split("/");
issueId = urlParts[urlParts.length - 1];
} else {
// Extract the issue number directly from the hashtag
const hashtagParts = match.split("#");
issueId = hashtagParts[hashtagParts.length - 1]; // The issue number follows the '#'
}
});

return issueId === issueNumber.toString();
}
45 changes: 44 additions & 1 deletion tests/__mocks__/db.ts
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,29 @@ export const db = factory({
body: nullable(String),
repo: String,
owner: String,
author: Object,
author: nullable({
avatar_url: String,
email: nullable(String),
events_url: String,
followers_url: String,
following_url: String,
gists_url: String,
gravatar_id: nullable(String),
html_url: String,
id: Number,
login: String,
name: nullable(String),
node_id: String,
organizations_url: String,
received_events_url: String,
repos_url: String,
site_admin: Boolean,
starred_at: String,
starred_url: String,
subscriptions_url: String,
type: String,
url: String,
}),
assignees: Array,
requested_reviewers: Array,
requested_teams: Array,
Expand Down Expand Up @@ -119,5 +141,26 @@ export const db = factory({
commit_id: nullable(String),
commit_url: String,
created_at: Date,
source: nullable({
issue: {
number: Number,
html_url: String,
state: String,
body: nullable(String),
repository: {
full_name: String,
},
user: {
login: String,
},
pull_request: {
url: String,
html_url: String,
diff_url: String,
patch_url: String,
merged_at: Date,
},
},
}),
},
});
Loading