Compare commits

..

13 Commits

Author SHA1 Message Date
Ross Brodbeck
6e742474aa Add more tests 2020-04-16 13:51:24 -04:00
Ross Brodbeck
420680dd9b Fix test cases to return the right pagination 2020-04-16 13:36:59 -04:00
Ross Brodbeck
6c28e8b071 Fix linting rules 2020-04-16 11:47:26 -04:00
Ross Brodbeck
2893271b29 Move processor to use its own types so testing is easier 2020-04-16 11:47:06 -04:00
Ross Brodbeck
e83d301625 Add our first real test 2020-04-16 10:48:59 -04:00
Ross Brodbeck
dbb0a7494e Merge back multi issue changes and allow for injection of issues 2020-04-16 10:10:14 -04:00
Ross Brodbeck
d7719d70c7 Pack for distribution 2020-04-16 09:56:52 -04:00
Ross Brodbeck
f11770a586 Pack for distribution 2020-04-16 09:56:52 -04:00
Ross Brodbeck
89d925e4d0 Fix operations per run 2020-04-16 09:56:52 -04:00
Ross Brodbeck
84ccaafbed Fix linting and pack for distribution 2020-04-16 09:56:52 -04:00
Ross Brodbeck
4a0411b89b Add debugging documentation 2020-04-16 09:55:42 -04:00
Ross Brodbeck
cff99ee7cd Add debug mode to action definition 2020-04-16 09:55:42 -04:00
Ross Brodbeck
4677ee2b0f Refctor into an issue processor, add debug mode 2020-04-16 09:55:42 -04:00
11 changed files with 1201 additions and 2337 deletions

View File

@@ -1,26 +0,0 @@
name: "Build"
on: # rebuild any PRs and main branch changes
pull_request:
push:
branches:
- master
- 'releases/*'
jobs:
build: # make sure build/ci work properly
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- run: |
npm install
npm run all
test: # make sure the action works on a clean machine without building
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: ./
with:
repo-token: ${{ secrets.GITHUB_TOKEN }}
stale-issue-message: 'This issue is stale'
stale-pr-message: 'This PR is stale'
debug-only: true

20
.vscode/launch.json vendored
View File

@@ -1,20 +0,0 @@
{
"version": "0.2.0",
"configurations": [
{
"name": "Debug Jest Tests",
"type": "node",
"request": "launch",
"program": "${workspaceRoot}/node_modules/jest/bin/jest",
"args": [
"-i"
],
"preLaunchTask": "tsc: build - tsconfig.json",
"internalConsoleOptions": "openOnSessionStart",
"console": "integratedTerminal",
"outFiles": [
"${workspaceRoot}/build/dist/**/*"
]
}
]
}

View File

@@ -2,23 +2,6 @@
Warns and then closes issues and PRs that have had no activity for a specified amount of time. Warns and then closes issues and PRs that have had no activity for a specified amount of time.
### Building and testing
Install the dependencies
```bash
$ npm install
```
Build the typescript and package it for distribution
```bash
$ npm run build && npm run pack
```
Run the tests :heavy_check_mark:
```bash
$ npm test
```
### Usage ### Usage
See [action.yml](./action.yml) For comprehensive list of options. See [action.yml](./action.yml) For comprehensive list of options.
@@ -34,7 +17,7 @@ jobs:
stale: stale:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/stale@v3 - uses: actions/stale@v1.1.0
with: with:
repo-token: ${{ secrets.GITHUB_TOKEN }} repo-token: ${{ secrets.GITHUB_TOKEN }}
stale-issue-message: 'Message to comment on stale issues. If none provided, will not mark issues stale' stale-issue-message: 'Message to comment on stale issues. If none provided, will not mark issues stale'
@@ -52,7 +35,7 @@ jobs:
stale: stale:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/stale@v3 - uses: actions/stale@v1.1.0
with: with:
repo-token: ${{ secrets.GITHUB_TOKEN }} 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-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'
@@ -71,11 +54,11 @@ jobs:
stale: stale:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/stale@v3 - uses: actions/stale@v1.1.0
with: with:
repo-token: ${{ secrets.GITHUB_TOKEN }} repo-token: ${{ secrets.GITHUB_TOKEN }}
stale-issue-message: 'Stale issue message' stale-issue-message: 'Stale issue message'
stale-pr-message: 'Stale pull request message' stale-pr-message: 'Stale issue message'
stale-issue-label: 'no-issue-activity' stale-issue-label: 'no-issue-activity'
exempt-issue-labels: 'awaiting-approval,work-in-progress' exempt-issue-labels: 'awaiting-approval,work-in-progress'
stale-pr-label: 'no-pr-activity' stale-pr-label: 'no-pr-activity'
@@ -84,4 +67,4 @@ jobs:
### 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 ouput 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

