diff --git a/src/helper/advanced.ts b/src/helper/advanced.ts index de49585..4e6d645 100644 --- a/src/helper/advanced.ts +++ b/src/helper/advanced.ts @@ -1,15 +1,105 @@ import { dealStringToArr } from 'actions-util'; +import dayjs from 'dayjs'; +import utc from 'dayjs/plugin/utc'; +import isSameOrBefore from 'dayjs/plugin/isSameOrBefore'; import * as core from '../core'; import { TIssueState, TUpdateMode, TEmoji, TLockReasons } from '../types'; import { ELockReasons } from '../shared'; -import { IIssueCoreEngine } from '../issue'; +import { IIssueCoreEngine, IListIssuesParams, TListIssuesResults } from '../issue'; +import { + doAddLabels, + doCreateComment, +} from './base'; let ICE: IIssueCoreEngine; - export function initAdvancedICE(_ICE: IIssueCoreEngine) { ICE = _ICE; } -export async function doCheckInactive() { +export async function doQueryIssues(state: TIssueState | 'all', creator?: string): Promise { + const params = { + state, + } as IListIssuesParams; + const issueCreator = core.getInput('issue-creator'); + const issueAssignee = core.getInput('issue-assignee'); + const issueMentioned = core.getInput('issue-mentioned'); + issueCreator ? (params.creator = issueCreator) : null; + issueAssignee ? (params.assignee = issueAssignee) : null; + issueMentioned ? (params.mentioned = issueMentioned) : null; + + const labels = core.getInput('labels'); + labels ? params.labels = labels : null; + + creator? params.creator = creator : null; + + const issuesList = await ICE.listIssues(params); + const issues: TListIssuesResults = []; + const issueNumbers: number[] = []; + + if (issuesList.length) { + const excludeLabels = core.getInput('exclude-labels') || ''; + const bodyIncludes = core.getInput('body-includes'); + const titleIncludes = core.getInput('title-includes'); + + const excludeLabelsArr = dealStringToArr(excludeLabels); + issuesList.forEach(issue => { + const bodyCheck = bodyIncludes ? issue.body.includes(bodyIncludes) : true; + const titleCheck = titleIncludes ? issue.title.includes(titleIncludes) : true; + /** + * Note: GitHub's REST API v3 considers every pull request an issue, but not every issue is a pull request. + * For this reason, "Issues" endpoints may return both issues and pull requests in the response. + * You can identify pull requests by the pull_request key. + */ + if (bodyCheck && titleCheck && issue.pull_request === undefined) { + if (excludeLabelsArr.length) { + for (let i = 0; i < issue.labels.length; i += 1) { + if (excludeLabelsArr.includes(issue.labels[i].name)) return; + } + } + + const inactiveDay = core.getInput('inactive-day'); + if (inactiveDay) { + dayjs.extend(utc); + dayjs.extend(isSameOrBefore); + + const lastTime = dayjs.utc().subtract(+inactiveDay, 'day'); + const updateTime = dayjs.utc(issue.updated_at); + if (updateTime.isSameOrBefore(lastTime)) { + issues.push(issue); + issueNumbers.push(issue.number); + } + } else { + issues.push(issue); + issueNumbers.push(issue.number); + } + } + }); + } + + core.info(`[doQueryIssues] issueNumbers is [${JSON.stringify(issueNumbers)}]`); + return issues; +} + +export async function doCheckInactive(body: string, emoji?: string) { + let issueState = core.getInput('issue-state'); + if (issueState !== 'all' && issueState !== 'closed') { + issueState = 'open'; + } + const issues = await doQueryIssues(issueState as TIssueState | 'all'); + if (issues.length) { + const inactiveLabel = core.getInput('inactive-label') || 'inactive'; + for (const issue of issues) { + const { labels, number } = issue; + const labelNames = labels.map(({name}) => name); + if (!labelNames.includes(inactiveLabel)) { + await doAddLabels([inactiveLabel], number); + if (body) await doCreateComment(body, emoji, number); + } else { + core.info(`[doCheckInactive] The issue ${number} already has ${inactiveLabel} label!`); + } + } + } else { + core.info(`[doCheckInactive] Query issues empty!`); + } } diff --git a/src/helper/base.ts b/src/helper/base.ts index 278095d..c680ca9 100644 --- a/src/helper/base.ts +++ b/src/helper/base.ts @@ -5,7 +5,6 @@ import { ELockReasons } from '../shared'; import { IIssueCoreEngine } from '../issue'; let ICE: IIssueCoreEngine; - export function initBaseICE(_ICE: IIssueCoreEngine) { ICE = _ICE; } @@ -15,7 +14,8 @@ export async function doAddAssignees(assignees: string[]) { core.info(`[doAddAssignees] [${assignees}] success!`); } -export async function doAddLabels(labels: string[]) { +export async function doAddLabels(labels: string[], issueNumber?: number) { + if (issueNumber) ICE.setIssueNumber(issueNumber); await ICE.addLabels(labels); core.info(`[doAddLabels] [${labels}] success!`); } @@ -25,8 +25,9 @@ export async function doCloseIssue(issueNumber: number) { core.info(`[doCloseIssue] [${issueNumber}] success!`); } -export async function doCreateComment(body: string, emoji?: string) { +export async function doCreateComment(body: string, emoji?: string, issueNumber?: number) { if (body) { + if (issueNumber) ICE.setIssueNumber(issueNumber); const commentId = await ICE.createComment(body); core.info(`[doCreateComment] [${body}] success!`); core.setOutput('comment-id', commentId); @@ -127,6 +128,9 @@ export async function doUpdateComment(_commentId: number | void, body: string, u if (commentId) { await ICE.updateComment(+commentId, body, updateMode); core.info(`[doUpdateComment] [${commentId}] success!`); + if (emoji) { + await doCreateCommentEmoji(+commentId, emoji); + } } else { core.warning(`[doUpdateComment] commentId is empty!`); } diff --git a/src/helper/helper.ts b/src/helper/helper.ts index 2de688b..f3a9724 100644 --- a/src/helper/helper.ts +++ b/src/helper/helper.ts @@ -193,7 +193,7 @@ export class IssueHelperEngine implements IIssueHelperEngine { // ^_^ ============= ^_^ // -[ Advanced Begin ]-> case 'check-inactive': { - await doCheckInactive(); + await doCheckInactive(body, emoji); break; } } diff --git a/src/issue/issue.ts b/src/issue/issue.ts index fe1e279..3bf2b70 100644 --- a/src/issue/issue.ts +++ b/src/issue/issue.ts @@ -1,7 +1,7 @@ import { Octokit } from '@octokit/rest'; import { TEmoji, TLockReasons, TUpdateMode, TIssueState } from '../types'; -import { IIssueBaseInfo, IIssueCoreEngine } from './types'; +import { IIssueBaseInfo, IIssueCoreEngine, IListIssuesParams, TListIssuesResults } from './types'; export class IssueCoreEngine implements IIssueCoreEngine { private owner!: string; @@ -124,6 +124,22 @@ export class IssueCoreEngine implements IIssueCoreEngine { }); } + public async listIssues(params: IListIssuesParams, page = 1) { + const { octokit, owner, repo } = this; + const { data } = await octokit.issues.listForRepo({ + ...params, + owner, + repo, + per_page: 100, + page, + }); + let issues = [...data] as unknown as TListIssuesResults; + if (issues.length >= 100) { + issues = issues.concat(await this.listIssues(params, page + 1)); + } + return issues; + } + public async lockIssue(lockReason: TLockReasons) { const { owner, repo, octokit, issueNumber } = this; const params: any = { @@ -165,7 +181,7 @@ export class IssueCoreEngine implements IIssueCoreEngine { issue_number: issueNumber, }); - const baseLabels = issue.data.labels.map(({ name }: any) => name); + const baseLabels: string[] = issue.data.labels.map(({ name }: any) => name); const removeLabels = baseLabels.filter(name => labels.includes(name)); for (const label of removeLabels) { @@ -190,7 +206,7 @@ export class IssueCoreEngine implements IIssueCoreEngine { issue_number: issueNumber, }); - const baseLabels = issue.data.labels.map(({ name }: any) => name); + const baseLabels: string[] = issue.data.labels.map(({ name }: any) => name); const removeLabels = baseLabels.filter(name => !labels.includes(name)); const addLabels = labels.filter(name => !baseLabels.includes(name)); @@ -257,12 +273,4 @@ export class IssueCoreEngine implements IIssueCoreEngine { assignees: assignees?.length ? assignees : baseAssignessName, }); } - - public async queryIssues() { - - } - - private async listIssues(params, page = 1) { - - } } diff --git a/src/issue/types.ts b/src/issue/types.ts index 90fa5af..dd15c61 100644 --- a/src/issue/types.ts +++ b/src/issue/types.ts @@ -8,13 +8,26 @@ export interface IIssueBaseInfo { } export interface IListIssuesParams { - owner: string; - repo: string; - state: 'all' | 'open' | 'closed'; - - + state: TIssueState | 'all'; + creator?: string; + assignee?: string; + mentioned?: string; + labels?: string; } +export type TListIssuesResult = { + number: number; + title: string; + body: string; + labels: { + name: string; + }[]; + updated_at: string; + pull_request?: any; +} + +export type TListIssuesResults = TListIssuesResult[]; + export interface IIssueCoreEngine { setIssueNumber(newIssueNumber: number): void; addAssignees(assignees: string[]): Promise; @@ -40,6 +53,7 @@ export interface IIssueCoreEngine { deleteComment(commentId: number): Promise; + listIssues(params: IListIssuesParams): Promise; lockIssue(lockReason: TLockReasons): Promise; openIssue(): Promise;