feat(stale-and-close): add new options to change the days before close (#224)

* docs(readme): add new options in the documentation

* chore: update the action schema

* chore: parse the new arguments

* feat(stale-and-close): add new options to change the days before close

to avoid a breaking change and simplify the configuration the old options 'daysBeforeStale' and 'daysBeforePrClose' are kept and new options are available to override them with 'daysBeforeIssueStale', 'daysBeforePrStale', 'daysBeforeIssueClose' and 'daysBeforePrClose'

* chore: rename the issue type enum to remove the enum suffix

* chore: add missing dependency for eslint and typescript

also upgrade the parser

* chore: fix an issue with the linter for the shadow rules

it was not configured properly for TypeScript

* chore: use camelCase for constants

* chore: use camelCase for enum members

* chore: fix the tests

* chore: enhance prettier to also lint other kind of files

it was configured to only work with ts and it was not working well to be honest
also now the lint scripts will also run prettier
This commit is contained in:
Geoffrey Testelin 2021-01-16 14:28:29 +01:00 committed by GitHub
parent b12dccced8
commit 552e4c60f0
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
19 changed files with 767 additions and 203 deletions

View File

@ -12,7 +12,10 @@
"import/no-namespace": "off",
"no-unused-vars": "off",
"@typescript-eslint/no-unused-vars": "error",
"@typescript-eslint/explicit-member-accessibility": ["error", {"accessibility": "no-public"}],
"@typescript-eslint/explicit-member-accessibility": [
"error",
{"accessibility": "no-public"}
],
"@typescript-eslint/no-require-imports": "error",
"@typescript-eslint/array-type": "error",
"@typescript-eslint/await-thenable": "error",
@ -42,7 +45,9 @@
"@typescript-eslint/restrict-plus-operands": "error",
"semi": "off",
"@typescript-eslint/type-annotation-spacing": "error",
"@typescript-eslint/unbound-method": "off"
"@typescript-eslint/unbound-method": "off",
"no-shadow": "off",
"@typescript-eslint/no-shadow": "error"
},
"env": {
"node": true,

6
.prettierignore Normal file
View File

@ -0,0 +1,6 @@
.idea
.licenses
.vscode
dist
node_modules
package-lock.json

View File

@ -6,6 +6,5 @@
"singleQuote": true,
"trailingComma": "none",
"bracketSpacing": false,
"arrowParens": "avoid",
"parser": "typescript"
"arrowParens": "avoid"
}

View File

@ -25,10 +25,14 @@ $ npm test
### Arguments
| Input | Description | Usage |
| :-------------------------: | :-------------------------------------------------------------------------------: | :------: |
| --------------------------- | ------------------------------------------------------------------------------------ | -------- |
| `repo-token` | PAT(Personal Access Token) for authorizing repository. | 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-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-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 |
| `stale-issue-message` | Message to post on the stale issue. | 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 |
@ -54,7 +58,7 @@ See [action.yml](./action.yml) For comprehensive list of options.
Basic:
```yaml
name: 'Close stale issues'
name: 'Close stale issues and PRs'
on:
schedule:
- cron: '30 1 * * *'
@ -72,7 +76,7 @@ jobs:
Configure stale timeouts:
```yaml
name: 'Close stale issues'
name: 'Close stale issues and PRs'
on:
schedule:
- cron: '30 1 * * *'
@ -83,15 +87,63 @@ jobs:
steps:
- uses: actions/stale@v3
with:
stale-issue-message: 'This issue is stale because it has been open 30 days with no activity. Remove stale label or comment or this will be closed in 5 days'
stale-issue-message: 'This issue is stale because it has been open 30 days with no activity. Remove stale label or comment or this will be closed in 5 days.'
days-before-stale: 30
days-before-close: 5
```
Configure different stale timeouts but never close a pr:
```yaml
name: 'Close stale issues and PR'
on:
schedule:
- cron: '30 1 * * *'
jobs:
stale:
runs-on: ubuntu-latest
steps:
- uses: actions/stale@v3
with:
repo-token: ${{ secrets.GITHUB_TOKEN }}
stale-issue-message: 'This issue is stale because it has been open 30 days with no activity. Remove stale label or comment or this will be closed in 5 days.'
stale-pr-message: 'This pr is stale because it has been open 45 days with no activity. Remove stale label or comment or this will be closed in 10 days.'
close-issue-message: 'This issue was closed because it has been stalled for 5 days with no activity.'
days-before-stale: 30
days-before-close: 5
days-before-pr-close: -1
```
Configure different stale timeouts:
```yaml
name: 'Close stale issues and PRs'
on:
schedule:
- cron: '30 1 * * *'
jobs:
stale:
runs-on: ubuntu-latest
steps:
- uses: actions/stale@v3
with:
repo-token: ${{ secrets.GITHUB_TOKEN }}
stale-issue-message: 'This issue is stale because it has been open 30 days with no activity. Remove stale label or comment or this will be closed in 5 days.'
stale-pr-message: 'This pr is stale because it has been open 45 days with no activity. Remove stale label or comment or this will be closed in 10 days.'
close-issue-message: 'This issue was closed because it has been stalled for 5 days with no activity.'
close-pr-message: 'This pr was closed because it has been stalled for 10 days with no activity.'
days-before-issue-stale: 30
days-before-pr-stale: 45
days-before-issue-close: 5
days-before-pr-close: 10
```
Configure labels:
```yaml
name: 'Close stale issues'
name: 'Close stale issues and PRs'
on:
schedule:
- cron: '30 1 * * *'

