@shopware-ag/gh-project-automation
Version:
Project automation for shopware
1,149 lines (1,136 loc) • 38.9 kB
JavaScript
import { WebClient } from '@slack/web-api';
async function closeIssue(toolkit, issueId, reason = "NOT_PLANNED") {
const res = await toolkit.github.graphql(
/* GraphQL */
`
mutation closeIssue($issueId: ID!, $reason: IssueClosedStateReason!) {
closeIssue(input: {
issueId: $issueId,
stateReason: $reason,
}) {
clientMutationId
}
}
`,
{
issueId,
reason
}
);
toolkit.core.debug(`closeIssue response: ${JSON.stringify(res)}`);
}
async function closePullRequest(toolkit, pullRequestId) {
const res = await toolkit.github.graphql(
/* GraphQL */
`
mutation closeIssue($pullRequestId: ID!) {
closePullRequest(input: {
pullRequestId: $pullRequestId
}) {
clientMutationId
}
}
`,
{
pullRequestId
}
);
toolkit.core.debug(`closePullRequest response: ${JSON.stringify(res)}`);
}
async function getLabelByName(toolkit, repository, labelName) {
const res = await toolkit.github.graphql(
/* GraphQL */
`
query getLabelId($repository: String!, $labelName: String!) {
repository(owner: "shopware", name: $repository) {
label(name: $labelName) {
id
name
url
description
color
}
}
}
`,
{
repository,
labelName
}
);
return res.repository.label;
}
async function addLabelToLabelable(toolkit, labelId, labelableId) {
const res = await toolkit.github.graphql(
/* GraphQL */
`
mutation addLabels($labelId: ID!, $labelableId: ID!) {
addLabelsToLabelable(input: {
labelIds: [$labelId],
labelableId: $labelableId
}) {
clientMutationId
}
}
`,
{
labelId,
labelableId
}
);
toolkit.core.debug(`addLabelToIssue response: ${JSON.stringify(res)}`);
}
async function findIssueWithProjectItems(toolkit, number) {
const res = await toolkit.github.graphql(
/* GraphQL */
`
query findIssueWithProjectItems($number: Int!) {
repository(owner: "shopware", name: "shopware") {
issue(number: $number) {
projectItems(first: 20) {
nodes {
id
project {
number
}
fieldValueByName(name: "Status") {
... on ProjectV2ItemFieldSingleSelectValue {
name
}
}
}
}
id
number
}
}
}
`,
{
number
}
);
toolkit.core.debug(`findIssueInProject response: ${JSON.stringify(res)}`);
return {
node_id: res.repository.issue.id,
number: res.repository.issue.number,
projectItems: res.repository.issue.projectItems.nodes
};
}
async function findPRWithProjectItems(toolkit, number) {
const res = await toolkit.github.graphql(
/* GraphQL */
`
query findPRWithProjectItems($number: Int!) {
repository(owner: "shopware", name: "shopware") {
pullRequest(number: $number) {
projectItems(first: 20) {
nodes {
project {
number
}
fieldValueByName(name: "Status") {
... on ProjectV2ItemFieldSingleSelectValue {
name
}
}
}
}
id
number
}
}
}
`,
{
number
}
);
toolkit.core.debug(`findIssueInProject response: ${JSON.stringify(res)}`);
return {
node_id: res.repository.pullRequest.id,
number: res.repository.pullRequest.number,
projectItems: res.repository.pullRequest.projectItems.nodes
};
}
async function setFieldValue(toolkit, data) {
const res = await toolkit.github.graphql(
/* GraphQL */
`
mutation setFieldValue($projectId: ID!, $itemId: ID!, $fieldId: ID!, $valueId: String!) {
updateProjectV2ItemFieldValue(input: {
projectId: $projectId,
itemId: $itemId,
fieldId: $fieldId,
value: {singleSelectOptionId: $valueId}
}) {
projectV2Item {
id
}
}
}
`,
data
);
toolkit.core.debug(`setFieldValue response: ${JSON.stringify(res)}`);
return res;
}
async function getProjectInfo(toolkit, data) {
const res = await toolkit.github.graphql(
/* GraphQL */
`
query getProjectInfo($organization: String!, $projectNumber: Int!) {
organization(login: $organization) {
projectV2(number: $projectNumber) {
id
title
fields(first: 20) {
nodes {
... on ProjectV2SingleSelectField {
id
name
options {
id
name
}
}
}
}
}
}
}
`,
{
organization: data.organization ?? "shopware",
projectNumber: data.number
}
);
toolkit.core.debug(`getProjectInfo response: ${JSON.stringify(res)}`);
const project = res.organization.projectV2;
return {
node_id: project.id,
title: project.title,
fields: project.fields.nodes
};
}
async function addProjectItem(toolkit, data) {
const res = await toolkit.github.graphql(
/* GraphQL */
`
mutation addProjectItem($projectId: ID!, $contentId: ID!) {
addProjectV2ItemById(input: {
projectId: $projectId,
contentId: $contentId
}) {
item {
id
}
}
}
`,
{
projectId: data.projectId,
contentId: data.issueId
}
);
toolkit.core.debug(`addCard response: ${JSON.stringify(res)}`);
return {
node_id: res.addProjectV2ItemById.item.id
};
}
async function getProjectIdByNumber(toolkit, number, organization = "shopware") {
const res = await toolkit.github.graphql(
/* GraphQL */
`
query getProjectIdByNumber($organization: String!, $number: Int!) {
organization(login: $organization) {
projectV2(number: $number) {
id
}
}
}
`,
{
organization,
number
}
);
return res.organization.projectV2.id;
}
async function getIssuesByProject(toolkit, projectId, cursor = null, carry = null) {
const res = await toolkit.github.graphql(
/* GraphQL */
`
query getIssuesByProject($projectId: ID!, $count: Int, $cursor: String) {
node(id: $projectId) {
... on ProjectV2 {
items(first: $count, after: $cursor) {
pageInfo {
startCursor
endCursor
hasPreviousPage
hasNextPage
}
nodes {
fieldValueByName(name: "Status") {
... on ProjectV2ItemFieldSingleSelectValue {
name
}
}
content {
... on Issue {
id
title
number
url
issueType {
name
}
}
}
}
}
}
}
}
`,
{
projectId,
count: 100,
cursor
}
);
const issues = res.node.items.nodes.map((item) => {
return {
id: item.content.id,
title: item.content.title,
number: item.content.number,
url: item.content.url,
status: item.fieldValueByName?.name,
labels: [],
type: item.content?.issueType?.name
};
});
if (res.node.items.pageInfo.hasNextPage) {
return await getIssuesByProject(toolkit, projectId, res.node.items.pageInfo.endCursor, [...issues, ...carry ?? []]);
} else {
return [...issues, ...carry ?? []];
}
}
async function getCommentsForIssue(toolkit, issueId, cursor = null, carry = null) {
const res = await toolkit.github.graphql(
/* GraphQL */
`
query getCommentsForIssue($issueId: ID!, $count: Int, $cursor: String) {
node(id: $issueId) {
... on Issue {
comments(first: $count, after: $cursor) {
pageInfo {
startCursor
endCursor
hasPreviousPage
hasNextPage
}
nodes {
id
author {
login
}
body
}
}
}
}
}
`,
{
issueId,
count: 100,
cursor
}
);
const comments = res.node.comments.nodes;
if (res.node.comments.pageInfo.hasNextPage) {
return await getCommentsForIssue(toolkit, issueId, res.node.comments.pageInfo.endCursor, [...comments, ...carry ?? []]);
} else {
return [...comments, ...carry ?? []];
}
}
async function getPullRequests(toolkit, searchQuery) {
const pullRequests = await toolkit.github.graphql(
/* GraphQL */
`
query findPullRequests($searchQuery: String!) {
search(query: $searchQuery, type: ISSUE, first: 50) {
pageInfo {
startCursor
endCursor
hasPreviousPage
hasNextPage
}
nodes {
... on PullRequest {
id
title
number
url
repository {
owner {
login
}
name
}
assignees(first: 50) {
nodes {
login
}
}
reviewRequests(first: 50) {
nodes {
requestedReviewer {
... on User {
login
}
... on Team {
name
}
}
}
}
closingIssuesReferences(first: 1) {
nodes {
id
title
number
url
repository {
owner {
login
}
name
}
}
}
}
}
}
}`,
{
searchQuery
}
).then((res) => res.search.nodes);
return pullRequests;
}
async function addComment(toolkit, issueId, commentBody) {
const comment = await toolkit.github.graphql(
/* GraphQL */
`
mutation addComment($issueId: ID!, $body: String!) {
addComment(input: {
subjectId: $issueId,
body: $body
}) {
commentEdge {
node {
id
author {
login
}
body
url
}
}
}
}
`,
{
issueId,
body: commentBody
}
).then((res) => res.addComment.commentEdge.node);
if (!comment || !comment.id) {
throw new Error(`Failed to create comment: ${JSON.stringify(comment)}`);
}
return comment;
}
async function getVerifiedDomainEmails(toolkit, login, organization) {
const res = await toolkit.github.graphql(
/* GraphQL */
`
query getVerifiedDomainEmails($login: String!, $organization: String!) {
user(login: $login) {
organizationVerifiedDomainEmails(login: $organization)
}
}
`,
{
login,
organization
}
);
return res.user.organizationVerifiedDomainEmails;
}
const jiraHost = "shopware.atlassian.net";
const jiraBaseUrl = `https://${jiraHost}/rest/api/3`;
async function callJiraApi(toolkit, endpoint, requestBody) {
return await toolkit.fetch(`${jiraBaseUrl}${endpoint}`, {
method: "POST",
headers: {
"Authorization": `Basic ${Buffer.from(
`${process.env.JIRA_USERNAME}:${process.env.JIRA_API_TOKEN}`
).toString("base64")}`,
"Accept": "application/json",
"Content-Type": "application/json"
},
body: JSON.stringify(requestBody)
}).then((res) => res.json());
}
class SlackClient {
webClient;
constructor(token) {
this.webClient = new WebClient(token);
}
async getUserByEmail(email) {
const response = await this.webClient.users.lookupByEmail({ email });
return response.user?.id || null;
}
async sendIMToUser(userId, message) {
await this.webClient.chat.postMessage({
channel: userId,
text: message,
blocks: [
{
type: "markdown",
text: message
}
]
});
}
}
function isDryRun() {
return process.env.DRY_RUN === "true" || process.env.DRY_RUN === "1";
}
async function getOldBranches(toolkit, repo, excludeRegex = "", organization = "shopware") {
const DAYS_UNTIL_STALE = 6 * 30;
const regex = new RegExp(excludeRegex);
toolkit.core.info(`Getting branches for ${organization}/${repo}`);
const branches = [];
let currentCursor = null;
while (true) {
const res = await toolkit.github.graphql(
/* GraphQL */
`
query getBranches(
$cursor: String
$organization: String!
$repository: String!
) {
repository(owner: $organization, name: $repository) {
refs(first: 100, after: $cursor, refPrefix: "refs/heads/") {
pageInfo {
startCursor
endCursor
hasNextPage
}
nodes {
name
target {
... on Commit {
committedDate
}
}
associatedPullRequests(first: 100) {
nodes {
number
}
}
}
}
}
}
`,
{
cursor: currentCursor,
repository: repo,
organization
}
);
branches.push(...res.repository.refs.nodes);
if (!res.repository.refs.pageInfo.hasNextPage) {
break;
}
currentCursor = res.repository.refs.pageInfo.endCursor;
}
const today = /* @__PURE__ */ new Date();
const cmpDate = new Date((/* @__PURE__ */ new Date()).setDate(today.getDate() - DAYS_UNTIL_STALE));
const oldBranches = [];
for (const branch of branches.filter((x) => x.associatedPullRequests.nodes.length == 0)) {
if (excludeRegex != "" && regex.test(branch.name)) {
continue;
}
const lastUpdate = new Date(branch.target.committedDate);
if (lastUpdate < cmpDate) {
oldBranches.push(branch.name);
toolkit.core.debug(`${branch.name} : ${lastUpdate}`);
}
}
return oldBranches;
}
async function cleanupBranches(toolkit, repo, organization = "shopware") {
const branches = await getOldBranches(toolkit, repo, organization);
if (!branches) {
toolkit.core.error("No old branches found!");
return;
}
for (const branch of branches) {
if (isDryRun()) {
toolkit.core.info(`Would delete ${branch}`);
continue;
}
toolkit.core.info(`Deleting ${branch}...`);
toolkit.github.rest.git.deleteRef({
owner: organization,
repo,
ref: branch
});
}
}
const docIssueReference = Buffer.from("doc-issue-created").toString("base64");
async function getDevelopmentIssueForPullRequest(toolkit, repo, pullRequestNumber, pullRequestHead, pullRequestAssignee) {
const pullRequests = await getPullRequests(
toolkit,
`repo:${repo} is:pr assignee:${pullRequestAssignee} head:${pullRequestHead}`
);
const matchingPullRequests = pullRequests.filter((pr) => pr.number === pullRequestNumber);
const developmentIssue = matchingPullRequests.length > 0 ? matchingPullRequests[0]?.closingIssuesReferences?.nodes[0] : null;
if (developmentIssue) {
return {
id: developmentIssue.id,
title: developmentIssue.title,
number: developmentIssue.number,
url: developmentIssue.url,
labels: [],
owner: developmentIssue.repository.owner.login,
repository: developmentIssue.repository.name
};
} else {
toolkit.core.info(`No development issue found for PR #${pullRequestNumber}`);
return null;
}
}
async function getEpicsInProgressByProject(toolkit, projectId) {
const res = await getIssuesByProject(toolkit, projectId);
return res.filter((item) => {
return item.status?.toLowerCase() === "in progress" && item.type?.toLowerCase() === "epic";
});
}
async function createDocIssueComment(toolkit, issueId, docIssueKey, content = null) {
const commentBody = `${content ?? "A documentation Task has been created for this issue:"} [${docIssueKey}](https://${jiraHost}/browse/${docIssueKey}). <!-- ${docIssueReference} -->`;
const comment = await addComment(toolkit, issueId, commentBody);
toolkit.core.info(`Created documentation issue reference comment: ${comment.url}`);
return comment;
}
async function hasDocIssueComment(toolkit, issueId) {
const comments = await getCommentsForIssue(toolkit, issueId);
for (const comment of comments) {
if (referencesDocIssue(comment)) {
return true;
}
}
return false;
}
function referencesDocIssue(comment) {
return comment.body.includes(docIssueReference);
}
async function manageNeedsTriageLabel(toolkit, labelable, needsTriageLabel, dryRun) {
const labels = labelable.labels.nodes.map((label) => label.name);
const hasNeedsTriage = labels.includes("needs-triage");
const hasDomainOrServiceLabel = labels.some(
(label) => label.startsWith("domain/") || label.startsWith("service/")
);
if (hasDomainOrServiceLabel && hasNeedsTriage) {
if (dryRun) {
toolkit.core.info(`Would remove needs-triage label from #${labelable.number}: ${labelable.title} (has domain/service label)`);
} else {
await toolkit.github.graphql(
/* GraphQL */
`
mutation removeLabel($labelableId: ID!, $labelIds: [ID!]!) {
removeLabelsFromLabelable(input: {
labelableId: $labelableId,
labelIds: $labelIds
}) {
clientMutationId
}
}
`,
{
labelableId: labelable.id,
labelIds: [needsTriageLabel.id]
}
);
toolkit.core.info(`Removed needs-triage label from #${labelable.number}: ${labelable.title} (has domain/service label)`);
}
} else if (!hasNeedsTriage && !hasDomainOrServiceLabel) {
if (dryRun) {
toolkit.core.info(`Would add needs-triage label to #${labelable.number}: ${labelable.title}`);
} else {
await addLabelToLabelable(toolkit, needsTriageLabel.id, labelable.id);
toolkit.core.info(`Added needs-triage label to #${labelable.number}: ${labelable.title}`);
}
}
}
async function cleanupNeedsTriage(toolkit, dryRun = false) {
const needsTriageLabel = await getLabelByName(toolkit, "shopware", "needs-triage");
if (!needsTriageLabel) {
throw new Error("Couldn't find the needs-triage label");
}
let processedItems = 0;
let hasNextPageIssues = true;
let cursorIssues = null;
while (hasNextPageIssues) {
const res = await toolkit.github.graphql(
/* GraphQL */
`
query getOpenIssues($cursor: String) {
repository(owner: "shopware", name: "shopware") {
issues(first: 100, after: $cursor, states: OPEN) {
pageInfo {
hasNextPage
endCursor
}
nodes {
id
number
title
labels(first: 20) {
nodes {
name
}
}
}
}
}
}
`,
{
cursor: cursorIssues
}
);
const issues = res.repository.issues?.nodes ?? [];
hasNextPageIssues = res.repository.issues?.pageInfo.hasNextPage ?? false;
cursorIssues = res.repository.issues?.pageInfo.endCursor ?? null;
for (const item of issues) {
await manageNeedsTriageLabel(toolkit, item, needsTriageLabel, dryRun);
}
processedItems += issues.length;
toolkit.core.info(`Processed ${processedItems} items so far...`);
}
let hasNextPagePRs = true;
let cursorPRs = null;
while (hasNextPagePRs) {
const res = await toolkit.github.graphql(
/* GraphQL */
`
query getOpenPRs($cursor: String) {
repository(owner: "shopware", name: "shopware") {
pullRequests(first: 100, after: $cursor, states: OPEN) {
pageInfo {
hasNextPage
endCursor
}
nodes {
id
number
title
labels(first: 20) {
nodes {
name
}
}
}
}
}
}
`,
{
cursor: cursorPRs
}
);
const pullRequests = res.repository.pullRequests?.nodes ?? [];
hasNextPagePRs = res.repository.pullRequests?.pageInfo.hasNextPage ?? false;
cursorPRs = res.repository.pullRequests?.pageInfo.endCursor ?? null;
for (const item of pullRequests) {
await manageNeedsTriageLabel(toolkit, item, needsTriageLabel, dryRun);
}
processedItems += pullRequests.length;
toolkit.core.info(`Processed ${processedItems} items so far...`);
}
toolkit.core.info(`Finished processing ${processedItems} items`);
}
async function findWithProjectItems(toolkit) {
if (toolkit.context.payload.issue) {
return await findIssueWithProjectItems(toolkit, toolkit.context.payload.issue.number);
} else if (toolkit.context.payload.pull_request) {
return await findPRWithProjectItems(toolkit, toolkit.context.payload.pull_request.number);
} else {
throw new Error("only issue and pull_request events are supported");
}
}
async function createDocumentationTask(toolkit, issue, documentationProjectId = 11806, description = null) {
const requestBody = buildDocumentationTaskRequest(issue, documentationProjectId, description);
const docTask = await callJiraApi(toolkit, "/issue", requestBody);
if (!docTask || !docTask.key) {
throw new Error(`Failed to create documentation task: ${JSON.stringify(docTask)}`);
}
toolkit.core.info(`Created documentation task in JIRA: https://${jiraHost}/browse/${docTask.key}`);
return docTask;
}
function buildDocumentationTaskRequest(issue, documentationProjectId, description) {
return {
fields: {
project: {
id: documentationProjectId
},
summary: `${issue.title}`,
description: buildDocumentationDescription(issue, description),
issuetype: {
name: "Task"
}
}
};
}
function buildDocumentationDescription(issue, description) {
return {
version: 1,
type: "doc",
content: [
{
type: "paragraph",
content: [
{
type: "text",
text: description ?? `This issue was created automatically.`
},
{
type: "text",
text: `
Please refer to the following links and/or related issues for more information:`
}
]
},
{
type: "bulletList",
content: [
{
type: "listItem",
content: [
{
type: "paragraph",
content: [
{
type: "text",
text: issue.url,
marks: [
{
type: "link",
attrs: {
href: issue.url
}
}
]
}
]
}
]
}
]
}
]
};
}
async function setStatusInProjects(toolkit, props) {
const issue = await findWithProjectItems(toolkit);
for (const i in issue.projectItems) {
const item = issue.projectItems[i];
const projectInfo = await getProjectInfo(toolkit, { number: item.project.number });
const statusField = projectInfo.fields.find((field) => field.name == "Status");
const toStatusOption = statusField?.options.find((x) => x.name.toLowerCase() === props.toStatus.toLowerCase());
if (!toStatusOption) {
toolkit.core.debug(`Option "${props.toStatus}" not found in project ${item.project.number}`);
continue;
}
if (props.fromStatus instanceof RegExp) {
if (!item.fieldValueByName.name.match(props.fromStatus)) {
toolkit.core.debug(`skipping: issue/pr ${issue.number} status ${item.fieldValueByName} did not match ${props.fromStatus} in project ${item.project.number}`);
continue;
}
} else if (props.fromStatus && item.fieldValueByName.name.toLowerCase() !== props.fromStatus.toLowerCase()) {
toolkit.core.debug(`skipping: issue/pr ${issue.number} status != ${props.fromStatus} in project ${item.project.number}`);
continue;
}
toolkit.core.info(`get item in project ${item.project.number} for issue/pr ${issue.number}`);
const itemId = (await addProjectItem(toolkit, {
projectId: projectInfo.node_id,
issueId: issue.node_id
})).node_id;
await setFieldValue(toolkit, {
projectId: projectInfo.node_id,
itemId,
fieldId: statusField.id,
valueId: toStatusOption.id
});
}
}
async function syncPriorities(toolkit, excludeList = []) {
const issue = toolkit.context.payload.issue;
toolkit.core.debug(`Issue node ID: ${issue.node_id}`);
const priorityLabel = issue.labels.find(
(label) => label.name.startsWith("priority/")
)?.name;
if (!priorityLabel) {
return;
}
toolkit.core.info(`Found priority label: ${priorityLabel}`);
const priority = priorityLabel.split("/")[1];
toolkit.core.info(`Priority: ${priority}`);
const issueWithProjectItems = await findIssueWithProjectItems(toolkit, issue.number);
for (const projectItem of issueWithProjectItems.projectItems) {
if (excludeList.includes(projectItem.project.number)) {
toolkit.core.info(`The project number ${projectItem.project.number} is on the excludeList. skipping...`);
continue;
}
const projectInfo = await getProjectInfo(toolkit, { number: projectItem.project.number });
const priorityField = projectInfo.fields.find((field) => field.name == "Priority");
if (!priorityField) {
toolkit.core.info(`${projectInfo.title} doesn't have a priority field. skipping...`);
continue;
}
const priorityOption = priorityField?.options.find((option) => option.name == priority);
if (!priorityOption) {
toolkit.core.info(`${projectInfo.title} doesn't have the priority option ${priority}. skipping...`);
continue;
}
toolkit.core.info(`Setting priority for issue ${issue.number} on ${projectInfo.title} to ${priority}`);
await setFieldValue(toolkit, {
projectId: projectInfo.node_id,
itemId: projectItem.id,
fieldId: priorityField.id,
valueId: priorityOption.id
});
}
}
async function createDocumentationTasksForProjects(toolkit, projectNumbers, organization = "shopware", documentationProjectId = 11806, description = null, comment = null) {
for (const projectNumber of projectNumbers) {
const projectId = await getProjectIdByNumber(toolkit, projectNumber, organization);
const epicsInProgress = await getEpicsInProgressByProject(toolkit, projectId);
for (const epic of epicsInProgress) {
if (await hasDocIssueComment(toolkit, epic.id)) {
toolkit.core.info(`Skipping issue ${epic.url} because it already has a documentation issue reference.`);
continue;
}
const docTask = await createDocumentationTask(toolkit, epic, documentationProjectId, description);
await createDocIssueComment(toolkit, epic.id, docTask.key, comment);
}
}
}
async function markStaleIssues(toolkit, projectNumber, dryRun) {
const DAYS_UNTIL_STALE = 180;
const now = /* @__PURE__ */ new Date();
now.setDate(now.getDate() - DAYS_UNTIL_STALE);
const staleDate = now.toISOString().split("T")[0];
switch (Number(projectNumber)) {
case 27: {
const query = (
/* GraphQL */
`
query searchClosableIssues {
search(
type: ISSUE
first: 100
query: "repo:shopware/shopware is:issue state:open project:shopware/27 label:priority/low -label:AboutToClose -label:DoNotClose created:<=$staleDate"
) {
pageInfo {
hasNextPage
endCursor
}
edges {
node {
... on Issue {
id
title
number
url
parent {
issueType {
name
}
}
labels(first: 20) {
nodes {
name
}
}
projectItems(first: 10) {
nodes {
project {
number
}
fieldValueByName(name: "Status") {
... on ProjectV2ItemFieldSingleSelectValue {
name
}
}
}
}
}
}
}
}
}
`
);
const res = await toolkit.github.graphql(query.replace("$staleDate", staleDate), {
headers: {
"GraphQL-Features": "issue_types"
}
});
const issues = res.search.edges;
for (const issueNode of issues) {
const issue = issueNode.node;
const parentIssueType = issue.parent?.issueType.name;
const labels = issue.labels.nodes;
const priorityLabel = labels.find(
(label) => label.name.startsWith("priority/")
)?.name;
const statusInProject = issue.projectItems.nodes.find((projectItem) => projectItem.project.number == 27)?.fieldValueByName.name;
if ((priorityLabel && priorityLabel.split("/")[1] === "low" || statusInProject == "Backlog") && parentIssueType != "Epic") {
if (dryRun) {
toolkit.core.info(`Would set "${issue.title}" (${issue.url}) to AboutToClose`);
continue;
}
const aboutToCloseLabel = await getLabelByName(toolkit, "shopware", "AboutToClose");
if (!aboutToCloseLabel) {
throw Error("Couldn't find the AboutToClose label");
}
await addLabelToLabelable(toolkit, issue.id, aboutToCloseLabel.id);
}
}
break;
}
default:
throw new Error(`There is no query for the project with the number ${projectNumber}`);
}
}
async function closeStaleIssues(toolkit, dryRun) {
const DAYS_UNTIL_CLOSE = 30;
const now = /* @__PURE__ */ new Date();
now.setDate(now.getDate() - DAYS_UNTIL_CLOSE);
const closeDate = now.toISOString().split("T")[0];
const query = (
/* GraphQL */
`
query searchStaleIssues {
search(
type: ISSUE
first: 100
query: "repo:shopware/shopware is:issue state:open label:AboutToClose updated:<=$closeDate"
) {
pageInfo {
hasNextPage
endCursor
}
edges {
node {
... on Issue {
id
title
number
url
parent {
issueType {
name
}
}
labels(first: 20) {
nodes {
name
}
}
projectItems(first: 10) {
nodes {
fieldValueByName(name: "Status") {
... on ProjectV2ItemFieldSingleSelectValue {
name
}
}
}
}
}
}
}
}
}
`
);
const res = await toolkit.github.graphql(query.replace("$closeDate", closeDate), {
headers: {
"GraphQL-Features": "issue_types"
}
});
const issues = res.search.edges;
for (const issueNode of issues) {
const issue = issueNode.node;
if (dryRun) {
toolkit.core.info(`Would close "${issue.title}" (${issue.url})`);
continue;
}
await closeIssue(toolkit, issue.id);
}
}
async function manageOldPullRequests(toolkit, organization = "shopware", days = 7, close = false) {
const pullRequests = await getPullRequests(
toolkit,
`org:${organization} is:pr is:open draft:false updated:<${new Date(Date.now() - days * 24 * 60 * 60 * 1e3).toISOString()}`
);
const closeMsg = `This pull request has been closed automatically. If you would like to continue working on it, please feel free to re-open it!`;
for (const pr of pullRequests) {
const assignee = pr.assignees.nodes[0];
if (!assignee) {
toolkit.core.debug(`Pull request ${pr.repository.owner.login}/${pr.repository.name}#${pr.number} has no assignee, skipping.`);
continue;
}
const emails = await getVerifiedDomainEmails(toolkit, assignee.login, organization);
if (emails.length < 1) {
continue;
}
if (close) {
if (isDryRun()) {
toolkit.core.info(`[DRY_RUN] ${pr.url}`);
} else {
await closePullRequest(toolkit, pr.id);
await addComment(toolkit, pr.id, closeMsg);
}
}
}
}
async function sendSlackMessageForEMail(toolkit, emails, message) {
if (process.env.SLACK_TOKEN === void 0) {
throw new Error("Unauthorized. SLACK_TOKEN environment variable not provided.");
}
const slackClient = new SlackClient(process.env.SLACK_TOKEN);
for (const email of emails) {
if (!email || email.trim() === "") {
continue;
}
const userId = await slackClient.getUserByEmail(email);
if (!userId) {
continue;
}
await slackClient.sendIMToUser(userId, message);
return;
}
toolkit.core.warning(`No valid Slack user found for emails, can't send message.`);
}
export { SlackClient, addComment, addLabelToLabelable, addProjectItem, callJiraApi, cleanupBranches, cleanupNeedsTriage, closeIssue, closePullRequest, closeStaleIssues, createDocIssueComment, createDocumentationTask, createDocumentationTasksForProjects, docIssueReference, findIssueWithProjectItems, findPRWithProjectItems, findWithProjectItems, getCommentsForIssue, getDevelopmentIssueForPullRequest, getEpicsInProgressByProject, getIssuesByProject, getLabelByName, getOldBranches, getProjectIdByNumber, getProjectInfo, getPullRequests, getVerifiedDomainEmails, hasDocIssueComment, jiraBaseUrl, jiraHost, manageOldPullRequests, markStaleIssues, referencesDocIssue, sendSlackMessageForEMail, setFieldValue, setStatusInProjects, syncPriorities };
//# sourceMappingURL=index.mjs.map