Skip to content

Commit

Permalink
Created filters for comments and authors (#6)
Browse files Browse the repository at this point in the history
Added filters to ignore issues that have no comments or that come from a
particular author.

Abstracted the code for the filters to be in an external method, as the
evaluation of such filters.

This resolves #2 and resolves #3
  • Loading branch information
Bullrich authored Apr 2, 2023
1 parent 9e04807 commit 7761ca8
Show file tree
Hide file tree
Showing 8 changed files with 127 additions and 32 deletions.
33 changes: 32 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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:
Expand Down Expand Up @@ -160,7 +191,7 @@ on:
- cron: '0 9 * * 1'
jobs:
sync:
fetch-issues:
runs-on: ubuntu-latest
steps:
- name: Fetch issues from here
Expand Down
8 changes: 8 additions & 0 deletions action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
13 changes: 13 additions & 0 deletions src/filters.ts
Original file line number Diff line number Diff line change
@@ -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());
}
28 changes: 7 additions & 21 deletions src/github/issuesParser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<typeof GitHub>, daysStale: number, repo: Repo): Promise<IssueData[]> => {
export const fetchIssues = async (octokit: InstanceType<typeof GitHub>, repo: Repo): Promise<IssueData[]> => {
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);
}
43 changes: 37 additions & 6 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -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`;
Expand All @@ -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;

Expand All @@ -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,
Expand Down
25 changes: 25 additions & 0 deletions src/types/global.d.ts
Original file line number Diff line number Diff line change
@@ -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 { }
5 changes: 3 additions & 2 deletions tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,11 @@
"rootDir": "./src",
"strict": true,
"noImplicitAny": true,
"esModuleInterop": true
"esModuleInterop": true,
"typeRoots": ["./src/types"]
},
"exclude": [
"node_modules",
"**/*.test.ts"
]
],
}

0 comments on commit 7761ca8

Please sign in to comment.