View File

@ -1,8 +1,8 @@
import * as github from '@actions/github';
import {
IssueProcessor,
Issue,
IssueProcessor,
IssueProcessorOptions
} from '../src/IssueProcessor';
@ -35,7 +35,11 @@ const DefaultProcessorOptions: IssueProcessorOptions = Object.freeze({
closeIssueMessage: 'This issue is being closed',
closePrMessage: 'This PR is being closed',
daysBeforeStale: 1,
daysBeforeIssueStale: NaN,
daysBeforePrStale: NaN,
daysBeforeClose: 30,
daysBeforeIssueClose: NaN,
daysBeforePrClose: NaN,
staleIssueLabel: 'Stale',
closeIssueLabel: '',
exemptIssueLabels: '',
@ -73,8 +77,36 @@ test('processing an issue with no label will make it stale and close it, if it i
generateIssue(1, 'An issue with no label', '2020-01-01T17:00:00Z')
];
const opts = {...DefaultProcessorOptions};
opts.daysBeforeClose = 0;
const opts: IssueProcessorOptions = {
...DefaultProcessorOptions,
daysBeforeClose: 0
};
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).toEqual(1);
expect(processor.closedIssues.length).toEqual(1);
});
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[] = [
generateIssue(1, 'An issue with no label', '2020-01-01T17:00:00Z')
];
const opts: IssueProcessorOptions = {
...DefaultProcessorOptions,
daysBeforeClose: 1,
daysBeforeIssueClose: 0
};
const processor = new IssueProcessor(
opts,
@ -92,16 +124,70 @@ test('processing an issue with no label will make it stale and close it, if it i
expect(processor.deletedBranchIssues.length).toEqual(0);
});
test('processing an issue with no label will make it stale and not 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[] = [
generateIssue(1, 'An issue with no label', '2020-01-01T17:00:00Z')
];
const opts: IssueProcessorOptions = {
...DefaultProcessorOptions,
daysBeforeClose: 1,
daysBeforeIssueClose: 1
};
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).toEqual(1);
expect(processor.closedIssues.length).toEqual(0);
});
test('processing an issue with no label will make it stale and not close it if days-before-close is set to > 0', async () => {
const TestIssueList: Issue[] = [
generateIssue(1, 'An issue with no label', '2020-01-01T17:00:00Z')
];
const opts = {...DefaultProcessorOptions};
opts.daysBeforeClose = 15;
const opts: IssueProcessorOptions = {
...DefaultProcessorOptions,
daysBeforeClose: 15
};
const processor = new IssueProcessor(
DefaultProcessorOptions,
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).toEqual(1);
expect(processor.closedIssues.length).toEqual(0);
});
test('processing an issue with no label will make it stale and not close it if days-before-close is set to -1 and days-before-issue-close is set to > 0', async () => {
const TestIssueList: Issue[] = [
generateIssue(1, 'An issue with no label', '2020-01-01T17:00:00Z')
];
const opts: IssueProcessorOptions = {
...DefaultProcessorOptions,
daysBeforeClose: -1,
daysBeforeIssueClose: 15
};
const processor = new IssueProcessor(
opts,
async () => 'abot',
async p => (p == 1 ? TestIssueList : []),
async (num, dt) => [],
@ -120,7 +206,7 @@ test('processing an issue with no label will not make it stale if days-before-st
generateIssue(1, 'An issue with no label', '2020-01-01T17:00:00Z')
];
const opts = {
const opts: IssueProcessorOptions = {
...DefaultProcessorOptions,
staleIssueMessage: '',
daysBeforeStale: -1
@ -141,6 +227,33 @@ test('processing an issue with no label will not make it stale if days-before-st
expect(processor.closedIssues.length).toEqual(0);
});
test('processing an issue with no label will not make it stale if days-before-stale and days-before-issue-stale are set to -1', async () => {
const TestIssueList: Issue[] = [
generateIssue(1, 'An issue with no label', '2020-01-01T17:00:00Z')
];
const opts: IssueProcessorOptions = {
...DefaultProcessorOptions,
staleIssueMessage: '',
daysBeforeStale: -1,
daysBeforeIssueStale: -1
};
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).toEqual(0);
expect(processor.closedIssues.length).toEqual(0);
});
test('processing an issue with no label will make it stale but not close it', async () => {
// issue should be from 2 days ago so it will be
// stale but not close-able, based on default settings
@ -177,8 +290,13 @@ test('processing a stale issue will close it', async () => {
)
];
const opts: IssueProcessorOptions = {
...DefaultProcessorOptions,
daysBeforeClose: 30
};
const processor = new IssueProcessor(
DefaultProcessorOptions,
opts,
async () => 'abot',
async p => (p == 1 ? TestIssueList : []),
async (num, dt) => [],
@ -254,6 +372,38 @@ test('processing a stale issue containing a slash in the label will close it', a
expect(processor.closedIssues.length).toEqual(1);
});
test('processing a stale issue will close it when days-before-issue-stale override days-before-stale', async () => {
const TestIssueList: Issue[] = [
generateIssue(
1,
'A stale issue that should be closed',
'2020-01-01T17:00:00Z',
false,
['Stale']
)
];
const opts: IssueProcessorOptions = {
...DefaultProcessorOptions,
daysBeforeClose: 30,
daysBeforeIssueStale: 30
};
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).toEqual(0);
expect(processor.closedIssues.length).toEqual(1);
});
test('processing a stale PR will close it', async () => {
const TestIssueList: Issue[] = [
generateIssue(
@ -265,8 +415,45 @@ test('processing a stale PR will close it', async () => {
)
];
const opts: IssueProcessorOptions = {
...DefaultProcessorOptions,
daysBeforeClose: 30
};
const processor = new IssueProcessor(
DefaultProcessorOptions,
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).toEqual(0);
expect(processor.closedIssues.length).toEqual(1);
});
test('processing a stale PR will close it when days-before-pr-stale override days-before-stale', async () => {
const TestIssueList: Issue[] = [
generateIssue(
1,
'A stale PR that should be closed',
'2020-01-01T17:00:00Z',
true,
['Stale']
)
];
const opts: IssueProcessorOptions = {
...DefaultProcessorOptions,
daysBeforeClose: 30,
daysBeforePrClose: 30
};
const processor = new IssueProcessor(
opts,
async () => 'abot',
async p => (p == 1 ? TestIssueList : []),
async (num, dt) => [],
@ -308,6 +495,35 @@ test('processing a stale issue will close it even if configured not to mark as s
expect(processor.closedIssues.length).toEqual(1);
});
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[] = [
generateIssue(1, 'An issue with no label', '2020-01-01T17:00:00Z', false, [
'Stale'
])
];
const opts = {
...DefaultProcessorOptions,
daysBeforeStale: 0,
daysBeforeIssueStale: -1,
staleIssueMessage: ''
};
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).toEqual(0);
expect(processor.closedIssues.length).toEqual(1);
});
test('processing a stale PR will close it even if configured not to mark as stale', async () => {
const TestIssueList: Issue[] = [
generateIssue(1, 'An issue with no label', '2020-01-01T17:00:00Z', true, [
@ -336,6 +552,35 @@ test('processing a stale PR will close it even if configured not to mark as stal
expect(processor.closedIssues.length).toEqual(1);
});
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[] = [
generateIssue(1, 'An issue with no label', '2020-01-01T17:00:00Z', true, [
'Stale'
])
];
const opts = {
...DefaultProcessorOptions,
daysBeforeStale: 0,
daysBeforePrStale: -1,
stalePrMessage: ''
};
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).toEqual(0);
expect(processor.closedIssues.length).toEqual(1);
});
test('closed issues will not be marked stale', async () => {
const TestIssueList: Issue[] = [
generateIssue(
@ -692,7 +937,14 @@ test('stale label should be removed if a comment was added to a stale issue', as
opts,
async () => 'abot',
async p => (p == 1 ? TestIssueList : []),
async (num: number, dt: string) => [{user: {login: 'notme', type: 'User'}}], // return a fake comment to indicate there was an update
async (num: number, dt: string) => [
{
user: {
login: 'notme',
type: 'User'
}
}
], // return a fake comment to indicate there was an update
async (issue: Issue, label: string) => new Date().toDateString()
);
@ -723,7 +975,14 @@ test('stale label should not be removed if a comment was added by the bot (and t
opts,
async () => 'abot',
async p => (p == 1 ? TestIssueList : []),
async (num: number, dt: string) => [{user: {login: 'abot', type: 'User'}}], // return a fake comment to indicate there was an update by the bot
async (num: number, dt: string) => [
{
user: {
login: 'abot',
type: 'User'
}
}
], // return a fake comment to indicate there was an update by the bot
async (issue: Issue, label: string) => new Date().toDateString()
);

View File

@ -7,58 +7,90 @@ inputs:
default: ${{ github.token }}
stale-issue-message:
description: 'The message to post on the issue when tagging it. If none provided, will not mark issues stale.'
required: false
stale-pr-message:
description: 'The message to post on the pr when tagging it. If none provided, will not mark pull requests stale.'
required: false
close-issue-message:
description: 'The message to post on the issue when closing it. If none provided, will not comment when closing an issue.'
required: false
close-pr-message:
description: 'The message to post on the pr when closing it. If none provided, will not comment when closing a pull requests.'
required: false
days-before-stale:
description: 'The number of days old an issue can be before marking it stale. Set to -1 to never mark issues or pull requests as stale automatically.'
default: 60
description: 'The number of days old an issue or a pull request can be before marking it stale. Set to -1 to never mark issues or pull requests as stale automatically.'
required: false
default: '60'
days-before-issue-stale:
description: 'The number of days old an issue can be before marking it stale. Set to -1 to never mark issues as stale automatically. Override "days-before-stale" option regarding the issues only.'
required: false
days-before-pr-stale:
description: 'The number of days old a pull request can be before marking it stale. Set to -1 to never mark pull requests as stale automatically. Override "days-before-stale" option regarding the pull requests only.'
required: false
days-before-close:
description: 'The number of days to wait to close an issue or pull request after it being marked stale. Set to -1 to never close stale issues.'
default: 7
description: 'The number of days to wait to close an issue or a pull request after it being marked stale. Set to -1 to never close stale issues or pull requests.'
required: false
default: '7'
days-before-issue-close:
description: 'The number of days to wait to close an issue after it being marked stale. Set to -1 to never close stale issues. Override "days-before-close" option regarding the issues only.'
required: false
days-before-pr-close:
description: 'The number of days to wait to close a pull request after it being marked stale. Set to -1 to never close stale pull requests. Override "days-before-close" option regarding the pull requests only.'
required: false
stale-issue-label:
description: 'The label to apply when an issue is stale.'
required: false
default: 'Stale'
close-issue-label:
description: 'The label to apply when an issue is closed.'
required: false
exempt-issue-labels:
description: 'The labels that mean an issue is exempt from being marked stale. Separate multiple labels with commas (eg. "label1,label2")'
default: ''
required: false
stale-pr-label:
description: 'The label to apply when a pull request is stale.'
default: 'Stale'
required: false
close-pr-label:
description: 'The label to apply when a pull request is closed.'
required: false
exempt-pr-labels:
description: 'The labels that mean a pull request is exempt from being marked stale. Separate multiple labels with commas (eg. "label1,label2")'
default: ''
required: false
only-labels:
description: 'Only issues or pull requests with all of these labels are checked if stale. Defaults to `[]` (disabled) and can be a comma-separated list of labels.'
default: ''
required: false
operations-per-run:
description: 'The maximum number of operations per run, used to control rate limiting.'
default: 30
default: '30'
required: false
remove-stale-when-updated:
description: 'Remove stale labels from issues when they are updated or commented on.'
default: true
default: 'true'
required: false
debug-only:
description: 'Run the processor in debug mode without actually performing any operations on live issues.'
default: false
default: 'false'
required: false
ascending:
description: 'The order to get issues or pull requests. Defaults to false, which is descending'
default: false
default: 'false'
required: false
skip-stale-pr-message:
description: 'Skip adding stale message when marking a pull request as stale.'
default: false
default: 'false'
required: false
skip-stale-issue-message:
description: 'Skip adding stale message when marking an issue as stale.'
default: false
default: 'false'
required: false
delete-branch:
description: 'Delete the git branch after closing a stale pull request.'
default: false
default: 'false'
required: false
runs:
using: 'node12'
main: 'dist/index.js'

128
package-lock.json generated
View File

@ -1912,53 +1912,60 @@
"dev": true
},
"@typescript-eslint/eslint-plugin": {
"version": "3.7.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-3.7.0.tgz",
"integrity": "sha512-4OEcPON3QIx0ntsuiuFP/TkldmBGXf0uKxPQlGtS/W2F3ndYm8Vgdpj/woPJkzUc65gd3iR+qi3K8SDQP/obFg==",
"version": "4.13.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-4.13.0.tgz",
"integrity": "sha512-ygqDUm+BUPvrr0jrXqoteMqmIaZ/bixYOc3A4BRwzEPTZPi6E+n44rzNZWaB0YvtukgP+aoj0i/fyx7FkM2p1w==",
"dev": true,
"requires": {
"@typescript-eslint/experimental-utils": "3.7.0",
"@typescript-eslint/experimental-utils": "4.13.0",
"@typescript-eslint/scope-manager": "4.13.0",
"debug": "^4.1.1",
"functional-red-black-tree": "^1.0.1",
"lodash": "^4.17.15",
"regexpp": "^3.0.0",
"semver": "^7.3.2",
"tsutils": "^3.17.1"
},
"dependencies": {
"@typescript-eslint/experimental-utils": {
"version": "3.7.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/experimental-utils/-/experimental-utils-3.7.0.tgz",
"integrity": "sha512-xpfXXAfZqhhqs5RPQBfAFrWDHoNxD5+sVB5A46TF58Bq1hRfVROrWHcQHHUM9aCBdy9+cwATcvCbRg8aIRbaHQ==",
"version": "4.13.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/experimental-utils/-/experimental-utils-4.13.0.tgz",
"integrity": "sha512-/ZsuWmqagOzNkx30VWYV3MNB/Re/CGv/7EzlqZo5RegBN8tMuPaBgNK6vPBCQA8tcYrbsrTdbx3ixMRRKEEGVw==",
"dev": true,
"requires": {
"@types/json-schema": "^7.0.3",
"@typescript-eslint/types": "3.7.0",
"@typescript-eslint/typescript-estree": "3.7.0",
"@typescript-eslint/scope-manager": "4.13.0",
"@typescript-eslint/types": "4.13.0",
"@typescript-eslint/typescript-estree": "4.13.0",
"eslint-scope": "^5.0.0",
"eslint-utils": "^2.0.0"
}
},
"@typescript-eslint/typescript-estree": {
"version": "3.7.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-3.7.0.tgz",
"integrity": "sha512-xr5oobkYRebejlACGr1TJ0Z/r0a2/HUf0SXqPvlgUMwiMqOCu/J+/Dr9U3T0IxpE5oLFSkqMx1FE/dKaZ8KsOQ==",
"@typescript-eslint/scope-manager": {
"version": "4.13.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-4.13.0.tgz",
"integrity": "sha512-UpK7YLG2JlTp/9G4CHe7GxOwd93RBf3aHO5L+pfjIrhtBvZjHKbMhBXTIQNkbz7HZ9XOe++yKrXutYm5KmjWgQ==",
"dev": true,
"requires": {
"@typescript-eslint/types": "3.7.0",
"@typescript-eslint/visitor-keys": "3.7.0",
"@typescript-eslint/types": "4.13.0",
"@typescript-eslint/visitor-keys": "4.13.0"
}
},
"@typescript-eslint/typescript-estree": {
"version": "4.13.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-4.13.0.tgz",
"integrity": "sha512-9A0/DFZZLlGXn5XA349dWQFwPZxcyYyCFX5X88nWs2uachRDwGeyPz46oTsm9ZJE66EALvEns1lvBwa4d9QxMg==",
"dev": true,
"requires": {
"@typescript-eslint/types": "4.13.0",
"@typescript-eslint/visitor-keys": "4.13.0",
"debug": "^4.1.1",
"glob": "^7.1.6",
"globby": "^11.0.1",
"is-glob": "^4.0.1",
"lodash": "^4.17.15",
"semver": "^7.3.2",
"tsutils": "^3.17.1"
}
},
"regexpp": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/regexpp/-/regexpp-3.1.0.tgz",
"integrity": "sha512-ZOIzd8yVsQQA7j8GCSlPGXwg5PfmA1mrq0JP4nGhh54LaKN3xdai/vHUDu74pKwV8OxseMS65u2NImosQcSD0Q==",
"dev": true
}
}
},
@ -1985,41 +1992,35 @@
}
},
"@typescript-eslint/parser": {
"version": "4.8.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-4.8.1.tgz",
"integrity": "sha512-QND8XSVetATHK9y2Ltc/XBl5Ro7Y62YuZKnPEwnNPB8E379fDsvzJ1dMJ46fg/VOmk0hXhatc+GXs5MaXuL5Uw==",
"version": "4.13.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-4.13.0.tgz",
"integrity": "sha512-KO0J5SRF08pMXzq9+abyHnaGQgUJZ3Z3ax+pmqz9vl81JxmTTOUfQmq7/4awVfq09b6C4owNlOgOwp61pYRBSg==",
"dev": true,
"requires": {
"@typescript-eslint/scope-manager": "4.8.1",
"@typescript-eslint/types": "4.8.1",
"@typescript-eslint/typescript-estree": "4.8.1",
"@typescript-eslint/scope-manager": "4.13.0",
"@typescript-eslint/types": "4.13.0",
"@typescript-eslint/typescript-estree": "4.13.0",
"debug": "^4.1.1"
},
"dependencies": {
"@typescript-eslint/scope-manager": {
"version": "4.8.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-4.8.1.tgz",
"integrity": "sha512-r0iUOc41KFFbZdPAdCS4K1mXivnSZqXS5D9oW+iykQsRlTbQRfuFRSW20xKDdYiaCoH+SkSLeIF484g3kWzwOQ==",
"version": "4.13.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-4.13.0.tgz",
"integrity": "sha512-UpK7YLG2JlTp/9G4CHe7GxOwd93RBf3aHO5L+pfjIrhtBvZjHKbMhBXTIQNkbz7HZ9XOe++yKrXutYm5KmjWgQ==",
"dev": true,
"requires": {
"@typescript-eslint/types": "4.8.1",
"@typescript-eslint/visitor-keys": "4.8.1"
"@typescript-eslint/types": "4.13.0",
"@typescript-eslint/visitor-keys": "4.13.0"
}
},
"@typescript-eslint/types": {
"version": "4.8.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-4.8.1.tgz",
"integrity": "sha512-ave2a18x2Y25q5K05K/U3JQIe2Av4+TNi/2YuzyaXLAsDx6UZkz1boZ7nR/N6Wwae2PpudTZmHFXqu7faXfHmA==",
"dev": true
},
"@typescript-eslint/typescript-estree": {
"version": "4.8.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-4.8.1.tgz",
"integrity": "sha512-bJ6Fn/6tW2g7WIkCWh3QRlaSU7CdUUK52shx36/J7T5oTQzANvi6raoTsbwGM11+7eBbeem8hCCKbyvAc0X3sQ==",
"version": "4.13.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-4.13.0.tgz",
"integrity": "sha512-9A0/DFZZLlGXn5XA349dWQFwPZxcyYyCFX5X88nWs2uachRDwGeyPz46oTsm9ZJE66EALvEns1lvBwa4d9QxMg==",
"dev": true,
"requires": {
"@typescript-eslint/types": "4.8.1",
"@typescript-eslint/visitor-keys": "4.8.1",
"@typescript-eslint/types": "4.13.0",
"@typescript-eslint/visitor-keys": "4.13.0",
"debug": "^4.1.1",
"globby": "^11.0.1",
"is-glob": "^4.0.1",
@ -2027,22 +2028,6 @@
"semver": "^7.3.2",
"tsutils": "^3.17.1"
}
},
"@typescript-eslint/visitor-keys": {
"version": "4.8.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-4.8.1.tgz",
"integrity": "sha512-3nrwXFdEYALQh/zW8rFwP4QltqsanCDz4CwWMPiIZmwlk9GlvBeueEIbq05SEq4ganqM0g9nh02xXgv5XI3PeQ==",
"dev": true,
"requires": {
"@typescript-eslint/types": "4.8.1",
"eslint-visitor-keys": "^2.0.0"
}
},
"eslint-visitor-keys": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-2.0.0.tgz",
"integrity": "sha512-QudtT6av5WXels9WjIM7qz1XD1cWGvX4gGXvp/zBn9nXG02D0utdU3Em2m/QjTnrsk6bBjmCygl3rmj118msQQ==",
"dev": true
}
}
},
@ -2081,9 +2066,9 @@
}
},
"@typescript-eslint/types": {
"version": "3.7.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-3.7.0.tgz",
"integrity": "sha512-reCaK+hyKkKF+itoylAnLzFeNYAEktB0XVfSQvf0gcVgpz1l49Lt6Vo9x4MVCCxiDydA0iLAjTF/ODH0pbfnpg==",
"version": "4.13.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-4.13.0.tgz",
"integrity": "sha512-/+aPaq163oX+ObOG00M0t9tKkOgdv9lq0IQv/y4SqGkAXmhFmCfgsELV7kOCTb2vVU5VOmVwXBXJTDr353C1rQ==",
"dev": true
},
"@typescript-eslint/typescript-estree": {
@ -2127,12 +2112,21 @@
}
},
"@typescript-eslint/visitor-keys": {
"version": "3.7.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-3.7.0.tgz",
"integrity": "sha512-k5PiZdB4vklUpUX4NBncn5RBKty8G3ihTY+hqJsCdMuD0v4jofI5xuqwnVcWxfv6iTm2P/dfEa2wMUnsUY8ODw==",
"version": "4.13.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-4.13.0.tgz",
"integrity": "sha512-6RoxWK05PAibukE7jElqAtNMq+RWZyqJ6Q/GdIxaiUj2Ept8jh8+FUVlbq9WxMYxkmEOPvCE5cRSyupMpwW31g==",
"dev": true,
"requires": {
"eslint-visitor-keys": "^1.1.0"
"@typescript-eslint/types": "4.13.0",
"eslint-visitor-keys": "^2.0.0"
},
"dependencies": {
"eslint-visitor-keys": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-2.0.0.tgz",
"integrity": "sha512-QudtT6av5WXels9WjIM7qz1XD1cWGvX4gGXvp/zBn9nXG02D0utdU3Em2m/QjTnrsk6bBjmCygl3rmj118msQQ==",
"dev": true
}
}
},
"@vercel/ncc": {

View File

@ -8,8 +8,8 @@
"build": "tsc",
"format": "prettier --write **/*.ts",
"format-check": "prettier --check **/*.ts",
"lint": "eslint src/**/*.ts",
"lint:fix": "eslint src/**/*.ts --fix",
"lint": "prettier --check --ignore-unknown **/*.{md,json,yml,ts} && eslint src/**/*.ts",
"lint:fix": "prettier --write --ignore-unknown **/*.{md,json,yml,ts} && eslint src/**/*.ts --fix",
"pack": "ncc build",
"test": "jest",
"all": "npm run build && npm run format && npm run lint && npm run pack && npm test"
@ -37,7 +37,8 @@
"@types/lodash.deburr": "^4.1.6",
"@types/node": "^14.10.0",
"@types/semver": "^7.3.4",
"@typescript-eslint/parser": "^4.8.1",
"@typescript-eslint/eslint-plugin": "^4.13.0",
"@typescript-eslint/parser": "^4.13.0",
"@vercel/ncc": "^0.27.0",
"eslint": "^7.17.0",
"eslint-plugin-github": "^4.0.1",

View File

@ -2,8 +2,12 @@ import * as core from '@actions/core';
import {context, getOctokit} from '@actions/github';
import {GitHub} from '@actions/github/lib/utils';
import {GetResponseTypeFromEndpointMethod} from '@octokit/types';
import {IssueType} from './enums/issue-type.enum';
import {getIssueType} from './functions/get-issue-type';
import {isLabeled} from './functions/is-labeled';
import {isPullRequest} from './functions/is-pull-request';
import {labelsToList} from './functions/labels-to-list';
import {shouldMarkWhenStale} from './functions/should-mark-when-stale';
export interface Issue {
title: string;
@ -48,7 +52,11 @@ export interface IssueProcessorOptions {
closeIssueMessage: string;
closePrMessage: string;
daysBeforeStale: number;
daysBeforeIssueStale: number; // Could be NaN
daysBeforePrStale: number; // Could be NaN
daysBeforeClose: number;
daysBeforeIssueClose: number; // Could be NaN
daysBeforePrClose: number; // Could be NaN
staleIssueLabel: string;
closeIssueLabel: string;
exemptIssueLabels: string;
@ -69,14 +77,21 @@ export interface IssueProcessorOptions {
* Handle processing of issues for staleness/closure.
*/
export class IssueProcessor {
private static updatedSince(timestamp: string, num_days: number): boolean {
const daysInMillis = 1000 * 60 * 60 * 24 * num_days;
const millisSinceLastUpdated =
new Date().getTime() - new Date(timestamp).getTime();
return millisSinceLastUpdated <= daysInMillis;
}
readonly client: InstanceType<typeof GitHub>;
readonly options: IssueProcessorOptions;
private operationsLeft = 0;
readonly staleIssues: Issue[] = [];
readonly closedIssues: Issue[] = [];
readonly deletedBranchIssues: Issue[] = [];
readonly removedLabelIssues: Issue[] = [];
private operationsLeft = 0;
constructor(
options: IssueProcessorOptions,
@ -131,7 +146,7 @@ export class IssueProcessor {
}
for (const issue of issues.values()) {
const isPr = !!issue.pull_request;
const isPr = isPullRequest(issue);
core.info(
`Found issue: issue #${issue.number} last updated ${issue.updated_at} (is pr? ${isPr})`
@ -156,10 +171,20 @@ export class IssueProcessor {
const skipMessage = isPr
? this.options.skipStalePrMessage
: this.options.skipStaleIssueMessage;
const issueType: string = isPr ? 'pr' : 'issue';
const shouldMarkWhenStale = this.options.daysBeforeStale > -1;
const issueType: IssueType = getIssueType(isPr);
const daysBeforeStale: number = isPr
? this._getDaysBeforePrStale()
: this._getDaysBeforeIssueStale();
if (!staleMessage && shouldMarkWhenStale) {
if (isPr) {
core.info(`Days before pull request stale: ${daysBeforeStale}`);
} else {
core.info(`Days before issue stale: ${daysBeforeStale}`);
}
const shouldMarkAsStale: boolean = shouldMarkWhenStale(daysBeforeStale);
if (!staleMessage && shouldMarkAsStale) {
core.info(`Skipping ${issueType} due to empty stale message`);
continue;
}
@ -199,7 +224,7 @@ export class IssueProcessor {
);
// determine if this issue needs to be marked stale first
if (!isStale && shouldBeStale && shouldMarkWhenStale) {
if (!isStale && shouldBeStale && shouldMarkAsStale) {
core.info(
`Marking ${issueType} stale because it was last updated on ${issue.updated_at} and it does not have a stale label`
);
@ -233,7 +258,7 @@ export class IssueProcessor {
// handle all of the stale issue logic when we find a stale issue
private async processStaleIssue(
issue: Issue,
issueType: string,
issueType: IssueType,
staleLabel: string,
actor: string,
closeMessage?: string,
@ -252,9 +277,20 @@ export class IssueProcessor {
`Issue #${issue.number} has been commented on: ${issueHasComments}`
);
const isPr: boolean = isPullRequest(issue);
const daysBeforeClose: number = isPr
? this._getDaysBeforePrClose()
: this._getDaysBeforeIssueClose();
if (isPr) {
core.info(`Days before pull request close: ${daysBeforeClose}`);
} else {
core.info(`Days before issue close: ${daysBeforeClose}`);
}
const issueHasUpdate: boolean = IssueProcessor.updatedSince(
issue.updated_at,
this.options.daysBeforeClose
daysBeforeClose
);
core.info(`Issue #${issue.number} has been updated: ${issueHasUpdate}`);
@ -267,7 +303,7 @@ export class IssueProcessor {
}
// now start closing logic
if (this.options.daysBeforeClose < 0) {
if (daysBeforeClose < 0) {
return; // nothing to do because we aren't closing stale issues
}
@ -590,11 +626,27 @@ export class IssueProcessor {
return staleLabeledEvent.created_at;
}
private static updatedSince(timestamp: string, num_days: number): boolean {
const daysInMillis = 1000 * 60 * 60 * 24 * num_days;
const millisSinceLastUpdated =
new Date().getTime() - new Date(timestamp).getTime();
private _getDaysBeforeIssueStale(): number {
return isNaN(this.options.daysBeforeIssueStale)
? this.options.daysBeforeStale
: this.options.daysBeforeIssueStale;
}
return millisSinceLastUpdated <= daysInMillis;
private _getDaysBeforePrStale(): number {
return isNaN(this.options.daysBeforePrStale)
? this.options.daysBeforeStale
: this.options.daysBeforePrStale;
}
private _getDaysBeforeIssueClose(): number {
return isNaN(this.options.daysBeforeIssueClose)
? this.options.daysBeforeClose
: this.options.daysBeforeIssueClose;
}
private _getDaysBeforePrClose(): number {
return isNaN(this.options.daysBeforePrClose)
? this.options.daysBeforeClose
: this.options.daysBeforePrClose;
}
}

View File

@ -0,0 +1,4 @@
export enum IssueType {
Issue = 'issue',
PullRequest = 'pr'
}

View File

@ -0,0 +1,33 @@
import {getIssueType} from './get-issue-type';
describe('getIssueType()', (): void => {
let isPullRequest: boolean;
describe('when the issue is a not pull request', (): void => {
beforeEach((): void => {
isPullRequest = false;
});
it('should return that the issue is really an issue', (): void => {
expect.assertions(1);
const result = getIssueType(isPullRequest);
expect(result).toStrictEqual('issue');
});
});
describe('when the issue is a pull request', (): void => {
beforeEach((): void => {
isPullRequest = true;
});
it('should return that the issue is a pull request', (): void => {
expect.assertions(1);
const result = getIssueType(isPullRequest);
expect(result).toStrictEqual('pr');
});
});
});

View File

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

View File

@ -0,0 +1,57 @@
import {Issue} from '../IssueProcessor';
import {isPullRequest} from './is-pull-request';
describe('isPullRequest()', (): void => {
let issue: Issue;
describe('when the given issue has an undefined pull request', (): void => {
beforeEach((): void => {
issue = {
pull_request: undefined
} as Issue;
});
it('should return false', (): void => {
expect.assertions(1);
const result = isPullRequest(issue);
expect(result).toStrictEqual(false);
});
});
describe('when the given issue has a null pull request', (): void => {
beforeEach((): void => {
issue = {
pull_request: null
} as Issue;
});
it('should return false', (): void => {
expect.assertions(1);
const result = isPullRequest(issue);
expect(result).toStrictEqual(false);
});
});
describe.each([{}, true])(
'when the given issue has pull request',
(value): void => {
beforeEach((): void => {
issue = {
pull_request: value
} as Issue;
});
it('should return true', (): void => {
expect.assertions(1);
const result = isPullRequest(issue);
expect(result).toStrictEqual(true);
});
}
);
});

View File

@ -0,0 +1,5 @@
import {Issue} from '../IssueProcessor';
export function isPullRequest(issue: Readonly<Issue>): boolean {
return !!issue.pull_request;
}

View File

@ -0,0 +1,47 @@
import {shouldMarkWhenStale} from './should-mark-when-stale';
describe('shouldMarkWhenStale()', (): void => {
let daysBeforeStale: number;
describe('when the given number of days indicate that it should be stalled', (): void => {
beforeEach((): void => {
daysBeforeStale = -1;
});
it('should return false', (): void => {
expect.assertions(1);
const result = shouldMarkWhenStale(daysBeforeStale);
expect(result).toStrictEqual(false);
});
});
describe('when the given number of days indicate that it should be stalled today', (): void => {
beforeEach((): void => {
daysBeforeStale = 0;
});
it('should return true', (): void => {
expect.assertions(1);
const result = shouldMarkWhenStale(daysBeforeStale);
expect(result).toStrictEqual(true);
});
});
describe('when the given number of days indicate that it should be stalled tomorrow', (): void => {
beforeEach((): void => {
daysBeforeStale = 1;
});
it('should return true', (): void => {
expect.assertions(1);
const result = shouldMarkWhenStale(daysBeforeStale);
expect(result).toStrictEqual(true);
});
});
});

View File

@ -0,0 +1,5 @@
export function shouldMarkWhenStale(
daysBeforeStale: Readonly<number>
): boolean {
return daysBeforeStale >= 0;
}

View File

@ -23,9 +23,13 @@ function getAndValidateArgs(): IssueProcessorOptions {
daysBeforeStale: parseInt(
core.getInput('days-before-stale', {required: true})
),
daysBeforeIssueStale: parseInt(core.getInput('days-before-issue-stale')),
daysBeforePrStale: parseInt(core.getInput('days-before-pr-stale')),
daysBeforeClose: parseInt(
core.getInput('days-before-close', {required: true})
),
daysBeforeIssueClose: parseInt(core.getInput('days-before-issue-close')),
daysBeforePrClose: parseInt(core.getInput('days-before-pr-close')),
staleIssueLabel: core.getInput('stale-issue-label', {required: true}),
closeIssueLabel: core.getInput('close-issue-label'),
exemptIssueLabels: core.getInput('exempt-issue-labels'),
@ -48,7 +52,11 @@ function getAndValidateArgs(): IssueProcessorOptions {
for (const numberInput of [
'days-before-stale',
'days-before-issue-stale',
'days-before-pr-stale',
'days-before-close',
'days-before-issue-close',
'days-before-pr-close',
'operations-per-run'
]) {
if (isNaN(parseInt(core.getInput(numberInput)))) {

View File

@ -1,12 +1,12 @@
{
"compilerOptions": {
"target": "es6", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019' or 'ESNEXT'. */
"module": "commonjs", /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */
"outDir": "./lib", /* Redirect output structure to the directory. */
"rootDir": "./src", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */
"strict": true, /* Enable all strict type-checking options. */
"noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */
"esModuleInterop": true, /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */
"target": "es6" /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019' or 'ESNEXT'. */,
"module": "commonjs" /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */,
"outDir": "./lib" /* Redirect output structure to the directory. */,
"rootDir": "./src" /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */,
"strict": true /* Enable all strict type-checking options. */,
"noImplicitAny": true /* Raise error on expressions and declarations with an implied 'any' type. */,
"esModuleInterop": true /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */
//"sourceMap": true
},
"exclude": ["node_modules", "**/*.test.ts"]