@@ -14,9 +14,7 @@ function generateIssue(
title: string, title: string,
updatedAt: string, updatedAt: string,
isPullRequest: boolean = false, isPullRequest: boolean = false,
labels: string[] = [], labels: string[] = []
isClosed: boolean = false,
isLocked: boolean = false
): Issue { ): Issue {
return { return {
number: id, number: id,
@@ -25,38 +23,27 @@ function generateIssue(
}), }),
title: title, title: title,
updated_at: updatedAt, updated_at: updatedAt,
pull_request: isPullRequest ? {} : null, pull_request: isPullRequest ? {} : null
state: isClosed ? 'closed' : 'open',
locked: isLocked
}; };
} }
const DefaultProcessorOptions: IssueProcessorOptions = Object.freeze({ const DefaultProcessorOptions: IssueProcessorOptions = {
repoToken: 'none', repoToken: 'none',
staleIssueMessage: 'This issue is stale', staleIssueMessage: 'This issue is stale',
stalePrMessage: 'This PR is stale', stalePrMessage: 'This PR is stale',
closeIssueMessage: 'This issue is being closed',
closePrMessage: 'This PR is being closed',
daysBeforeStale: 1, daysBeforeStale: 1,
daysBeforeClose: 30, daysBeforeClose: 1,
staleIssueLabel: 'Stale', staleIssueLabel: 'Stale',
exemptIssueLabels: '', exemptIssueLabels: '',
stalePrLabel: 'Stale', stalePrLabel: 'Stale',
exemptPrLabels: '', exemptPrLabels: '',
onlyLabels: '', onlyLabels: '',
operationsPerRun: 100, operationsPerRun: 100,
debugOnly: true, debugOnly: true
removeStaleWhenUpdated: false, };
ascending: false
});
test('empty issue list results in 1 operation', async () => { test('empty issue list results in 1 operation', async () => {
const processor = new IssueProcessor( const processor = new IssueProcessor(DefaultProcessorOptions, async () => []);
DefaultProcessorOptions,
async () => [],
async (num, dt) => [],
async (issue, label) => new Date().toDateString()
);
// process our fake issue list // process our fake issue list
const operationsLeft = await processor.processIssues(1); const operationsLeft = await processor.processIssues(1);
@@ -65,90 +52,13 @@ test('empty issue list results in 1 operation', async () => {
expect(operationsLeft).toEqual(99); expect(operationsLeft).toEqual(99);
}); });
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', async () => { test('processing an issue with no label will make it stale', async () => {
const TestIssueList: Issue[] = [ const TestIssueList: Issue[] = [
generateIssue(1, 'An issue with no label', '2020-01-01T17:00:00Z') generateIssue(1, 'My first issue', '2020-01-01T17:00:00Z')
]; ];
const opts = {...DefaultProcessorOptions}; const processor = new IssueProcessor(DefaultProcessorOptions, async p =>
opts.daysBeforeClose = 0; p == 1 ? TestIssueList : []
const processor = new IssueProcessor(
opts,
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 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 processor = new IssueProcessor(
DefaultProcessorOptions,
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 not make it stale if days-before-stale is set to -1', async () => {
const TestIssueList: Issue[] = [
generateIssue(1, 'An issue with no label', '2020-01-01T17:00:00Z')
];
const opts = {
...DefaultProcessorOptions,
staleIssueMessage: '',
daysBeforeStale: -1
};
const processor = new IssueProcessor(
opts,
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
let issueDate = new Date();
issueDate.setDate(issueDate.getDate() - 2);
const TestIssueList: Issue[] = [
generateIssue(1, 'An issue with no label', issueDate.toDateString())
];
const processor = new IssueProcessor(
DefaultProcessorOptions,
async p => (p == 1 ? TestIssueList : []),
async (num, dt) => [],
async (issue, label) => new Date().toDateString()
); );
// process our fake issue list // process our fake issue list
@@ -160,20 +70,11 @@ test('processing an issue with no label will make it stale but not close it', as
test('processing a stale issue will close it', async () => { test('processing a stale issue will close it', async () => {
const TestIssueList: Issue[] = [ const TestIssueList: Issue[] = [
generateIssue( generateIssue(1, 'My first issue', '2020-01-01T17:00:00Z', false, ['Stale'])
1,
'A stale issue that should be closed',
'2020-01-01T17:00:00Z',
false,
['Stale']
)
]; ];
const processor = new IssueProcessor( const processor = new IssueProcessor(DefaultProcessorOptions, async p =>
DefaultProcessorOptions, p == 1 ? TestIssueList : []
async p => (p == 1 ? TestIssueList : []),
async (num, dt) => [],
async (issue, label) => new Date().toDateString()
); );
// process our fake issue list // process our fake issue list
@@ -185,197 +86,7 @@ test('processing a stale issue will close it', async () => {
test('processing a stale PR will close it', async () => { test('processing a stale PR will close it', async () => {
const TestIssueList: Issue[] = [ const TestIssueList: Issue[] = [
generateIssue( generateIssue(1, 'My first PR', '2020-01-01T17:00:00Z', true, ['Stale'])
1,
'A stale PR that should be closed',
'2020-01-01T17:00:00Z',
true,
['Stale']
)
];
const processor = new IssueProcessor(
DefaultProcessorOptions,
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 issue 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', false, [
'Stale'
])
];
const opts = {
...DefaultProcessorOptions,
daysBeforeStale: -1,
staleIssueMessage: ''
};
const processor = new IssueProcessor(
opts,
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, [
'Stale'
])
];
const opts = {
...DefaultProcessorOptions,
daysBeforeStale: -1,
stalePrMessage: ''
};
const processor = new IssueProcessor(
opts,
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(
1,
'A closed issue that will not be marked',
'2020-01-01T17:00:00Z',
false,
[],
true
)
];
const processor = new IssueProcessor(
DefaultProcessorOptions,
async p => (p == 1 ? TestIssueList : []),
async (num, dt) => []
);
// process our fake issue list
await processor.processIssues(1);
expect(processor.staleIssues.length).toEqual(0);
expect(processor.closedIssues.length).toEqual(0);
});
test('stale closed issues will not be closed', async () => {
const TestIssueList: Issue[] = [
generateIssue(
1,
'A stale closed issue',
'2020-01-01T17:00:00Z',
false,
['Stale'],
true
)
];
const processor = new IssueProcessor(
DefaultProcessorOptions,
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('closed prs will not be marked stale', async () => {
const TestIssueList: Issue[] = [
generateIssue(
1,
'A closed PR that will not be marked',
'2020-01-01T17:00:00Z',
true,
[],
true
)
];
const processor = new IssueProcessor(
DefaultProcessorOptions,
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('stale closed prs will not be closed', async () => {
const TestIssueList: Issue[] = [
generateIssue(
1,
'A stale closed PR that will not be closed again',
'2020-01-01T17:00:00Z',
true,
['Stale'],
true
)
];
const processor = new IssueProcessor(
DefaultProcessorOptions,
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('locked issues will not be marked stale', async () => {
const TestIssueList: Issue[] = [
generateIssue(
1,
'A locked issue that will not be stale',
'2020-01-01T17:00:00Z',
false,
[],
false,
true
)
]; ];
const processor = new IssueProcessor(DefaultProcessorOptions, async p => const processor = new IssueProcessor(DefaultProcessorOptions, async p =>
@@ -386,85 +97,7 @@ test('locked issues will not be marked stale', async () => {
await processor.processIssues(1); await processor.processIssues(1);
expect(processor.staleIssues.length).toEqual(0); expect(processor.staleIssues.length).toEqual(0);
expect(processor.closedIssues.length).toEqual(0); expect(processor.closedIssues.length).toEqual(1);
});
test('stale locked issues will not be closed', async () => {
const TestIssueList: Issue[] = [
generateIssue(
1,
'A stale locked issue that will not be closed',
'2020-01-01T17:00:00Z',
false,
['Stale'],
false,
true
)
];
const processor = new IssueProcessor(
DefaultProcessorOptions,
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('locked prs will not be marked stale', async () => {
const TestIssueList: Issue[] = [
generateIssue(
1,
'A locked PR that will not be marked stale',
'2020-01-01T17:00:00Z',
true,
[],
false,
true
)
];
const processor = new IssueProcessor(DefaultProcessorOptions, async p =>
p == 1 ? TestIssueList : []
);
// process our fake issue list
await processor.processIssues(1);
expect(processor.staleIssues.length).toEqual(0);
expect(processor.closedIssues.length).toEqual(0);
});
test('stale locked prs will not be closed', async () => {
const TestIssueList: Issue[] = [
generateIssue(
1,
'A stale locked PR that will not be closed',
'2020-01-01T17:00:00Z',
true,
['Stale'],
false,
true
)
];
const processor = new IssueProcessor(
DefaultProcessorOptions,
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('exempt issue labels will not be marked stale', async () => { test('exempt issue labels will not be marked stale', async () => {
@@ -474,14 +107,11 @@ test('exempt issue labels will not be marked stale', async () => {
]) ])
]; ];
const opts = {...DefaultProcessorOptions}; let opts = DefaultProcessorOptions;
opts.exemptIssueLabels = 'Exempt'; opts.exemptIssueLabels = 'Exempt';
const processor = new IssueProcessor( const processor = new IssueProcessor(DefaultProcessorOptions, async p =>
opts, p == 1 ? TestIssueList : []
async p => (p == 1 ? TestIssueList : []),
async (num, dt) => [],
async (issue, label) => new Date().toDateString()
); );
// process our fake issue list // process our fake issue list
@@ -496,14 +126,11 @@ test('exempt issue labels will not be marked stale (multi issue label with space
generateIssue(1, 'My first issue', '2020-01-01T17:00:00Z', false, ['Cool']) generateIssue(1, 'My first issue', '2020-01-01T17:00:00Z', false, ['Cool'])
]; ];
const opts = {...DefaultProcessorOptions}; let opts = DefaultProcessorOptions;
opts.exemptIssueLabels = 'Exempt, Cool, None'; opts.exemptIssueLabels = 'Exempt, Cool, None';
const processor = new IssueProcessor( const processor = new IssueProcessor(DefaultProcessorOptions, async p =>
opts, p == 1 ? TestIssueList : []
async p => (p == 1 ? TestIssueList : []),
async (num, dt) => [],
async (issue, label) => new Date().toDateString()
); );
// process our fake issue list // process our fake issue list
@@ -518,14 +145,11 @@ test('exempt issue labels will not be marked stale (multi issue label)', async (
generateIssue(1, 'My first issue', '2020-01-01T17:00:00Z', false, ['Cool']) generateIssue(1, 'My first issue', '2020-01-01T17:00:00Z', false, ['Cool'])
]; ];
const opts = {...DefaultProcessorOptions}; let opts = DefaultProcessorOptions;
opts.exemptIssueLabels = 'Exempt,Cool,None'; opts.exemptIssueLabels = 'Exempt,Cool,None';
const processor = new IssueProcessor( const processor = new IssueProcessor(DefaultProcessorOptions, async p =>
opts, p == 1 ? TestIssueList : []
async p => (p == 1 ? TestIssueList : []),
async (num, dt) => [],
async (issue, label) => new Date().toDateString()
); );
// process our fake issue list // process our fake issue list
@@ -533,7 +157,6 @@ test('exempt issue labels will not be marked stale (multi issue label)', async (
expect(processor.staleIssues.length).toEqual(0); expect(processor.staleIssues.length).toEqual(0);
expect(processor.closedIssues.length).toEqual(0); expect(processor.closedIssues.length).toEqual(0);
expect(processor.removedLabelIssues.length).toEqual(0);
}); });
test('exempt pr labels will not be marked stale', async () => { test('exempt pr labels will not be marked stale', async () => {
@@ -543,14 +166,11 @@ test('exempt pr labels will not be marked stale', async () => {
generateIssue(3, 'Another issue', '2020-01-01T17:00:00Z', false) generateIssue(3, 'Another issue', '2020-01-01T17:00:00Z', false)
]; ];
const opts = {...DefaultProcessorOptions}; let opts = DefaultProcessorOptions;
opts.exemptIssueLabels = 'Cool'; opts.exemptIssueLabels = 'Cool';
const processor = new IssueProcessor( const processor = new IssueProcessor(DefaultProcessorOptions, async p =>
opts, p == 1 ? TestIssueList : []
async p => (p == 1 ? TestIssueList : []),
async (num, dt) => [],
async (issue, label) => new Date().toDateString()
); );
// process our fake issue list // process our fake issue list
@@ -558,182 +178,3 @@ test('exempt pr labels will not be marked stale', async () => {
expect(processor.staleIssues.length).toEqual(2); // PR should get processed even though it has an exempt **issue** label expect(processor.staleIssues.length).toEqual(2); // PR should get processed even though it has an exempt **issue** label
}); });
test('stale issues should not be closed if days is set to -1', async () => {
const TestIssueList: Issue[] = [
generateIssue(1, 'My first issue', '2020-01-01T17:00:00Z', false, [
'Stale'
]),
generateIssue(2, 'My first PR', '2020-01-01T17:00:00Z', true, ['Stale']),
generateIssue(3, 'Another issue', '2020-01-01T17:00:00Z', false, ['Stale'])
];
const opts = {...DefaultProcessorOptions};
opts.daysBeforeClose = -1;
const processor = new IssueProcessor(
opts,
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.closedIssues.length).toEqual(0);
expect(processor.removedLabelIssues.length).toEqual(0);
});
test('stale label should be removed if a comment was added to a stale issue', async () => {
const TestIssueList: Issue[] = [
generateIssue(
1,
'An issue that should un-stale',
'2020-01-01T17:00:00Z',
false,
['Stale']
)
];
const opts = {...DefaultProcessorOptions};
opts.removeStaleWhenUpdated = true;
const processor = new IssueProcessor(
opts,
async p => (p == 1 ? TestIssueList : []),
async (num, dt) => [{user: {login: 'notme', type: 'User'}}], // return a fake comment to indicate there was an update
async (issue, label) => new Date().toDateString()
);
// process our fake issue list
await processor.processIssues(1);
expect(processor.closedIssues.length).toEqual(0);
expect(processor.staleIssues.length).toEqual(0);
expect(processor.removedLabelIssues.length).toEqual(1);
});
test('stale label should not be removed if a comment was added by the bot (and the issue should be closed)', async () => {
github.context.actor = 'abot';
const TestIssueList: Issue[] = [
generateIssue(
1,
'An issue that should stay stale',
'2020-01-01T17:00:00Z',
false,
['Stale']
)
];
const opts = {...DefaultProcessorOptions};
opts.removeStaleWhenUpdated = true;
const processor = new IssueProcessor(
opts,
async p => (p == 1 ? TestIssueList : []),
async (num, dt) => [{user: {login: 'abot', type: 'User'}}], // return a fake comment to indicate there was an update by the bot
async (issue, label) => new Date().toDateString()
);
// process our fake issue list
await processor.processIssues(1);
expect(processor.closedIssues.length).toEqual(1);
expect(processor.staleIssues.length).toEqual(0);
expect(processor.removedLabelIssues.length).toEqual(0);
});
test('stale issues should not be closed until after the closed number of days', async () => {
let lastUpdate = new Date();
lastUpdate.setDate(lastUpdate.getDate() - 5);
const TestIssueList: Issue[] = [
generateIssue(
1,
'An issue that should be marked stale but not closed',
lastUpdate.toString(),
false
)
];
const opts = {...DefaultProcessorOptions};
opts.daysBeforeStale = 5; // stale after 5 days
opts.daysBeforeClose = 1; // closes after 6 days
const processor = new IssueProcessor(
opts,
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.closedIssues.length).toEqual(0);
expect(processor.removedLabelIssues.length).toEqual(0);
expect(processor.staleIssues.length).toEqual(1);
});
test('stale issues should be closed if the closed nubmer of days (additive) is also passed', async () => {
let lastUpdate = new Date();
lastUpdate.setDate(lastUpdate.getDate() - 7);
const TestIssueList: Issue[] = [
generateIssue(
1,
'An issue that should be stale and closed',
lastUpdate.toString(),
false,
['Stale']
)
];
const opts = {...DefaultProcessorOptions};
opts.daysBeforeStale = 5; // stale after 5 days
opts.daysBeforeClose = 1; // closes after 6 days
const processor = new IssueProcessor(
opts,
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.closedIssues.length).toEqual(1);
expect(processor.removedLabelIssues.length).toEqual(0);
expect(processor.staleIssues.length).toEqual(0);
});
test('stale issues should not be closed until after the closed number of days (long)', async () => {
let lastUpdate = new Date();
lastUpdate.setDate(lastUpdate.getDate() - 10);
const TestIssueList: Issue[] = [
generateIssue(
1,
'An issue that should be marked stale but not closed',
lastUpdate.toString(),
false
)
];
const opts = {...DefaultProcessorOptions};
opts.daysBeforeStale = 5; // stale after 5 days
opts.daysBeforeClose = 20; // closes after 25 days
const processor = new IssueProcessor(
opts,
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.closedIssues.length).toEqual(0);
expect(processor.removedLabelIssues.length).toEqual(0);
expect(processor.staleIssues.length).toEqual(1);
});

View File

@@ -9,12 +9,8 @@ inputs:
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.'
stale-pr-message: stale-pr-message:
description: 'The message to post on the pr when tagging it. If none provided, will not mark pull requests stale.' description: 'The message to post on the pr when tagging it. If none provided, will not mark pull requests stale.'
close-issue-message:
description: 'The message to post on the issue when closing it. If none provided, will not comment when closing an issue.'
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.'
days-before-stale: 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.' description: 'The number of days old an issue can be before marking it stale.'
default: 60 default: 60
days-before-close: 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.' 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.'
@@ -29,7 +25,7 @@ inputs:
description: 'The label to apply when a pull request is stale.' description: 'The label to apply when a pull request is stale.'
default: 'Stale' default: 'Stale'
exempt-pr-labels: exempt-pr-labels:
description: 'The labels to apply when a pull request is exempt from being marked stale. Separate multiple labels with commas (eg. "label1,label2")' description: 'The labels to apply when a pull request is exempt from being marked stale. Separate multiple labels with commas (eg. "label1,label2")''
default: '' default: ''
only-labels: 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.' 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.'
@@ -37,15 +33,9 @@ inputs:
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.'
default: 30 default: 30
remove-stale-when-updated:
description: 'Remove stale labels from issues when they are updated or commented on.'
default: true
debug-only: debug-only:
description: 'Run the processor in debug mode without actually performing any operations on live issues.' description: 'Run the processor in debug mode without actually performing any operations on live issues.'
default: false default: false
ascending:
description: 'The order to get issues or pull requests. Defaults to false, which is descending'
default: false
runs: runs:
using: 'node12' using: 'node12'
main: 'dist/index.js' main: 'dist/index.js'

2358
dist/index.js vendored

File diff suppressed because it is too large Load Diff

71
package-lock.json generated
View File

@@ -5,24 +5,59 @@
"requires": true, "requires": true,
"dependencies": { "dependencies": {
"@actions/core": { "@actions/core": {
"version": "1.2.4", "version": "1.2.3",
"resolved": "https://registry.npmjs.org/@actions/core/-/core-1.2.4.tgz", "resolved": "https://registry.npmjs.org/@actions/core/-/core-1.2.3.tgz",
"integrity": "sha512-YJCEq8BE3CdN8+7HPZ/4DxJjk/OkZV2FFIf+DlZTC/4iBlzYCD5yjRR6eiOS5llO11zbRltIRuKAjMKaWTE6cg==" "integrity": "sha512-Wp4xnyokakM45Uuj4WLUxdsa8fJjKVl1fDTsPbTEcTcuu0Nb26IPQbOtjmnfaCPGcaoPOOqId8H9NapZ8gii4w=="
}, },
"@actions/github": { "@actions/github": {
"version": "2.2.0", "version": "2.1.1",
"resolved": "https://registry.npmjs.org/@actions/github/-/github-2.2.0.tgz", "resolved": "https://registry.npmjs.org/@actions/github/-/github-2.1.1.tgz",
"integrity": "sha512-9UAZqn8ywdR70n3GwVle4N8ALosQs4z50N7XMXrSTUVOmVpaBC5kE3TRTT7qQdi3OaQV24mjGuJZsHUmhD+ZXw==", "integrity": "sha512-kAgTGUx7yf5KQCndVeHSwCNZuDBvPyxm5xKTswW2lofugeuC1AZX73nUUVDNaysnM9aKFMHv9YCdVJbg7syEyA==",
"requires": { "requires": {
"@actions/http-client": "^1.0.3", "@actions/http-client": "^1.0.3",
"@octokit/graphql": "^4.3.1", "@octokit/graphql": "^4.3.1",
"@octokit/rest": "^16.43.1" "@octokit/rest": "^16.43.1"
},
"dependencies": {
"@octokit/request-error": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/@octokit/request-error/-/request-error-1.2.1.tgz",
"integrity": "sha512-+6yDyk1EES6WK+l3viRDElw96MvwfJxCt45GvmjDUKWjYIb3PJZQkq3i46TwGwoPD4h8NmTrENmtyA1FwbmhRA==",
"requires": {
"@octokit/types": "^2.0.0",
"deprecation": "^2.0.0",
"once": "^1.4.0"
}
},
"@octokit/rest": {
"version": "16.43.1",
"resolved": "https://registry.npmjs.org/@octokit/rest/-/rest-16.43.1.tgz",
"integrity": "sha512-gfFKwRT/wFxq5qlNjnW2dh+qh74XgTQ2B179UX5K1HYCluioWj8Ndbgqw2PVqa1NnVJkGHp2ovMpVn/DImlmkw==",
"requires": {
"@octokit/auth-token": "^2.4.0",
"@octokit/plugin-paginate-rest": "^1.1.1",
"@octokit/plugin-request-log": "^1.0.0",
"@octokit/plugin-rest-endpoint-methods": "2.4.0",
"@octokit/request": "^5.2.0",
"@octokit/request-error": "^1.0.2",
"atob-lite": "^2.0.0",
"before-after-hook": "^2.0.0",
"btoa-lite": "^1.0.0",
"deprecation": "^2.0.0",
"lodash.get": "^4.4.2",
"lodash.set": "^4.3.2",
"lodash.uniq": "^4.5.0",
"octokit-pagination-methods": "^1.1.0",
"once": "^1.4.0",
"universal-user-agent": "^4.0.0"
}
}
} }
}, },
"@actions/http-client": { "@actions/http-client": {
"version": "1.0.8", "version": "1.0.7",
"resolved": "https://registry.npmjs.org/@actions/http-client/-/http-client-1.0.8.tgz", "resolved": "https://registry.npmjs.org/@actions/http-client/-/http-client-1.0.7.tgz",
"integrity": "sha512-G4JjJ6f9Hb3Zvejj+ewLLKLf99ZC+9v+yCxoYf9vSyH+WkzPLB2LuUtRMGNkooMqdugGBFStIKXOuvH1W+EctA==", "integrity": "sha512-PY3ys/XH5WMekkHyZhYSa/scYvlE5T/TV/T++vABHuY5ZRgtiBZkn2L2tV5Pv/xDCl59lSZb9WwRuWExDyAsSg==",
"requires": { "requires": {
"tunnel": "0.0.6" "tunnel": "0.0.6"
} }
@@ -522,23 +557,13 @@
} }
}, },
"@octokit/graphql": { "@octokit/graphql": {
"version": "4.4.0", "version": "4.3.1",
"resolved": "https://registry.npmjs.org/@octokit/graphql/-/graphql-4.4.0.tgz", "resolved": "https://registry.npmjs.org/@octokit/graphql/-/graphql-4.3.1.tgz",
"integrity": "sha512-Du3hAaSROQ8EatmYoSAJjzAz3t79t9Opj/WY1zUgxVUGfIKn0AEjg+hlOLscF6fv6i/4y/CeUvsWgIfwMkTccw==", "integrity": "sha512-hCdTjfvrK+ilU2keAdqNBWOk+gm1kai1ZcdjRfB30oA3/T6n53UVJb7w0L5cR3/rhU91xT3HSqCd+qbvH06yxA==",
"requires": { "requires": {
"@octokit/request": "^5.3.0", "@octokit/request": "^5.3.0",
"@octokit/types": "^2.0.0", "@octokit/types": "^2.0.0",
"universal-user-agent": "^5.0.0" "universal-user-agent": "^4.0.0"
},
"dependencies": {
"universal-user-agent": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/universal-user-agent/-/universal-user-agent-5.0.0.tgz",
"integrity": "sha512-B5TPtzZleXyPrUMKCpEHFmVhMN6EhmJYjG5PQna9s7mXeSqGTLap4OpqLl5FCEFUI3UBmllkETwKf/db66Y54Q==",
"requires": {
"os-name": "^3.1.0"
}
}
} }
}, },
"@octokit/plugin-paginate-rest": { "@octokit/plugin-paginate-rest": {

View File

@@ -25,8 +25,8 @@
"author": "GitHub", "author": "GitHub",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@actions/core": "^1.2.4", "@actions/core": "^1.2.3",
"@actions/github": "^2.2.0", "@actions/github": "^2.1.1",
"@octokit/rest": "^16.43.1", "@octokit/rest": "^16.43.1",
"semver": "^6.1.1" "semver": "^6.1.1"
}, },

View File

@@ -2,7 +2,7 @@ import * as core from '@actions/core';
import * as github from '@actions/github'; import * as github from '@actions/github';
import {Octokit} from '@octokit/rest'; import {Octokit} from '@octokit/rest';
type OctoKitIssueList = Octokit.Response<Octokit.IssuesListForRepoResponse>; type OcotoKitIssueList = Octokit.Response<Octokit.IssuesListForRepoResponse>;
export interface Issue { export interface Issue {
title: string; title: string;
@@ -10,23 +10,6 @@ export interface Issue {
updated_at: string; updated_at: string;
labels: Label[]; labels: Label[];
pull_request: any; pull_request: any;
state: string;
locked: boolean;
}
export interface User {
type: string;
login: string;
}
export interface Comment {
user: User;
}
export interface IssueEvent {
created_at: string;
event: string;
label: Label;
} }
export interface Label { export interface Label {
@@ -37,8 +20,6 @@ export interface IssueProcessorOptions {
repoToken: string; repoToken: string;
staleIssueMessage: string; staleIssueMessage: string;
stalePrMessage: string; stalePrMessage: string;
closeIssueMessage: string;
closePrMessage: string;
daysBeforeStale: number; daysBeforeStale: number;
daysBeforeClose: number; daysBeforeClose: number;
staleIssueLabel: string; staleIssueLabel: string;
@@ -47,9 +28,7 @@ export interface IssueProcessorOptions {
exemptPrLabels: string; exemptPrLabels: string;
onlyLabels: string; onlyLabels: string;
operationsPerRun: number; operationsPerRun: number;
removeStaleWhenUpdated: boolean;
debugOnly: boolean; debugOnly: boolean;
ascending: boolean;
} }
/*** /***
@@ -62,19 +41,10 @@ export class IssueProcessor {
readonly staleIssues: Issue[] = []; readonly staleIssues: Issue[] = [];
readonly closedIssues: Issue[] = []; readonly closedIssues: Issue[] = [];
readonly removedLabelIssues: Issue[] = [];
constructor( constructor(
options: IssueProcessorOptions, options: IssueProcessorOptions,
getIssues?: (page: number) => Promise<Issue[]>, getIssues?: (page: number) => Promise<Issue[]>
listIssueComments?: (
issueNumber: number,
sinceDate: string
) => Promise<Comment[]>,
getLabelCreationDate?: (
issue: Issue,
label: string
) => Promise<string | undefined>
) { ) {
this.options = options; this.options = options;
this.operationsLeft = options.operationsPerRun; this.operationsLeft = options.operationsPerRun;
@@ -84,14 +54,6 @@ export class IssueProcessor {
this.getIssues = getIssues; this.getIssues = getIssues;
} }
if (listIssueComments) {
this.listIssueComments = listIssueComments;
}
if (getLabelCreationDate) {
this.getLabelCreationDate = getLabelCreationDate;
}
if (this.options.debugOnly) { if (this.options.debugOnly) {
core.warning( core.warning(
'Executing in debug mode. Debug output will be written but no issues will be processed.' 'Executing in debug mode. Debug output will be written but no issues will be processed.'
@@ -100,19 +62,24 @@ export class IssueProcessor {
} }
async processIssues(page: number = 1): Promise<number> { async processIssues(page: number = 1): Promise<number> {
if (this.operationsLeft <= 0) {
core.warning('Reached max number of operations to process. Exiting.');
return 0;
}
// get the next batch of issues // get the next batch of issues
const issues: Issue[] = await this.getIssues(page); const issues: Issue[] = await this.getIssues(page);
this.operationsLeft -= 1; this.operationsLeft -= 1;
if (issues.length <= 0) { if (issues.length <= 0) {
core.info('No more issues found to process. Exiting.'); core.debug('No more issues found to process. Exiting.');
return this.operationsLeft; return this.operationsLeft;
} }
for (const issue of issues.values()) { for (const issue of issues.values()) {
const isPr = !!issue.pull_request; const isPr = !!issue.pull_request;
core.info( core.debug(
`Found issue: issue #${issue.number} - ${issue.title} last updated ${issue.updated_at} (is pr? ${isPr})` `Found issue: issue #${issue.number} - ${issue.title} last updated ${issue.updated_at} (is pr? ${isPr})`
); );
@@ -120,9 +87,6 @@ export class IssueProcessor {
const staleMessage: string = isPr const staleMessage: string = isPr
? this.options.stalePrMessage ? this.options.stalePrMessage
: this.options.staleIssueMessage; : this.options.staleIssueMessage;
const closeMessage: string = isPr
? this.options.closePrMessage
: this.options.closeIssueMessage;
const staleLabel: string = isPr const staleLabel: string = isPr
? this.options.stalePrLabel ? this.options.stalePrLabel
: this.options.staleIssueLabel; : this.options.staleIssueLabel;
@@ -130,190 +94,69 @@ export class IssueProcessor {
isPr ? this.options.exemptPrLabels : this.options.exemptIssueLabels isPr ? this.options.exemptPrLabels : this.options.exemptIssueLabels
); );
const issueType: string = isPr ? 'pr' : 'issue'; const issueType: string = isPr ? 'pr' : 'issue';
const shouldMarkWhenStale = this.options.daysBeforeStale > -1;
if (!staleMessage && shouldMarkWhenStale) { if (!staleMessage) {
core.info(`Skipping ${issueType} due to empty stale message`); core.debug(`Skipping ${issueType} due to empty stale message`);
continue; continue;
} }
if (issue.state === 'closed') {
core.info(`Skipping ${issueType} because it is closed`);
continue; // don't process closed issues
}
if (issue.locked) {
core.info(`Skipping ${issueType} because it is locked`);
continue; // don't process locked issues
}
if ( if (
exemptLabels.some((exemptLabel: string) => exemptLabels.some((exemptLabel: string) =>
IssueProcessor.isLabeled(issue, exemptLabel) IssueProcessor.isLabeled(issue, exemptLabel)
) )
) { ) {
core.info(`Skipping ${issueType} because it has an exempt label`); core.debug(`Skipping ${issueType} because it has an exempt label`);
continue; // don't process exempt issues continue; // don't process exempt issues
} }
// does this issue have a stale label? if (IssueProcessor.isLabeled(issue, staleLabel)) {
let isStale = IssueProcessor.isLabeled(issue, staleLabel); core.debug(`Found a stale ${issueType}`);
if (
// should this issue be marked stale? this.options.daysBeforeClose >= 0 &&
const shouldBeStale = !IssueProcessor.updatedSince( IssueProcessor.wasLastUpdatedBefore(
issue.updated_at, issue,
this.options.daysBeforeStale this.options.daysBeforeClose
); )
) {
// determine if this issue needs to be marked stale first core.debug(
if (!isStale && shouldBeStale && shouldMarkWhenStale) { `Closing ${issueType} because it was last updated on ${issue.updated_at}`
core.info( );
`Marking ${issueType} stale because it was last updated on ${issue.updated_at} and it does not have a stale label` await this.closeIssue(issue);
this.operationsLeft -= 1;
} else {
core.debug(
`Ignoring stale ${issueType} because it was updated recenlty`
);
}
} else if (
IssueProcessor.wasLastUpdatedBefore(issue, this.options.daysBeforeStale)
) {
core.debug(
`Marking ${issueType} stale because it was last updated on ${issue.updated_at}`
); );
await this.markStale(issue, staleMessage, staleLabel); await this.markStale(issue, staleMessage, staleLabel);
isStale = true; // this issue is now considered stale this.operationsLeft -= 2;
} }
// process the issue if it was marked stale
if (isStale) {
core.info(`Found a stale ${issueType}`);
await this.processStaleIssue(
issue,
issueType,
staleLabel,
closeMessage
);
}
}
if (this.operationsLeft <= 0) {
core.warning('Reached max number of operations to process. Exiting.');
return 0;
} }
// do the next batch // do the next batch
return this.processIssues(page + 1); return this.processIssues(page + 1);
} }
// handle all of the stale issue logic when we find a stale issue
private async processStaleIssue(
issue: Issue,
issueType: string,
staleLabel: string,
closeMessage?: string
) {
const markedStaleOn: string =
(await this.getLabelCreationDate(issue, staleLabel)) || issue.updated_at;
core.info(`Issue #${issue.number} marked stale on: ${markedStaleOn}`);
const issueHasComments: boolean = await this.hasCommentsSince(
issue,
markedStaleOn
);
core.info(
`Issue #${issue.number} has been commented on: ${issueHasComments}`
);
const issueHasUpdate: boolean = IssueProcessor.updatedSince(
issue.updated_at,
this.options.daysBeforeClose
);
core.info(`Issue #${issue.number} has been updated: ${issueHasUpdate}`);
// should we un-stale this issue?
if (this.options.removeStaleWhenUpdated && issueHasComments) {
core.info(
`Issue #${issue.number} is no longer stale. Removing stale label.`
);
await this.removeLabel(issue, staleLabel);
}
// now start closing logic
if (this.options.daysBeforeClose < 0) {
return; // nothing to do because we aren't closing stale issues
}
if (!issueHasComments && !issueHasUpdate) {
core.info(
`Closing ${issueType} because it was last updated on ${issue.updated_at}`
);
await this.closeIssue(issue, closeMessage);
} else {
core.info(
`Stale ${issueType} is not old enough to close yet (hasComments? ${issueHasComments}, hasUpdate? ${issueHasUpdate}`
);
}
}
// checks to see if a given issue is still stale (has had activity on it)
private async hasCommentsSince(
issue: Issue,
sinceDate: string
): Promise<boolean> {
core.info(
`Checking for comments on issue #${issue.number} since ${sinceDate}`
);
if (!sinceDate) {
return true;
}
// find any comments since the date
const comments = await this.listIssueComments(issue.number, sinceDate);
const filteredComments = comments.filter(
comment =>
comment.user.type === 'User' &&
comment.user.login !== github.context.actor
);
core.info(
`Comments not made by ${github.context.actor} or another bot: ${filteredComments.length}`
);
// if there are any user comments returned
return filteredComments.length > 0;
}
// grab comments for an issue since a given date
private async listIssueComments(
issueNumber: number,
sinceDate: string
): Promise<Comment[]> {
// find any comments since date on the given issue
try {
const comments = await this.client.issues.listComments({
owner: github.context.repo.owner,
repo: github.context.repo.repo,
issue_number: issueNumber,
since: sinceDate
});
return comments.data;
} catch (error) {
core.error(`List issue comments error: ${error.message}`);
return Promise.resolve([]);
}
}
// grab issues from github in baches of 100 // grab issues from github in baches of 100
private async getIssues(page: number): Promise<Issue[]> { private async getIssues(page: number): Promise<Issue[]> {
try { const issueResult: OcotoKitIssueList = await this.client.issues.listForRepo(
const issueResult: OctoKitIssueList = await this.client.issues.listForRepo( {
{ owner: github.context.repo.owner,
owner: github.context.repo.owner, repo: github.context.repo.repo,
repo: github.context.repo.repo, state: 'open',
state: 'open', labels: this.options.onlyLabels,
labels: this.options.onlyLabels, per_page: 100,
per_page: 100, page
direction: this.options.ascending ? 'asc' : 'desc', }
page );
}
); return issueResult.data;
return issueResult.data;
} catch (error) {
core.error(`Get issues for repo error: ${error.message}`);
return Promise.resolve([]);
}
} }
// Mark an issue as stale with a comment and a label // Mark an issue as stale with a comment and a label
@@ -322,139 +165,47 @@ export class IssueProcessor {
staleMessage: string, staleMessage: string,
staleLabel: string staleLabel: string
): Promise<void> { ): Promise<void> {
core.info(`Marking issue #${issue.number} - ${issue.title} as stale`); core.debug(`Marking issue #${issue.number} - ${issue.title} as stale`);
this.staleIssues.push(issue); this.staleIssues.push(issue);
this.operationsLeft -= 2;
// if the issue is being marked stale, the updated date should be changed to right now
// so that close calculations work correctly
const newUpdatedAtDate: Date = new Date();
issue.updated_at = newUpdatedAtDate.toString();
if (this.options.debugOnly) { if (this.options.debugOnly) {
return; return;
} }
try { await this.client.issues.createComment({
await this.client.issues.createComment({ owner: github.context.repo.owner,
owner: github.context.repo.owner, repo: github.context.repo.repo,
repo: github.context.repo.repo, issue_number: issue.number,
issue_number: issue.number, body: staleMessage
body: staleMessage });
});
} catch (error) {
core.error(`Error creating a comment: ${error.message}`);
}
try { await this.client.issues.addLabels({
await this.client.issues.addLabels({ owner: github.context.repo.owner,
owner: github.context.repo.owner, repo: github.context.repo.repo,
repo: github.context.repo.repo, issue_number: issue.number,
issue_number: issue.number, labels: [staleLabel]
labels: [staleLabel] });
});
} catch (error) {
core.error(`Error adding a label: ${error.message}`);
}
} }
// Close an issue based on staleness /// Close an issue based on staleness
private async closeIssue(issue: Issue, closeMessage?: string): Promise<void> { private async closeIssue(issue: Issue): Promise<void> {
core.info( core.debug(
`Closing issue #${issue.number} - ${issue.title} for being stale` `Closing issue #${issue.number} - ${issue.title} for being stale`
); );
this.closedIssues.push(issue); this.closedIssues.push(issue);
this.operationsLeft -= 1;
if (this.options.debugOnly) { if (this.options.debugOnly) {
return; return;
} }
if (closeMessage) { await this.client.issues.update({
try {
await this.client.issues.createComment({
owner: github.context.repo.owner,
repo: github.context.repo.repo,
issue_number: issue.number,
body: closeMessage
});
} catch (error) {
core.error(`Error creating a comment: ${error.message}`);
}
}
try {
await this.client.issues.update({
owner: github.context.repo.owner,
repo: github.context.repo.repo,
issue_number: issue.number,
state: 'closed'
});
} catch (error) {
core.error(`Error updating an issue: ${error.message}`);
}
}
// Remove a label from an issue
private async removeLabel(issue: Issue, label: string): Promise<void> {
core.info(
`Removing label ${label} from issue #${issue.number} - ${issue.title}`
);
this.removedLabelIssues.push(issue);
this.operationsLeft -= 1;
if (this.options.debugOnly) {
return;
}
try {
await this.client.issues.removeLabel({
owner: github.context.repo.owner,
repo: github.context.repo.repo,
issue_number: issue.number,
name: encodeURIComponent(label) // A label can have a "?" in the name
});
} catch (error) {
core.error(`Error removing a label: ${error.message}`);
}
}
// returns the creation date of a given label on an issue (or nothing if no label existed)
///see https://developer.github.com/v3/activity/events/
private async getLabelCreationDate(
issue: Issue,
label: string
): Promise<string | undefined> {
core.info(`Checking for label ${label} on issue #${issue.number}`);
this.operationsLeft -= 1;
const options = this.client.issues.listEvents.endpoint.merge({
owner: github.context.repo.owner, owner: github.context.repo.owner,
repo: github.context.repo.repo, repo: github.context.repo.repo,
per_page: 100, issue_number: issue.number,
issue_number: issue.number state: 'closed'
}); });
const events: IssueEvent[] = await this.client.paginate(options);
const reversedEvents = events.reverse();
const staleLabeledEvent = reversedEvents.find(
event => event.event === 'labeled' && event.label.name === label
);
if (!staleLabeledEvent) {
// Must be old rather than labeled
return undefined;
}
return staleLabeledEvent.created_at;
} }
private static isLabeled(issue: Issue, label: string): boolean { private static isLabeled(issue: Issue, label: string): boolean {
@@ -463,12 +214,11 @@ export class IssueProcessor {
return issue.labels.filter(labelComparer).length > 0; return issue.labels.filter(labelComparer).length > 0;
} }
private static updatedSince(timestamp: string, num_days: number): boolean { private static wasLastUpdatedBefore(issue: Issue, num_days: number): boolean {
const daysInMillis = 1000 * 60 * 60 * 24 * num_days; const daysInMillis = 1000 * 60 * 60 * 24 * num_days;
const millisSinceLastUpdated = const millisSinceLastUpdated =
new Date().getTime() - new Date(timestamp).getTime(); new Date().getTime() - new Date(issue.updated_at).getTime();
return millisSinceLastUpdated >= daysInMillis;
return millisSinceLastUpdated <= daysInMillis;
} }
private static parseCommaSeparatedString(s: string): string[] { private static parseCommaSeparatedString(s: string): string[] {

View File

@@ -18,8 +18,6 @@ function getAndValidateArgs(): IssueProcessorOptions {
repoToken: core.getInput('repo-token', {required: true}), repoToken: core.getInput('repo-token', {required: true}),
staleIssueMessage: core.getInput('stale-issue-message'), staleIssueMessage: core.getInput('stale-issue-message'),
stalePrMessage: core.getInput('stale-pr-message'), stalePrMessage: core.getInput('stale-pr-message'),
closeIssueMessage: core.getInput('close-issue-message'),
closePrMessage: core.getInput('close-pr-message'),
daysBeforeStale: parseInt( daysBeforeStale: parseInt(
core.getInput('days-before-stale', {required: true}) core.getInput('days-before-stale', {required: true})
), ),
@@ -34,11 +32,7 @@ function getAndValidateArgs(): IssueProcessorOptions {
operationsPerRun: parseInt( operationsPerRun: parseInt(
core.getInput('operations-per-run', {required: true}) core.getInput('operations-per-run', {required: true})
), ),
removeStaleWhenUpdated: !( debugOnly: core.getInput('debug-only') === 'true'
core.getInput('remove-stale-when-updated') === 'false'
),
debugOnly: core.getInput('debug-only') === 'true',
ascending: core.getInput('ascending') === 'true'
}; };
for (const numberInput of [ for (const numberInput of [

View File

@@ -6,8 +6,7 @@
"rootDir": "./src", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */ "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. */ "strict": true, /* Enable all strict type-checking options. */
"noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */ "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'. */ "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"] "exclude": ["node_modules", "**/*.test.ts"]
} }