diff --git a/README.md b/README.md index 0add069..787834e 100644 --- a/README.md +++ b/README.md @@ -63,6 +63,13 @@ You can find all the inputs in [the action file](./action.yml) but let's walk th - **defaults** to the organization where this action is ran. - `days-stale`: Amount of days since the last activity for an issue to be considered *stale*. - **default**: 5 +- `noComments`: Boolean. If the action should only fetch issues that have 0 comments. + - Short for `Ignore issues that have comments`. + - **default**: false +- `ignoreAuthors`: Array of usernames that, if an issue was created by them, will be ignored. + - Short for `Ignore issues coming from these authors`. + - **optional** + - **Important**: If set, be sure to read the [Warning about authors field](#warning-about-authors-field) section. #### Accessing other repositories @@ -71,6 +78,30 @@ The action has the ability to access other repositories but if it can read it or The default `${{ github.token }}` variable has enough permissions to read the issues in **public repositories**. If you want this action to access to the issues in a private repository, then you will need a `Personal Access Token` with `repo` permissions. +### Warning about authors field +The authors field accepts an array or a single value, [but only with some particular format](https://github.com/actions/toolkit/issues/184#issuecomment-1198653452), so it is important to follow it. +It accepts either: +```yml +ignoreAuthors: username1 +``` +or an array of authors using a `pipe`: +```yml +ignoreAuthors: | + username1 + username2 + username3 +``` +It **does not** support the following type of arrays: +```yml +# not this one +ignoreAuthors: + - username1 + - username2 + +# also doesn't support this one +ignoreAuthors: ["username1", "username2"] +``` + ### Outputs Outputs are needed for your chained actions. If you want to use this information, remember to set an `id` field in the step so you can access it. You can find all the outputs in [the action file](./action.yml) but let's walk through each one of them: @@ -160,7 +191,7 @@ on: - cron: '0 9 * * 1' jobs: - sync: + fetch-issues: runs-on: ubuntu-latest steps: - name: Fetch issues from here diff --git a/action.yml b/action.yml index d0da8e5..4c6fa45 100644 --- a/action.yml +++ b/action.yml @@ -18,6 +18,14 @@ inputs: required: false description: How many days have to pass to consider an action "stale" default: '5' + noComments: + required: false + description: If true, it will only collect issues with NO comments. + default: false + ignoreAuthors: + required: false + description: array of usernames that should be ignored if they are the author of the issue. + type: string outputs: repo: description: 'The name of the repo in owner/repo pattern' diff --git a/package.json b/package.json index d7985a0..5750ad2 100644 --- a/package.json +++ b/package.json @@ -2,12 +2,12 @@ "name": "stale-issues-finder", "version": "0.0.1", "description": "Find what issues have been stale for a given time", - "main": "dist/index.js", + "main": "src/index.ts", "engines": { "node": ">=18.0.0" }, "scripts": { - "build": "ncc build src/index.ts" + "build": "ncc build" }, "repository": { "type": "git", diff --git a/src/filters.ts b/src/filters.ts new file mode 100644 index 0000000..58a2349 --- /dev/null +++ b/src/filters.ts @@ -0,0 +1,13 @@ +import moment from "moment"; + +export const olderThanDays = (issue: IssueData, daysStale: number): boolean => { + return moment().diff(moment(issue.updated_at), "days") > daysStale; +} + +export const byNoComments = (issue: IssueData): boolean => { + return issue.comments === 0; +} + +export const isNotFromAuthor = ({ user }: IssueData, authors: string[]): boolean => { + return authors.some(author => author.toLowerCase() === user?.login.toLowerCase()); +} diff --git a/src/github/issuesParser.ts b/src/github/issuesParser.ts index c0fb677..468c884 100644 --- a/src/github/issuesParser.ts +++ b/src/github/issuesParser.ts @@ -2,32 +2,18 @@ import { debug } from "@actions/core"; import { GitHub } from "@actions/github/lib/utils"; import moment from "moment"; -export interface IssueData { - html_url: string; - title: string; - created_at: string; - updated_at: string; - number: number; -} - -interface Repo { - owner: string, - repo: string; -} - -export const fetchIssues = async (octokit: InstanceType, daysStale: number, repo: Repo): Promise => { +export const fetchIssues = async (octokit: InstanceType, repo: Repo): Promise => { const issues = await octokit.rest.issues.listForRepo({ ...repo, per_page: 100, state: "open" }); debug(`Found elements ${issues.data.length}`); - // filter old actions - const filtered = issues.data.filter(od => moment().diff(moment(od.updated_at), "days") > daysStale); - if (filtered.length < 1) { - return [] - } - // order them from stalest to most recent - const orderedDates = filtered.sort((a, b) => { + const orderedDates = issues.data.sort((a, b) => { return b.updated_at > a.updated_at ? -1 : b.updated_at < a.updated_at ? 1 : 0 }); + return orderedDates; } + +export const filterByDays = (issues: IssueData[], daysStale: number) => { + return issues.filter(issue => moment().diff(moment(issue.updated_at), "days") > daysStale); +} diff --git a/src/index.ts b/src/index.ts index 3f9117d..6900302 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,13 +1,27 @@ -import { getInput, info, setOutput } from "@actions/core"; +import { getBooleanInput, getInput, getMultilineInput, info, setOutput } from "@actions/core"; import { context, getOctokit } from "@actions/github"; import { Context } from "@actions/github/lib/context"; import moment from "moment"; -import { fetchIssues, IssueData } from "./github/issuesParser"; +import { byNoComments, isNotFromAuthor, olderThanDays } from "./filters"; +import { fetchIssues } from "./github/issuesParser"; const daysSinceDate = (date: string): number => { return moment().diff(moment(date), 'days') } +const getFiltersFromInput = (): Filters => { + const inputDays = Number.parseInt(getInput("days-stale", { required: false })); + const daysStale = isNaN(inputDays) ? 5 : inputDays; + + const noComments = !!getInput("noComments") ? getBooleanInput("noComments") : false; + + const ignoreAuthors = getMultilineInput("ignoreAuthors"); + + return { + daysStale, noComments, notFromAuthor: ignoreAuthors + } +} + const generateMarkdownMessage = (issues: IssueData[], repo: { owner: string, repo: string; }) => { const messages = issues.map(issue => { return ` - [${issue.title}](${issue.html_url}) - Stale for ${daysSinceDate(issue.updated_at)} days`; @@ -30,14 +44,29 @@ const getRepo = (ctx: Context): { owner: string, repo: string } => { return { repo, owner }; } +const filterIssues = (issues: IssueData[], filters: Filters) => { + let filteredData = issues; + if (filters.daysStale) { + filteredData = filteredData.filter(is => olderThanDays(is, filters.daysStale)); + } + if (filters.noComments) { + filteredData = filteredData.filter(byNoComments); + } + if (filters.notFromAuthor && filters.notFromAuthor.length > 0) { + filteredData = filteredData.filter(is => isNotFromAuthor(is, filters.notFromAuthor)); + } + + return filteredData; +} + const runAction = async (ctx: Context) => { const repo = getRepo(ctx); const token = getInput("GITHUB_TOKEN", { required: true }); - const inputDays = Number.parseInt(getInput("days-stale", { required: false })); - const daysStale = isNaN(inputDays) ? 5 : inputDays; + + const filters = getFiltersFromInput(); const octokit = getOctokit(token); - const staleIssues = await fetchIssues(octokit, daysStale, repo); + const staleIssues = await fetchIssues(octokit, repo); const amountOfStaleIssues = staleIssues.length; @@ -46,7 +75,9 @@ const runAction = async (ctx: Context) => { setOutput("stale", amountOfStaleIssues); if (amountOfStaleIssues > 0) { - const cleanedData = staleIssues.map(issue => { + const filteredData = filterIssues(staleIssues, filters); + + let cleanedData = filteredData.map(issue => { return { url: issue.html_url, title: issue.title, diff --git a/src/types/global.d.ts b/src/types/global.d.ts new file mode 100644 index 0000000..cf7a01f --- /dev/null +++ b/src/types/global.d.ts @@ -0,0 +1,25 @@ +declare global { + interface IssueData { + html_url: string; + title: string; + created_at: string; + updated_at: string; + number: number; + comments: number; + /** If user was deleted it is going to be null */ + user: { login: string } | null + } + + interface Repo { + owner: string, + repo: string; + } + + interface Filters { + noComments?: boolean; + daysStale: number; + notFromAuthor: string[]; + } +} + +export { } diff --git a/tsconfig.json b/tsconfig.json index a09a8ee..993566a 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -6,10 +6,11 @@ "rootDir": "./src", "strict": true, "noImplicitAny": true, - "esModuleInterop": true + "esModuleInterop": true, + "typeRoots": ["./src/types"] }, "exclude": [ "node_modules", "**/*.test.ts" - ] + ], }