Feat: add any-of-labels option (#319)

* feat: add any-of-labels option

* chore: run pack script

* fix: error in milestones spec

* chore: update readme

* chore: fix default value in action.yml

* chore: add some unit tests

* docs: update README.md

Co-authored-by: Geoffrey Testelin <geoffrey.testelin@gmail.com>

* refactor: add return type to lambda

Co-authored-by: Geoffrey Testelin <geoffrey.testelin@gmail.com>
This commit is contained in:
Jose Veiga 2021-03-01 11:05:53 -05:00 committed by GitHub
parent 8f5f223d0c
commit 63ae8ac024
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 205 additions and 43 deletions

View File

@ -5,7 +5,7 @@ Warns and then closes issues and PRs that have had no activity for a specified a
### Arguments ### Arguments
| Input | Description | Usage | | Input | Description | Usage |
| ----------------------------- | --------------------------------------------------------------------------------------------------------------- | -------- | | ----------------------------- | ----------------------------------------------------------------------------------------------------------------------------------- | -------- |
| `repo-token` | PAT(Personal Access Token) for authorizing repository. _Defaults to **${{ github.token }}**_ | Optional | | `repo-token` | PAT(Personal Access Token) for authorizing repository. _Defaults to **${{ github.token }}**_ | Optional |
| `days-before-stale` | Idle number of days before marking an issue/PR as stale. _Defaults to **60**_ | Optional | | `days-before-stale` | Idle number of days before marking an issue/PR as stale. _Defaults to **60**_ | Optional |
| `days-before-issue-stale` | Idle number of days before marking an issue as stale (override `days-before-stale`). | Optional | | `days-before-issue-stale` | Idle number of days before marking an issue as stale (override `days-before-stale`). | Optional |
@ -23,9 +23,17 @@ Warns and then closes issues and PRs that have had no activity for a specified a
| `close-pr-label` | Label to apply on the closing PR (automatically removed if no longer closed nor locked). | Optional | | `close-pr-label` | Label to apply on the closing PR (automatically removed if no longer closed nor locked). | Optional |
| `exempt-issue-labels` | Labels on an issue exempted from being marked as stale. | Optional | | `exempt-issue-labels` | Labels on an issue exempted from being marked as stale. | Optional |
| `exempt-pr-labels` | Labels on the PR exempted from being marked as stale. | Optional | | `exempt-pr-labels` | Labels on the PR exempted from being marked as stale. | Optional |
| `exempt-milestones` | Milestones on an issue or a PR exempted from being marked as stale. | Optional |
| `exempt-issue-milestones` | Milestones on an issue exempted from being marked as stale (override `exempt-milestones`). | Optional |
| `exempt-pr-milestones` | Milestones on the PR exempted from being marked as stale (override `exempt-milestones`). | Optional |
| `exempt-all-milestones` | Exempt all issues and PRs with milestones from being marked as stale. (priority over `exempt-milestones` rules) | Optional |
| `exempt-all-issue-milestones` | Exempt all issues with milestones from being marked as stale. (override `exempt-all-milestones`). | Optional |
| `exempt-all-pr-milestones` | Exempt all PRs with milestones from being marked as stale. (override `exempt-all-milestones`). | Optional |
| `only-labels` | Only issues and PRs with ALL these labels are checked. Separate multiple labels with commas (eg. "question,answered"). | Optional |
| `only-labels` | Only labels checked for stale issue/PR. | Optional | | `only-labels` | Only labels checked for stale issue/PR. | Optional |
| `only-issue-labels` | Only labels checked for stale issue (override `only-labels`). | Optional | | `only-issue-labels` | Only labels checked for stale issue (override `only-labels`). | Optional |
| `only-pr-labels` | Only labels checked for stale PR (override `only-labels`). | Optional | | `only-pr-labels` | Only labels checked for stale PR (override `only-labels`). | Optional |
| `any-of-labels` | Only issues and PRs with ANY of these labels are checked. Separate multiple labels with commas (eg. "incomplete,waiting-feedback"). | Optional |
| `operations-per-run` | Maximum number of operations per run (GitHub API CRUD related). _Defaults to **30**_ | Optional | | `operations-per-run` | Maximum number of operations per run (GitHub API CRUD related). _Defaults to **30**_ | Optional |
| `remove-stale-when-updated` | Remove stale label from issue/PR on updates or comments. _Defaults to **true**_ | Optional | | `remove-stale-when-updated` | Remove stale label from issue/PR on updates or comments. _Defaults to **true**_ | Optional |
| `debug-only` | Dry-run on action. _Defaults to **false**_ | Optional | | `debug-only` | Dry-run on action. _Defaults to **false**_ | Optional |
@ -211,6 +219,25 @@ jobs:
exempt-all-pr-milestones: true exempt-all-pr-milestones: true
``` ```
Avoid stale for specific labels:
```yaml
name: 'Close stale issues and PRs'
on:
schedule:
- cron: '30 1 * * *'
jobs:
stale:
runs-on: ubuntu-latest
steps:
- uses: actions/stale@v3
with:
any-of-labels: 'needs-more-info,needs-demo'
# You can opt for 'only-labels' instead if your usecase requires all labels
# to be present in the issue/PR
```
Avoid stale for specific assignees: Avoid stale for specific assignees:
```yaml ```yaml

View File

@ -0,0 +1,107 @@
import {Issue} from '../src/classes/issue';
import {IssuesProcessor} from '../src/classes/issues-processor';
import {IIssuesProcessorOptions} from '../src/interfaces/issues-processor-options';
import {DefaultProcessorOptions} from './constants/default-processor-options';
import {generateIssue} from './functions/generate-issue';
describe('any-of-labels option', () => {
test('should do nothing when not set', async () => {
const sut = new IssuesProcessorBuilder()
.emptyAnyOfLabels()
.issues([{labels: [{name: 'some-label'}]}])
.build();
await sut.processIssues();
expect(sut.staleIssues).toHaveLength(1);
});
test('should skip it when none of the issue labels match', async () => {
const sut = new IssuesProcessorBuilder()
.anyOfLabels('skip-this-issue,and-this-one')
.issues([{labels: [{name: 'some-label'}, {name: 'some-other-label'}]}])
.build();
await sut.processIssues();
expect(sut.staleIssues).toHaveLength(0);
});
test('should skip it when the issue has no labels', async () => {
const sut = new IssuesProcessorBuilder()
.anyOfLabels('skip-this-issue,and-this-one')
.issues([{labels: []}])
.build();
await sut.processIssues();
expect(sut.staleIssues).toHaveLength(0);
});
test('should process it when one of the issue labels match', async () => {
const sut = new IssuesProcessorBuilder()
.anyOfLabels('skip-this-issue,and-this-one')
.issues([{labels: [{name: 'some-label'}, {name: 'skip-this-issue'}]}])
.build();
await sut.processIssues();
expect(sut.staleIssues).toHaveLength(1);
});
test('should process it when all the issue labels match', async () => {
const sut = new IssuesProcessorBuilder()
.anyOfLabels('skip-this-issue,and-this-one')
.issues([{labels: [{name: 'and-this-one'}, {name: 'skip-this-issue'}]}])
.build();
await sut.processIssues();
expect(sut.staleIssues).toHaveLength(1);
});
});
class IssuesProcessorBuilder {
private _options: IIssuesProcessorOptions;
private _issues: Issue[];
constructor() {
this._options = {...DefaultProcessorOptions};
this._issues = [];
}
anyOfLabels(labels: string): IssuesProcessorBuilder {
this._options.anyOfLabels = labels;
return this;
}
emptyAnyOfLabels(): IssuesProcessorBuilder {
return this.anyOfLabels('');
}
issues(issues: Partial<Issue>[]): IssuesProcessorBuilder {
this._issues = issues.map(
(issue, index): Issue =>
generateIssue(
this._options,
index,
issue.title || 'Issue title',
issue.updated_at || '2000-01-01T00:00:00Z', // we only care about stale/expired issues here
issue.created_at || '2000-01-01T00:00:00Z',
issue.isPullRequest || false,
issue.labels ? issue.labels.map(label => label.name) : []
)
);
return this;
}
build(): IssuesProcessor {
return new IssuesProcessor(
this._options,
async () => 'abot',
async p => (p === 1 ? this._issues : []),
async () => [],
async () => new Date().toDateString()
);
}
}

View File

@ -21,6 +21,7 @@ export const DefaultProcessorOptions: IIssuesProcessorOptions = Object.freeze({
onlyLabels: '', onlyLabels: '',
onlyIssueLabels: '', onlyIssueLabels: '',
onlyPrLabels: '', onlyPrLabels: '',
anyOfLabels: '',
operationsPerRun: 100, operationsPerRun: 100,
debugOnly: true, debugOnly: true,
removeStaleWhenUpdated: false, removeStaleWhenUpdated: false,

View File

@ -85,7 +85,11 @@ inputs:
default: '' default: ''
required: false required: false
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.'
default: ''
required: false
any-of-labels:
description: 'Only issues or pull requests with at least one of these labels are checked if stale. Defaults to `` (disabled) and can be a comma-separated list of labels.'
default: '' default: ''
required: false required: false
only-issue-labels: only-issue-labels:

7
dist/index.js vendored
View File

@ -350,6 +350,12 @@ class IssuesProcessor {
issueLogger.info(`Skipping $$type because it has an exempt label`); issueLogger.info(`Skipping $$type because it has an exempt label`);
continue; // don't process exempt issues continue; // don't process exempt issues
} }
const anyOfLabels = words_to_list_1.wordsToList(this.options.anyOfLabels);
if (anyOfLabels.length &&
!anyOfLabels.some((label) => is_labeled_1.isLabeled(issue, label))) {
issueLogger.info(`Skipping ${issueType} because it does not have any of the required labels`);
continue; // don't process issues without any of the required labels
}
const milestones = new milestones_1.Milestones(this.options, issue); const milestones = new milestones_1.Milestones(this.options, issue);
if (milestones.shouldExemptMilestones()) { if (milestones.shouldExemptMilestones()) {
issueLogger.info(`Skipping $$type because it has an exempted milestone`); issueLogger.info(`Skipping $$type because it has an exempted milestone`);
@ -1201,6 +1207,7 @@ function _getAndValidateArgs() {
onlyLabels: core.getInput('only-labels'), onlyLabels: core.getInput('only-labels'),
onlyIssueLabels: core.getInput('only-issue-labels'), onlyIssueLabels: core.getInput('only-issue-labels'),
onlyPrLabels: core.getInput('only-pr-labels'), onlyPrLabels: core.getInput('only-pr-labels'),
anyOfLabels: core.getInput('any-of-labels'),
operationsPerRun: parseInt(core.getInput('operations-per-run', { required: true })), operationsPerRun: parseInt(core.getInput('operations-per-run', { required: true })),
removeStaleWhenUpdated: !(core.getInput('remove-stale-when-updated') === 'false'), removeStaleWhenUpdated: !(core.getInput('remove-stale-when-updated') === 'false'),
debugOnly: core.getInput('debug-only') === 'true', debugOnly: core.getInput('debug-only') === 'true',

View File

@ -30,6 +30,7 @@ describe('Issue', (): void => {
onlyLabels: '', onlyLabels: '',
onlyIssueLabels: '', onlyIssueLabels: '',
onlyPrLabels: '', onlyPrLabels: '',
anyOfLabels: '',
operationsPerRun: 0, operationsPerRun: 0,
removeStaleWhenUpdated: false, removeStaleWhenUpdated: false,
repoToken: '', repoToken: '',

View File

@ -230,6 +230,19 @@ export class IssuesProcessor {
continue; // don't process exempt issues continue; // don't process exempt issues
} }
const anyOfLabels: string[] = wordsToList(this.options.anyOfLabels);
if (
anyOfLabels.length &&
!anyOfLabels.some((label: Readonly<string>): boolean =>
isLabeled(issue, label)
)
) {
issueLogger.info(
`Skipping ${issueType} because it does not have any of the required labels`
);
continue; // don't process issues without any of the required labels
}
const milestones: Milestones = new Milestones(this.options, issue); const milestones: Milestones = new Milestones(this.options, issue);
if (milestones.shouldExemptMilestones()) { if (milestones.shouldExemptMilestones()) {

View File

@ -21,6 +21,7 @@ export interface IIssuesProcessorOptions {
onlyLabels: string; onlyLabels: string;
onlyIssueLabels: string; onlyIssueLabels: string;
onlyPrLabels: string; onlyPrLabels: string;
anyOfLabels: string;
operationsPerRun: number; operationsPerRun: number;
removeStaleWhenUpdated: boolean; removeStaleWhenUpdated: boolean;
debugOnly: boolean; debugOnly: boolean;

View File

@ -41,6 +41,7 @@ function _getAndValidateArgs(): IIssuesProcessorOptions {
onlyLabels: core.getInput('only-labels'), onlyLabels: core.getInput('only-labels'),
onlyIssueLabels: core.getInput('only-issue-labels'), onlyIssueLabels: core.getInput('only-issue-labels'),
onlyPrLabels: core.getInput('only-pr-labels'), onlyPrLabels: core.getInput('only-pr-labels'),
anyOfLabels: core.getInput('any-of-labels'),
operationsPerRun: parseInt( operationsPerRun: parseInt(
core.getInput('operations-per-run', {required: true}) core.getInput('operations-per-run', {required: true})
), ),