Add a start date option to ignore old issues and PRs (#269)

* docs(readme): add a small precision about the operations-per-run

closes #230

* chore(lint): ignore the lib folder for prettier

* chore(date): add a function to check if a date is valid

* chore(date): add a function to get a humanized date

* chore(date): add a function to check if the date is more recent than

* feat(date): add a start date to ignore old issues and PRs

closes #174

* docs(readme): change the date to match the description

* chore(date): add a better type for the date

* docs(date): add missing JSDoc about the return type

* chore(rebase): fix issues due to rebase

* docs(readme): fix table formatting issues
This commit is contained in:
Geoffrey Testelin 2021-01-18 02:22:36 +01:00 committed by GitHub
parent 7f340a46f3
commit f698371c0d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
19 changed files with 694 additions and 69 deletions

View File

@ -2,5 +2,6 @@
.licenses .licenses
.vscode .vscode
dist dist
lib
node_modules node_modules
package-lock.json package-lock.json

View File

@ -24,32 +24,33 @@ $ npm test
### Arguments ### Arguments
| Input | Description | Usage | | Input | Description | Usage |
| --------------------------- | ------------------------------------------------------------------------------------ | -------- | | --------------------------- | -------------------------------------------------------------------------------------------- | -------- |
| `repo-token` | PAT(Personal Access Token) for authorizing repository. | Optional | | `repo-token` | PAT(Personal Access Token) for authorizing repository. _Defaults to **${{ github.token }}**_ | Optional |
| `days-before-stale` | Idle number of days before marking an issue/pr as stale. _Defaults to **60**_ | Optional | | `days-before-stale` | Idle number of days before marking an issue/pr as stale. _Defaults to **60**_ | Optional |
| `days-before-issue-stale` | Idle number of days before marking an issue as stale (override `days-before-stale`). | Optional | | `days-before-issue-stale` | Idle number of days before marking an issue as stale (override `days-before-stale`). | Optional |
| `days-before-pr-stale` | Idle number of days before marking an pr as stale (override `days-before-stale`). | Optional | | `days-before-pr-stale` | Idle number of days before marking an pr as stale (override `days-before-stale`). | Optional |
| `days-before-close` | Idle number of days before closing an stale issue/pr. _Defaults to **7**_ | Optional | | `days-before-close` | Idle number of days before closing an stale issue/pr. _Defaults to **7**_ | Optional |
| `days-before-issue-close` | Idle number of days before closing an stale issue (override `days-before-close`). | Optional | | `days-before-issue-close` | Idle number of days before closing an stale issue (override `days-before-close`). | Optional |
| `days-before-pr-close` | Idle number of days before closing an stale pr (override `days-before-close`). | Optional | | `days-before-pr-close` | Idle number of days before closing an stale pr (override `days-before-close`). | Optional |
| `stale-issue-message` | Message to post on the stale issue. | Optional | | `stale-issue-message` | Message to post on the stale issue. | Optional |
| `stale-pr-message` | Message to post on the stale pr. | Optional | | `stale-pr-message` | Message to post on the stale pr. | Optional |
| `close-issue-message` | Message to post on the stale issue while closing it. | Optional | | `close-issue-message` | Message to post on the stale issue while closing it. | Optional |
| `close-pr-message` | Message to post on the stale pr while closing it. | Optional | | `close-pr-message` | Message to post on the stale pr while closing it. | Optional |
| `stale-issue-label` | Label to apply on the stale issue. _Defaults to **stale**_ | Optional | | `stale-issue-label` | Label to apply on the stale issue. _Defaults to **stale**_ | Optional |
| `close-issue-label` | Label to apply on closing issue. | Optional | | `close-issue-label` | Label to apply on closing issue. | Optional |
| `stale-pr-label` | Label to apply on the stale pr. | Optional | | `stale-pr-label` | Label to apply on the stale pr. | Optional |
| `close-pr-label` | Label to apply on the closing pr. | Optional | | `close-pr-label` | Label to apply on the closing pr. | Optional |
| `exempt-issue-labels` | Labels on an issue exempted from being marked as stale. | Optional | | `exempt-issue-labels` | Labels on an issue exempted from being marked as stale. | Optional |
| `exempt-pr-labels` | Labels on the pr exempted from being marked as stale. | Optional | | `exempt-pr-labels` | Labels on the pr exempted from being marked as stale. | Optional |
| `only-labels` | Only labels checked for stale issue/pr. | Optional | | `only-labels` | Only labels checked for stale issue/pr. | Optional |
| `operations-per-run` | Maximum number of operations per run. _Defaults to **30**_ | Optional | | `operations-per-run` | Maximum number of operations per run (GitHub API CRUD related). _Defaults to **30**_ | Optional |
| `remove-stale-when-updated` | Remove stale label from issue/pr on updates or comments. _Defaults to **true**_ | Optional | | `remove-stale-when-updated` | Remove stale label from issue/pr on updates or comments. _Defaults to **true**_ | Optional |
| `debug-only` | Dry-run on action. _Defaults to **false**_ | Optional | | `debug-only` | Dry-run on action. _Defaults to **false**_ | Optional |
| `ascending` | Order to get issues/pr. _Defaults to **false**_ | Optional | | `ascending` | Order to get issues/pr. _Defaults to **false**_ | Optional |
| `skip-stale-issue-message` | Skip adding stale message on stale issue. _Defaults to **false**_ | Optional | | `skip-stale-issue-message` | Skip adding stale message on stale issue. _Defaults to **false**_ | Optional |
| `skip-stale-pr-message` | Skip adding stale message on stale pr. _Defaults to **false**_ | Optional | | `skip-stale-pr-message` | Skip adding stale message on stale pr. _Defaults to **false**_ | Optional |
| `start-date` | The date used to skip the stale action on issue/pr created before it (ISO 8601 or RFC 2822). | Optional |
### Usage ### Usage
@ -163,6 +164,23 @@ jobs:
only-labels: 'awaiting-feedback,awaiting-answers' only-labels: 'awaiting-feedback,awaiting-answers'
``` ```
Configure the stale action to only stale issue/pr created after the 18th april 2020:
```yaml
name: 'Close stale issues and PRs'
on:
schedule:
- cron: '30 1 * * *'
jobs:
stale:
runs-on: ubuntu-latest
steps:
- uses: actions/stale@v3
with:
start-date: '2020-18-04T00:00:00Z' // ISO 8601 or RFC 2822
```
### Debugging ### Debugging
To see debug output from this action, you must set the secret `ACTIONS_STEP_DEBUG` to `true` in your repository. You can run this action in debug only mode (no actions will be taken on your issues) by passing `debug-only` `true` as an argument to the action. To see debug output from this action, you must set the secret `ACTIONS_STEP_DEBUG` to `true` in your repository. You can run this action in debug only mode (no actions will be taken on your issues) by passing `debug-only` `true` as an argument to the action.

View File

@ -1,15 +1,16 @@
import * as github from '@actions/github'; import * as github from '@actions/github';
import { import {
Issue, Issue,
IssueProcessor, IssueProcessor,
IssueProcessorOptions IssueProcessorOptions
} from '../src/IssueProcessor'; } from '../src/IssueProcessor';
import {IsoDateString} from '../src/types/iso-date-string';
function generateIssue( function generateIssue(
id: number, id: number,
title: string, title: string,
updatedAt: string, updatedAt: IsoDateString,
createdAt: IsoDateString = updatedAt,
isPullRequest: boolean = false, isPullRequest: boolean = false,
labels: string[] = [], labels: string[] = [],
isClosed: boolean = false, isClosed: boolean = false,
@ -21,6 +22,7 @@ function generateIssue(
return {name: l}; return {name: l};
}), }),
title: title, title: title,
created_at: createdAt,
updated_at: updatedAt, updated_at: updatedAt,
pull_request: isPullRequest ? {} : null, pull_request: isPullRequest ? {} : null,
state: isClosed ? 'closed' : 'open', state: isClosed ? 'closed' : 'open',
@ -53,7 +55,8 @@ const DefaultProcessorOptions: IssueProcessorOptions = Object.freeze({
ascending: false, ascending: false,
skipStaleIssueMessage: false, skipStaleIssueMessage: false,
skipStalePrMessage: false, skipStalePrMessage: false,
deleteBranch: false deleteBranch: false,
startDate: ''
}); });
test('empty issue list results in 1 operation', async () => { test('empty issue list results in 1 operation', async () => {
@ -97,6 +100,254 @@ test('processing an issue with no label will make it stale and close it, if it i
expect(processor.closedIssues.length).toEqual(1); expect(processor.closedIssues.length).toEqual(1);
}); });
test('processing an issue with no label and a start date as ECMAScript epoch in seconds being before the issue creation date will not make it stale nor close it when it is old enough and days-before-close is set to 0', async () => {
expect.assertions(2);
const TestIssueList: Issue[] = [
generateIssue(
1,
'An issue with no label',
'2020-01-01T17:00:00Z',
'2020-01-01T17:00:00Z'
)
];
const january2000 = 946681200000;
const opts: IssueProcessorOptions = {
...DefaultProcessorOptions,
daysBeforeClose: 0,
startDate: january2000.toString()
};
const processor = new IssueProcessor(
opts,
async () => 'abot',
async p => (p == 1 ? TestIssueList : []),
async (num, dt) => [],
async (issue, label) => new Date().toDateString()
);
// process our fake issue list
await processor.processIssues(1);
expect(processor.staleIssues.length).toStrictEqual(0);
expect(processor.closedIssues.length).toStrictEqual(0);
});
test('processing an issue with no label and a start date as ECMAScript epoch in seconds being after the issue creation date will not make it stale nor close it when it is old enough and days-before-close is set to 0', async () => {
expect.assertions(2);
const TestIssueList: Issue[] = [
generateIssue(
1,
'An issue with no label',
'2020-01-01T17:00:00Z',
'2020-01-01T17:00:00Z'
)
];
const january2021 = 1609455600000;
const opts: IssueProcessorOptions = {
...DefaultProcessorOptions,
daysBeforeClose: 0,
startDate: january2021.toString()
};
const processor = new IssueProcessor(
opts,
async () => 'abot',
async p => (p == 1 ? TestIssueList : []),
async (num, dt) => [],
async (issue, label) => new Date().toDateString()
);
// process our fake issue list
await processor.processIssues(1);
expect(processor.staleIssues.length).toStrictEqual(0);
expect(processor.closedIssues.length).toStrictEqual(0);
});
test('processing an issue with no label and a start date as ECMAScript epoch in milliseconds being before the issue creation date will not make it stale nor close it when it is old enough and days-before-close is set to 0', async () => {
expect.assertions(2);
const TestIssueList: Issue[] = [
generateIssue(
1,
'An issue with no label',
'2020-01-01T17:00:00Z',
'2020-01-01T17:00:00Z'
)
];
const january2000 = 946681200000000;
const opts: IssueProcessorOptions = {
...DefaultProcessorOptions,
daysBeforeClose: 0,
startDate: january2000.toString()
};
const processor = new IssueProcessor(
opts,
async () => 'abot',
async p => (p == 1 ? TestIssueList : []),
async (num, dt) => [],
async (issue, label) => new Date().toDateString()
);
// process our fake issue list
await processor.processIssues(1);
expect(processor.staleIssues.length).toStrictEqual(0);
expect(processor.closedIssues.length).toStrictEqual(0);
});
test('processing an issue with no label and a start date as ECMAScript epoch in milliseconds being after the issue creation date will not make it stale nor close it when it is old enough and days-before-close is set to 0', async () => {
expect.assertions(2);
const TestIssueList: Issue[] = [
generateIssue(
1,
'An issue with no label',
'2020-01-01T17:00:00Z',
'2020-01-01T17:00:00Z'
)
];
const january2021 = 1609455600000;
const opts: IssueProcessorOptions = {
...DefaultProcessorOptions,
daysBeforeClose: 0,
startDate: january2021.toString()
};
const processor = new IssueProcessor(
opts,
async () => 'abot',
async p => (p == 1 ? TestIssueList : []),
async (num, dt) => [],
async (issue, label) => new Date().toDateString()
);
// process our fake issue list
await processor.processIssues(1);
expect(processor.staleIssues.length).toStrictEqual(0);
expect(processor.closedIssues.length).toStrictEqual(0);
});
test('processing an issue with no label and a start date as ISO 8601 being before the issue creation date will make it stale and close it when it is old enough and days-before-close is set to 0', async () => {
expect.assertions(2);
const TestIssueList: Issue[] = [
generateIssue(
1,
'An issue with no label',
'2020-01-01T17:00:00Z',
'2020-01-01T17:00:00Z'
)
];
const january2000 = '2000-01-01T00:00:00Z';
const opts: IssueProcessorOptions = {
...DefaultProcessorOptions,
daysBeforeClose: 0,
startDate: january2000.toString()
};
const processor = new IssueProcessor(
opts,
async () => 'abot',
async p => (p == 1 ? TestIssueList : []),
async (num, dt) => [],
async (issue, label) => new Date().toDateString()
);
// process our fake issue list
await processor.processIssues(1);
expect(processor.staleIssues.length).toStrictEqual(1);
expect(processor.closedIssues.length).toStrictEqual(1);
});
test('processing an issue with no label and a start date as ISO 8601 being after the issue creation date will not make it stale nor close it when it is old enough and days-before-close is set to 0', async () => {
expect.assertions(2);
const TestIssueList: Issue[] = [
generateIssue(
1,
'An issue with no label',
'2020-01-01T17:00:00Z',
'2020-01-01T17:00:00Z'
)
];
const january2021 = '2021-01-01T00:00:00Z';
const opts: IssueProcessorOptions = {
...DefaultProcessorOptions,
daysBeforeClose: 0,
startDate: january2021.toString()
};
const processor = new IssueProcessor(
opts,
async () => 'abot',
async p => (p == 1 ? TestIssueList : []),
async (num, dt) => [],
async (issue, label) => new Date().toDateString()
);
// process our fake issue list
await processor.processIssues(1);
expect(processor.staleIssues.length).toStrictEqual(0);
expect(processor.closedIssues.length).toStrictEqual(0);
});
test('processing an issue with no label and a start date as RFC 2822 being before the issue creation date will make it stale and close it when it is old enough and days-before-close is set to 0', async () => {
expect.assertions(2);
const TestIssueList: Issue[] = [
generateIssue(
1,
'An issue with no label',
'2020-01-01T17:00:00Z',
'2020-01-01T17:00:00Z'
)
];
const january2000 = 'January 1, 2000 00:00:00';
const opts: IssueProcessorOptions = {
...DefaultProcessorOptions,
daysBeforeClose: 0,
startDate: january2000.toString()
};
const processor = new IssueProcessor(
opts,
async () => 'abot',
async p => (p == 1 ? TestIssueList : []),
async (num, dt) => [],
async (issue, label) => new Date().toDateString()
);
// process our fake issue list
await processor.processIssues(1);
expect(processor.staleIssues.length).toStrictEqual(1);
expect(processor.closedIssues.length).toStrictEqual(1);
});
test('processing an issue with no label and a start date as RFC 2822 being after the issue creation date will not make it stale nor close it when it is old enough and days-before-close is set to 0', async () => {
expect.assertions(2);
const TestIssueList: Issue[] = [
generateIssue(
1,
'An issue with no label',
'2020-01-01T17:00:00Z',
'2020-01-01T17:00:00Z'
)
];
const january2021 = 'January 1, 2021 00:00:00';
const opts: IssueProcessorOptions = {
...DefaultProcessorOptions,
daysBeforeClose: 0,
startDate: january2021.toString()
};
const processor = new IssueProcessor(
opts,
async () => 'abot',
async p => (p == 1 ? TestIssueList : []),
async (num, dt) => [],
async (issue, label) => new Date().toDateString()
);
// process our fake issue list
await processor.processIssues(1);
expect(processor.staleIssues.length).toStrictEqual(0);
expect(processor.closedIssues.length).toStrictEqual(0);
});
test('processing an issue with no label will make it stale and close it, if it is old enough only if days-before-close is set to > 0 and days-before-issue-close is set to 0', async () => { test('processing an issue with no label will make it stale and close it, if it is old enough only if days-before-close is set to > 0 and days-before-issue-close is set to 0', async () => {
const TestIssueList: Issue[] = [ const TestIssueList: Issue[] = [
generateIssue(1, 'An issue with no label', '2020-01-01T17:00:00Z') generateIssue(1, 'An issue with no label', '2020-01-01T17:00:00Z')
@ -285,6 +536,7 @@ test('processing a stale issue will close it', async () => {
1, 1,
'A stale issue that should be closed', 'A stale issue that should be closed',
'2020-01-01T17:00:00Z', '2020-01-01T17:00:00Z',
'2020-01-01T17:00:00Z',
false, false,
['Stale'] ['Stale']
) )
@ -316,6 +568,7 @@ test('processing a stale issue containing a space in the label will close it', a
1, 1,
'A stale issue that should be closed', 'A stale issue that should be closed',
'2020-01-01T17:00:00Z', '2020-01-01T17:00:00Z',
'2020-01-01T17:00:00Z',
false, false,
['state: stale'] ['state: stale']
) )
@ -347,6 +600,7 @@ test('processing a stale issue containing a slash in the label will close it', a
1, 1,
'A stale issue that should be closed', 'A stale issue that should be closed',
'2020-01-01T17:00:00Z', '2020-01-01T17:00:00Z',
'2020-01-01T17:00:00Z',
false, false,
['lifecycle/stale'] ['lifecycle/stale']
) )
@ -378,6 +632,7 @@ test('processing a stale issue will close it when days-before-issue-stale overri
1, 1,
'A stale issue that should be closed', 'A stale issue that should be closed',
'2020-01-01T17:00:00Z', '2020-01-01T17:00:00Z',
'2020-01-01T17:00:00Z',
false, false,
['Stale'] ['Stale']
) )
@ -410,6 +665,7 @@ test('processing a stale PR will close it', async () => {
1, 1,
'A stale PR that should be closed', 'A stale PR that should be closed',
'2020-01-01T17:00:00Z', '2020-01-01T17:00:00Z',
'2020-01-01T17:00:00Z',
true, true,
['Stale'] ['Stale']
) )
@ -441,6 +697,7 @@ test('processing a stale PR will close it when days-before-pr-stale override day
1, 1,
'A stale PR that should be closed', 'A stale PR that should be closed',
'2020-01-01T17:00:00Z', '2020-01-01T17:00:00Z',
'2020-01-01T17:00:00Z',
true, true,
['Stale'] ['Stale']
) )
@ -469,9 +726,14 @@ test('processing a stale PR will close it when days-before-pr-stale override day
test('processing a stale issue will close it even if configured not to mark as stale', async () => { test('processing a stale issue will close it even if configured not to mark as stale', async () => {
const TestIssueList: Issue[] = [ const TestIssueList: Issue[] = [
generateIssue(1, 'An issue with no label', '2020-01-01T17:00:00Z', false, [ generateIssue(
'Stale' 1,
]) 'An issue with no label',
'2020-01-01T17:00:00Z',
'2020-01-01T17:00:00Z',
false,
['Stale']
)
]; ];
const opts = { const opts = {
@ -497,9 +759,14 @@ test('processing a stale issue will close it even if configured not to mark as s
test('processing a stale issue will close it even if configured not to mark as stale when days-before-issue-stale override days-before-stale', async () => { test('processing a stale issue will close it even if configured not to mark as stale when days-before-issue-stale override days-before-stale', async () => {
const TestIssueList: Issue[] = [ const TestIssueList: Issue[] = [
generateIssue(1, 'An issue with no label', '2020-01-01T17:00:00Z', false, [ generateIssue(
'Stale' 1,
]) 'An issue with no label',
'2020-01-01T17:00:00Z',
'2020-01-01T17:00:00Z',
false,
['Stale']
)
]; ];
const opts = { const opts = {
@ -526,9 +793,14 @@ test('processing a stale issue will close it even if configured not to mark as s
test('processing a stale PR will close it even if configured not to mark as stale', async () => { test('processing a stale PR will close it even if configured not to mark as stale', async () => {
const TestIssueList: Issue[] = [ const TestIssueList: Issue[] = [
generateIssue(1, 'An issue with no label', '2020-01-01T17:00:00Z', true, [ generateIssue(
'Stale' 1,
]) 'An issue with no label',
'2020-01-01T17:00:00Z',
'2020-01-01T17:00:00Z',
true,
['Stale']
)
]; ];
const opts = { const opts = {
@ -554,9 +826,14 @@ test('processing a stale PR will close it even if configured not to mark as stal
test('processing a stale PR will close it even if configured not to mark as stale when days-before-pr-stale override days-before-stale', async () => { test('processing a stale PR will close it even if configured not to mark as stale when days-before-pr-stale override days-before-stale', async () => {
const TestIssueList: Issue[] = [ const TestIssueList: Issue[] = [
generateIssue(1, 'An issue with no label', '2020-01-01T17:00:00Z', true, [ generateIssue(
'Stale' 1,
]) 'An issue with no label',
'2020-01-01T17:00:00Z',
'2020-01-01T17:00:00Z',
true,
['Stale']
)
]; ];
const opts = { const opts = {
@ -587,6 +864,7 @@ test('closed issues will not be marked stale', async () => {
1, 1,
'A closed issue that will not be marked', 'A closed issue that will not be marked',
'2020-01-01T17:00:00Z', '2020-01-01T17:00:00Z',
'2020-01-01T17:00:00Z',
false, false,
[], [],
true true
@ -613,6 +891,7 @@ test('stale closed issues will not be closed', async () => {
1, 1,
'A stale closed issue', 'A stale closed issue',
'2020-01-01T17:00:00Z', '2020-01-01T17:00:00Z',
'2020-01-01T17:00:00Z',
false, false,
['Stale'], ['Stale'],
true true
@ -640,6 +919,7 @@ test('closed prs will not be marked stale', async () => {
1, 1,
'A closed PR that will not be marked', 'A closed PR that will not be marked',
'2020-01-01T17:00:00Z', '2020-01-01T17:00:00Z',
'2020-01-01T17:00:00Z',
true, true,
[], [],
true true
@ -667,6 +947,7 @@ test('stale closed prs will not be closed', async () => {
1, 1,
'A stale closed PR that will not be closed again', 'A stale closed PR that will not be closed again',
'2020-01-01T17:00:00Z', '2020-01-01T17:00:00Z',
'2020-01-01T17:00:00Z',
true, true,
['Stale'], ['Stale'],
true true
@ -694,6 +975,7 @@ test('locked issues will not be marked stale', async () => {
1, 1,
'A locked issue that will not be stale', 'A locked issue that will not be stale',
'2020-01-01T17:00:00Z', '2020-01-01T17:00:00Z',
'2020-01-01T17:00:00Z',
false, false,
[], [],
false, false,
@ -720,6 +1002,7 @@ test('stale locked issues will not be closed', async () => {
1, 1,
'A stale locked issue that will not be closed', 'A stale locked issue that will not be closed',
'2020-01-01T17:00:00Z', '2020-01-01T17:00:00Z',
'2020-01-01T17:00:00Z',
false, false,
['Stale'], ['Stale'],
false, false,
@ -748,6 +1031,7 @@ test('locked prs will not be marked stale', async () => {
1, 1,
'A locked PR that will not be marked stale', 'A locked PR that will not be marked stale',
'2020-01-01T17:00:00Z', '2020-01-01T17:00:00Z',
'2020-01-01T17:00:00Z',
true, true,
[], [],
false, false,
@ -774,6 +1058,7 @@ test('stale locked prs will not be closed', async () => {
1, 1,
'A stale locked PR that will not be closed', 'A stale locked PR that will not be closed',
'2020-01-01T17:00:00Z', '2020-01-01T17:00:00Z',
'2020-01-01T17:00:00Z',
true, true,
['Stale'], ['Stale'],
false, false,
@ -799,9 +1084,14 @@ test('stale locked prs will not be closed', async () => {
test('exempt issue labels will not be marked stale', async () => { test('exempt issue labels will not be marked stale', async () => {
expect.assertions(3); expect.assertions(3);
const TestIssueList: Issue[] = [ const TestIssueList: Issue[] = [
generateIssue(1, 'My first issue', '2020-01-01T17:00:00Z', false, [ generateIssue(
'Exempt' 1,
]) 'My first issue',
'2020-01-01T17:00:00Z',
'2020-01-01T17:00:00Z',
false,
['Exempt']
)
]; ];
const opts = {...DefaultProcessorOptions}; const opts = {...DefaultProcessorOptions};
@ -825,7 +1115,14 @@ test('exempt issue labels will not be marked stale', async () => {
test('exempt issue labels will not be marked stale (multi issue label with spaces)', async () => { test('exempt issue labels will not be marked stale (multi issue label with spaces)', async () => {
const TestIssueList: Issue[] = [ const TestIssueList: Issue[] = [
generateIssue(1, 'My first issue', '2020-01-01T17:00:00Z', false, ['Cool']) generateIssue(
1,
'My first issue',
'2020-01-01T17:00:00Z',
'2020-01-01T17:00:00Z',
false,
['Cool']
)
]; ];
const opts = {...DefaultProcessorOptions}; const opts = {...DefaultProcessorOptions};
@ -848,7 +1145,14 @@ test('exempt issue labels will not be marked stale (multi issue label with space
test('exempt issue labels will not be marked stale (multi issue label)', async () => { test('exempt issue labels will not be marked stale (multi issue label)', async () => {
const TestIssueList: Issue[] = [ const TestIssueList: Issue[] = [
generateIssue(1, 'My first issue', '2020-01-01T17:00:00Z', false, ['Cool']) generateIssue(
1,
'My first issue',
'2020-01-01T17:00:00Z',
'2020-01-01T17:00:00Z',
false,
['Cool']
)
]; ];
const opts = {...DefaultProcessorOptions}; const opts = {...DefaultProcessorOptions};
@ -872,9 +1176,29 @@ test('exempt issue labels will not be marked stale (multi issue label)', async (
test('exempt pr labels will not be marked stale', async () => { test('exempt pr labels will not be marked stale', async () => {
const TestIssueList: Issue[] = [ const TestIssueList: Issue[] = [
generateIssue(1, 'My first issue', '2020-01-01T17:00:00Z', false, ['Cool']), generateIssue(
generateIssue(2, 'My first PR', '2020-01-01T17:00:00Z', true, ['Cool']), 1,
generateIssue(3, 'Another issue', '2020-01-01T17:00:00Z', false) 'My first issue',
'2020-01-01T17:00:00Z',
'2020-01-01T17:00:00Z',
false,
['Cool']
),
generateIssue(
2,
'My first PR',
'2020-01-01T17:00:00Z',
'2020-01-01T17:00:00Z',
true,
['Cool']
),
generateIssue(
3,
'Another issue',
'2020-01-01T17:00:00Z',
'2020-01-01T17:00:00Z',
false
)
]; ];
const opts = {...DefaultProcessorOptions}; const opts = {...DefaultProcessorOptions};
@ -896,17 +1220,18 @@ test('exempt pr labels will not be marked stale', async () => {
test('exempt issue labels will not be marked stale and will remove the existing stale label', async () => { test('exempt issue labels will not be marked stale and will remove the existing stale label', async () => {
expect.assertions(3); expect.assertions(3);
const TestIssueList: Issue[] = [ const TestIssueList: Issue[] = [
generateIssue(1, 'My first issue', '2020-01-01T17:00:00Z', false, [ generateIssue(
'Exempt', 1,
'Stale' 'My first issue',
]) '2020-01-01T17:00:00Z',
'2020-01-01T17:00:00Z',
false,
['Exempt', 'Stale']
)
]; ];
const opts = {...DefaultProcessorOptions}; const opts = {...DefaultProcessorOptions};
opts.exemptIssueLabels = 'Exempt'; opts.exemptIssueLabels = 'Exempt';
const processor = new IssueProcessor( const processor = new IssueProcessor(
opts, opts,
async () => 'abot', async () => 'abot',
@ -932,11 +1257,30 @@ test('exempt issue labels will not be marked stale and will remove the existing
test('stale issues should not be closed if days is set to -1', async () => { test('stale issues should not be closed if days is set to -1', async () => {
const TestIssueList: Issue[] = [ const TestIssueList: Issue[] = [
generateIssue(1, 'My first issue', '2020-01-01T17:00:00Z', false, [ generateIssue(
'Stale' 1,
]), 'My first issue',
generateIssue(2, 'My first PR', '2020-01-01T17:00:00Z', true, ['Stale']), '2020-01-01T17:00:00Z',
generateIssue(3, 'Another issue', '2020-01-01T17:00:00Z', false, ['Stale']) '2020-01-01T17:00:00Z',
false,
['Stale']
),
generateIssue(
2,
'My first PR',
'2020-01-01T17:00:00Z',
'2020-01-01T17:00:00Z',
true,
['Stale']
),
generateIssue(
3,
'Another issue',
'2020-01-01T17:00:00Z',
'2020-01-01T17:00:00Z',
false,
['Stale']
)
]; ];
const opts = {...DefaultProcessorOptions}; const opts = {...DefaultProcessorOptions};
@ -963,6 +1307,7 @@ test('stale label should be removed if a comment was added to a stale issue', as
1, 1,
'An issue that should un-stale', 'An issue that should un-stale',
'2020-01-01T17:00:00Z', '2020-01-01T17:00:00Z',
'2020-01-01T17:00:00Z',
false, false,
['Stale'] ['Stale']
) )
@ -1001,6 +1346,7 @@ test('stale label should not be removed if a comment was added by the bot (and t
1, 1,
'An issue that should stay stale', 'An issue that should stay stale',
'2020-01-01T17:00:00Z', '2020-01-01T17:00:00Z',
'2020-01-01T17:00:00Z',
false, false,
['Stale'] ['Stale']
) )
@ -1038,6 +1384,7 @@ test('stale label containing a space should be removed if a comment was added to
1, 1,
'An issue that should un-stale', 'An issue that should un-stale',
'2020-01-01T17:00:00Z', '2020-01-01T17:00:00Z',
'2020-01-01T17:00:00Z',
false, false,
['stat: stale'] ['stat: stale']
) )
@ -1073,6 +1420,7 @@ test('stale issues should not be closed until after the closed number of days',
1, 1,
'An issue that should be marked stale but not closed', 'An issue that should be marked stale but not closed',
lastUpdate.toString(), lastUpdate.toString(),
lastUpdate.toString(),
false false
) )
]; ];
@ -1105,6 +1453,7 @@ test('stale issues should be closed if the closed nubmer of days (additive) is a
1, 1,
'An issue that should be stale and closed', 'An issue that should be stale and closed',
lastUpdate.toString(), lastUpdate.toString(),
lastUpdate.toString(),
false, false,
['Stale'] ['Stale']
) )
@ -1138,6 +1487,7 @@ test('stale issues should not be closed until after the closed number of days (l
1, 1,
'An issue that should be marked stale but not closed', 'An issue that should be marked stale but not closed',
lastUpdate.toString(), lastUpdate.toString(),
lastUpdate.toString(),
false false
) )
]; ];
@ -1170,6 +1520,7 @@ test('skips stale message on issues when skip-stale-issue-message is set', async
1, 1,
'An issue that should be marked stale but not closed', 'An issue that should be marked stale but not closed',
lastUpdate.toString(), lastUpdate.toString(),
lastUpdate.toString(),
false false
) )
]; ];
@ -1215,6 +1566,7 @@ test('skips stale message on prs when skip-stale-pr-message is set', async () =>
1, 1,
'An issue that should be marked stale but not closed', 'An issue that should be marked stale but not closed',
lastUpdate.toString(), lastUpdate.toString(),
lastUpdate.toString(),
true true
) )
]; ];
@ -1260,6 +1612,7 @@ test('not providing state takes precedence over skipStaleIssueMessage', async ()
1, 1,
'An issue that should be marked stale but not closed', 'An issue that should be marked stale but not closed',
lastUpdate.toString(), lastUpdate.toString(),
lastUpdate.toString(),
false false
) )
]; ];
@ -1294,6 +1647,7 @@ test('not providing stalePrMessage takes precedence over skipStalePrMessage', as
1, 1,
'An issue that should be marked stale but not closed', 'An issue that should be marked stale but not closed',
lastUpdate.toString(), lastUpdate.toString(),
lastUpdate.toString(),
true true
) )
]; ];
@ -1328,6 +1682,7 @@ test('git branch is deleted when option is enabled', async () => {
1, 1,
'An issue that should have its branch deleted', 'An issue that should have its branch deleted',
'2020-01-01T17:00:00Z', '2020-01-01T17:00:00Z',
'2020-01-01T17:00:00Z',
isPullRequest, isPullRequest,
['Stale'] ['Stale']
) )
@ -1357,6 +1712,7 @@ test('git branch is not deleted when issue is not pull request', async () => {
1, 1,
'An issue that should not have its branch deleted', 'An issue that should not have its branch deleted',
'2020-01-01T17:00:00Z', '2020-01-01T17:00:00Z',
'2020-01-01T17:00:00Z',
isPullRequest, isPullRequest,
['Stale'] ['Stale']
) )

View File

@ -4,6 +4,7 @@ author: 'GitHub'
inputs: inputs:
repo-token: repo-token:
description: 'Token for the repository. Can be passed in using `{{ secrets.GITHUB_TOKEN }}`.' description: 'Token for the repository. Can be passed in using `{{ secrets.GITHUB_TOKEN }}`.'
required: false
default: ${{ github.token }} default: ${{ github.token }}
stale-issue-message: stale-issue-message:
description: 'The message to post on the issue when tagging it. If none provided, will not mark issues stale.' description: 'The message to post on the issue when tagging it. If none provided, will not mark issues stale.'
@ -64,7 +65,7 @@ inputs:
default: '' default: ''
required: false required: false
operations-per-run: operations-per-run:
description: 'The maximum number of operations per run, used to control rate limiting.' description: 'The maximum number of operations per run, used to control rate limiting (GitHub API CRUD related).'
default: '30' default: '30'
required: false required: false
remove-stale-when-updated: remove-stale-when-updated:
@ -91,6 +92,10 @@ inputs:
description: 'Delete the git branch after closing a stale pull request.' description: 'Delete the git branch after closing a stale pull request.'
default: 'false' default: 'false'
required: false required: false
start-date:
description: 'The date used to skip the stale action on issue/pr created before it (ISO 8601 or RFC 2822).'
default: ''
required: false
runs: runs:
using: 'node12' using: 'node12'
main: 'dist/index.js' main: 'dist/index.js'

View File

@ -1,7 +1,10 @@
import {context, getOctokit} from '@actions/github'; import {context, getOctokit} from '@actions/github';
import {GitHub} from '@actions/github/lib/utils'; import {GitHub} from '@actions/github/lib/utils';
import {GetResponseTypeFromEndpointMethod} from '@octokit/types'; import {GetResponseTypeFromEndpointMethod} from '@octokit/types';
import {IssueType} from './enums/issue-type.enum'; import {IssueType} from './enums/issue-type';
import {getHumanizedDate} from './functions/dates/get-humanized-date';
import {isDateMoreRecentThan} from './functions/dates/is-date-more-recent-than';
import {isValidDate} from './functions/dates/is-valid-date';
import {getIssueType} from './functions/get-issue-type'; import {getIssueType} from './functions/get-issue-type';
import {IssueLogger} from './classes/issue-logger'; import {IssueLogger} from './classes/issue-logger';
import {Logger} from './classes/logger'; import {Logger} from './classes/logger';
@ -9,11 +12,14 @@ import {isLabeled} from './functions/is-labeled';
import {isPullRequest} from './functions/is-pull-request'; import {isPullRequest} from './functions/is-pull-request';
import {labelsToList} from './functions/labels-to-list'; import {labelsToList} from './functions/labels-to-list';
import {shouldMarkWhenStale} from './functions/should-mark-when-stale'; import {shouldMarkWhenStale} from './functions/should-mark-when-stale';
import {IsoDateString} from './types/iso-date-string';
import {IsoOrRfcDateString} from './types/iso-or-rfc-date-string';
export interface Issue { export interface Issue {
title: string; title: string;
number: number; number: number;
updated_at: string; created_at: IsoDateString;
updated_at: IsoDateString;
labels: Label[]; labels: Label[];
pull_request: any; pull_request: any;
state: string; state: string;
@ -72,6 +78,7 @@ export interface IssueProcessorOptions {
skipStaleIssueMessage: boolean; skipStaleIssueMessage: boolean;
skipStalePrMessage: boolean; skipStalePrMessage: boolean;
deleteBranch: boolean; deleteBranch: boolean;
startDate: IsoOrRfcDateString | undefined; // Should be ISO 8601 or RFC 2822
} }
const logger: Logger = new Logger(); const logger: Logger = new Logger();
@ -204,6 +211,39 @@ export class IssueProcessor {
continue; // don't process locked issues continue; // don't process locked issues
} }
if (this.options.startDate) {
const startDate: Date = new Date(this.options.startDate);
const createdAt: Date = new Date(issue.created_at);
issueLogger.info(
`A start date was specified for the ${getHumanizedDate(startDate)} (${
this.options.startDate
})`
);
// Expecting that GitHub will always set a creation date on the issues and PRs
// But you never know!
if (!isValidDate(createdAt)) {
throw new Error(
`Invalid issue field: "created_at". Expected a valid date`
);
}
issueLogger.info(
`Issue created the ${getHumanizedDate(createdAt)} (${
issue.created_at
})`
);
if (!isDateMoreRecentThan(createdAt, startDate)) {
issueLogger.info(
`Skipping ${issueType} because it was created before the specified start date`
);
continue; // don't process issues which were created before the start date
}
}
// Does this issue have a stale label? // Does this issue have a stale label?
let isStale: boolean = isLabeled(issue, staleLabel); let isStale: boolean = isLabeled(issue, staleLabel);

View File

@ -0,0 +1,33 @@
import {getHumanizedDate} from './get-humanized-date';
describe('getHumanizedDate()', (): void => {
let date: Date;
describe('when the given date is the 1st of april 2020', (): void => {
beforeEach((): void => {
date = new Date(2020, 3, 1);
});
it('should return the date formatted as DD-MM-YYYY', (): void => {
expect.assertions(1);
const result = getHumanizedDate(date);
expect(result).toStrictEqual('01-04-2020');
});
});
describe('when the given date is the 18st of december 2020', (): void => {
beforeEach((): void => {
date = new Date(2020, 11, 18);
});
it('should return the date formatted as DD-MM-YYYY', (): void => {
expect.assertions(1);
const result = getHumanizedDate(date);
expect(result).toStrictEqual('18-12-2020');
});
});
});

View File

@ -0,0 +1,17 @@
import {HumanizedDate} from '../../types/humanized-date';
export function getHumanizedDate(date: Readonly<Date>): HumanizedDate {
const year: number = date.getFullYear();
let month = `${date.getMonth() + 1}`;
let day = `${date.getDate()}`;
if (month.length < 2) {
month = `0${month}`;
}
if (day.length < 2) {
day = `0${day}`;
}
return [day, month, year].join('-');
}

View File

@ -0,0 +1,51 @@
import {isDateMoreRecentThan} from './is-date-more-recent-than';
describe('isDateMoreRecentThan()', (): void => {
let date: Date;
let comparedDate: Date;
describe('when the given date is older than the compared date', (): void => {
beforeEach((): void => {
date = new Date(2020, 0, 20);
comparedDate = new Date(2021, 0, 20);
});
it('should return false', (): void => {
expect.assertions(1);
const result = isDateMoreRecentThan(date, comparedDate);
expect(result).toStrictEqual(false);
});
});
describe('when the given date is equal to the compared date', (): void => {
beforeEach((): void => {
date = new Date(2020, 0, 20);
comparedDate = new Date(2020, 0, 20);
});
it('should return false', (): void => {
expect.assertions(1);
const result = isDateMoreRecentThan(date, comparedDate);
expect(result).toStrictEqual(false);
});
});
describe('when the given date is more recent than the compared date', (): void => {
beforeEach((): void => {
date = new Date(2021, 0, 20);
comparedDate = new Date(2020, 0, 20);
});
it('should return true', (): void => {
expect.assertions(1);
const result = isDateMoreRecentThan(date, comparedDate);
expect(result).toStrictEqual(true);
});
});
});

View File

@ -0,0 +1,6 @@
export function isDateMoreRecentThan(
date: Readonly<Date>,
comparedDate: Readonly<Date>
): boolean {
return date > comparedDate;
}

View File

@ -0,0 +1,61 @@
import {isValidDate} from './is-valid-date';
describe('isValidDate()', (): void => {
let date: Date;
describe('when the given date is an invalid date', (): void => {
beforeEach((): void => {
date = new Date('16-04-1994');
});
it('should return false', (): void => {
expect.assertions(1);
const result = isValidDate(date);
expect(result).toStrictEqual(false);
});
});
describe('when the given date is a new date', (): void => {
beforeEach((): void => {
date = new Date();
});
it('should return true', (): void => {
expect.assertions(1);
const result = isValidDate(date);
expect(result).toStrictEqual(true);
});
});
describe('when the given date is an ISO and valid date', (): void => {
beforeEach((): void => {
date = new Date('2011-04-22T13:33:48Z');
});
it('should return true', (): void => {
expect.assertions(1);
const result = isValidDate(date);
expect(result).toStrictEqual(true);
});
});
describe('when the given date is an ISO with ms and valid date', (): void => {
beforeEach((): void => {
date = new Date('2011-10-05T14:48:00.000Z');
});
it('should return true', (): void => {
expect.assertions(1);
const result = isValidDate(date);
expect(result).toStrictEqual(true);
});
});
});

View File

@ -0,0 +1,18 @@
/**
* @description
* Check if a date is valid
*
* @see
* https://stackoverflow.com/a/1353711/4440414
*
* @param {Readonly<Date>} date The date to check
*
* @returns {boolean} true when the given date is valid
*/
export function isValidDate(date: Readonly<Date>): boolean {
if (Object.prototype.toString.call(date) === '[object Date]') {
return !isNaN(date.getTime());
}
return false;
}

View File

@ -1,4 +1,4 @@
import {IssueType} from '../enums/issue-type.enum'; import {IssueType} from '../enums/issue-type';
export function getIssueType(isPullRequest: Readonly<boolean>): IssueType { export function getIssueType(isPullRequest: Readonly<boolean>): IssueType {
return isPullRequest ? IssueType.PullRequest : IssueType.Issue; return isPullRequest ? IssueType.PullRequest : IssueType.Issue;

View File

@ -1,7 +1,6 @@
import deburr from 'lodash.deburr'; import deburr from 'lodash.deburr';
import {Issue, Label} from '../IssueProcessor'; import {Issue, Label} from '../IssueProcessor';
import {CleanLabel} from '../types/clean-label';
type CleanLabel = string;
/** /**
* @description * @description

View File

@ -1,4 +1,5 @@
import * as core from '@actions/core'; import * as core from '@actions/core';
import {isValidDate} from './functions/dates/is-valid-date';
import {IssueProcessor, IssueProcessorOptions} from './IssueProcessor'; import {IssueProcessor, IssueProcessorOptions} from './IssueProcessor';
async function run(): Promise<void> { async function run(): Promise<void> {
@ -14,7 +15,7 @@ async function run(): Promise<void> {
} }
function getAndValidateArgs(): IssueProcessorOptions { function getAndValidateArgs(): IssueProcessorOptions {
const args = { const args: IssueProcessorOptions = {
repoToken: core.getInput('repo-token'), repoToken: core.getInput('repo-token'),
staleIssueMessage: core.getInput('stale-issue-message'), staleIssueMessage: core.getInput('stale-issue-message'),
stalePrMessage: core.getInput('stale-pr-message'), stalePrMessage: core.getInput('stale-pr-message'),
@ -47,7 +48,11 @@ function getAndValidateArgs(): IssueProcessorOptions {
ascending: core.getInput('ascending') === 'true', ascending: core.getInput('ascending') === 'true',
skipStalePrMessage: core.getInput('skip-stale-pr-message') === 'true', skipStalePrMessage: core.getInput('skip-stale-pr-message') === 'true',
skipStaleIssueMessage: core.getInput('skip-stale-issue-message') === 'true', skipStaleIssueMessage: core.getInput('skip-stale-issue-message') === 'true',
deleteBranch: core.getInput('delete-branch') === 'true' deleteBranch: core.getInput('delete-branch') === 'true',
startDate:
core.getInput('start-date') !== ''
? core.getInput('start-date')
: undefined
}; };
for (const numberInput of [ for (const numberInput of [
@ -64,6 +69,17 @@ function getAndValidateArgs(): IssueProcessorOptions {
} }
} }
for (const optionalDateInput of ['start-date']) {
// Ignore empty dates because it is considered as the right type for a default value (so a valid one)
if (core.getInput(optionalDateInput) !== '') {
if (!isValidDate(new Date(core.getInput(optionalDateInput)))) {
throw new Error(
`input ${optionalDateInput} did not parse to a valid date`
);
}
}
}
return args; return args;
} }

1
src/types/clean-label.ts Normal file
View File

@ -0,0 +1 @@
export type CleanLabel = string;

View File

@ -0,0 +1 @@
export type HumanizedDate = string;

View File

@ -0,0 +1 @@
export type IsoDateString = string;

View File

@ -0,0 +1 @@
export type IsoOrRfcDateString = string;