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:
parent
8f5f223d0c
commit
63ae8ac024
29
README.md
29
README.md
|
@ -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
|
||||||
|
|
|
@ -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()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
|
@ -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,
|
||||||
|
|
|
@ -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:
|
||||||
|
|
|
@ -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',
|
||||||
|
|
|
@ -30,6 +30,7 @@ describe('Issue', (): void => {
|
||||||
onlyLabels: '',
|
onlyLabels: '',
|
||||||
onlyIssueLabels: '',
|
onlyIssueLabels: '',
|
||||||
onlyPrLabels: '',
|
onlyPrLabels: '',
|
||||||
|
anyOfLabels: '',
|
||||||
operationsPerRun: 0,
|
operationsPerRun: 0,
|
||||||
removeStaleWhenUpdated: false,
|
removeStaleWhenUpdated: false,
|
||||||
repoToken: '',
|
repoToken: '',
|
||||||
|
|
|
@ -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()) {
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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})
|
||||||
),
|
),
|
||||||
|
|
Loading…
Reference in New Issue