Skip to content

Commit fe80985

Browse files
committed
Retrieve Issues and PRs using a search filter
Currently all open issues for context.repo.owner and context.repo.repo are retrieved using a simple call to client.rest.issues.listForRepo(); If we wanted to add other critera to determine staleness, like only considering PRs with a review state of "changes_requested", we'd have to make additional rest calls to get the reviews for each PR. This is fine but it only solves the issue for review state. Instead, this PR introduces a new action parameter named `only-matching-filter` which takes one or more standard GitHub Issue and Pull Request search strings. So instead of retrieving all open issues and PRs, you can limit the set to operate on by any criteria that GitHub supports. In the process, it opens up the ability to expand the set to include an entire organization or owner instead of just one repo. Example: Retrieve all open PRs for organization "myorg" that are in review state "changes_requested": `only-matching-filter: 'org:myorg is:pr is:open review:changes_requested'` Once that set is retrieved, all the other label, milestone, assignee, date, etc. filters are applied as usual. Although GitHub only allows boolean search critera in a Code search, you an get around that somewhat by specifying multiple search strings separated by ` || `. Example: Retrieve all open PRs for organization "myorg" that are in review state "changes_requested" or that have the label `submitter-action-required` assigned: (split onto two lines for clarity) ``` only-matching-filter: 'org:myorg is:pr is:open review:changes_requested || org:myorg is:pr is:open label:submitter-action-required' ``` Again, once that set is retrieved and duplicates filtered out, all the other label, milestone, assignee, date, etc. filters are applied as usual. If there aren't any `owner`, `org`, `user` or `repo` search terms in the filters, the search is automatically scoped to the context owner and repo. This prevents accidental global searches. `is:open` is also added if not already present. Resolves: #1143
1 parent 3f3b017 commit fe80985

15 files changed

+246
-50
lines changed

README.md

+19
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@ Every argument is optional.
6060
| [close-issue-reason](#close-issue-reason) | Reason to use when closing issues | `not_planned` |
6161
| [stale-pr-label](#stale-pr-label) | Label to apply on staled PRs | `Stale` |
6262
| [close-pr-label](#close-pr-label) | Label to apply on closed PRs | |
63+
| [only-matching-filter](#only-matching-filter) | Only issues/PRs matching the search filter(s) will be retrieved and tested | |
6364
| [exempt-issue-labels](#exempt-issue-labels) | Labels on issues exempted from stale | |
6465
| [exempt-pr-labels](#exempt-pr-labels) | Labels on PRs exempted from stale | |
6566
| [only-labels](#only-labels) | Only issues/PRs with ALL these labels are checked | |
@@ -258,6 +259,24 @@ It will be automatically removed if the pull requests are no longer closed nor l
258259
Default value: unset
259260
Required Permission: `pull-requests: write`
260261

262+
#### only-matching-filter
263+
264+
One or more standard [GitHub Issues and Pull Requests search filters](https://docs.github.com/en/search-github/searching-on-github/searching-issues-and-pull-requests)
265+
which will be used to retrieve the set of issues/PRs to test and take action on. Normally, all open issues/PRs in the context's owner/repo are retrieved.
266+
267+
GitHub only allows boolean logic and grouping in a Code Search not in Issues and Pull Requests search so there's no way to do an "OR" operation but you can get around this to
268+
a limited degree by specifying multiple search requests separated by ` || `. Each request is run separately and the results are accumulated and duplicates
269+
removed before any further processing is done.
270+
271+
Each request is checked to ensure it contains an `owner:`, `org:`, `user:` or `repo:` search term. If it doesn't, the search will automatically be scoped to
272+
the owner and repository in the context. This prevents accidental global searches. If the request doesn't already contain an `is:open` search term, it will automatically be added as well.
273+
274+
Example: To retrieve all of the open PRs in your organization that have a review state of `changes_requested` or a label named `submitter-action-required`, you'd use:
275+
`only-matching-filter: 'org:myorg is:pr is:open review:changes_requested || org:myorg is:pr is:open label:submitter-action-required'`.
276+
From this set, all of the other label, milestone, date, assignee, etc. filters will be applied before taking any action.
277+
278+
Default value: unset
279+
261280
#### exempt-issue-labels
262281
263282
Comma separated list of labels that can be assigned to issues to exclude them from being marked as stale

__tests__/constants/default-processor-options.ts

+1
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ export const DefaultProcessorOptions: IIssuesProcessorOptions = Object.freeze({
1919
exemptIssueLabels: '',
2020
stalePrLabel: 'Stale',
2121
closePrLabel: '',
22+
onlyMatchingFilter: '',
2223
exemptPrLabels: '',
2324
onlyLabels: '',
2425
onlyIssueLabels: '',

__tests__/functions/generate-issue.ts

+2-1
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ export function generateIssue(
3939
login: assignee,
4040
type: 'User'
4141
};
42-
})
42+
}),
43+
repository_url: 'https://api.github.com/repos/dummy/dummy'
4344
});
4445
}

action.yml

+4
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,10 @@ inputs:
4545
close-issue-label:
4646
description: 'The label to apply when an issue is closed.'
4747
required: false
48+
only-matching-filter:
49+
description: 'Only issues/PRs matching the search filter(s) will be retrieved and tested'
50+
default: ''
51+
required: false
4852
exempt-issue-labels:
4953
description: 'The labels that mean an issue is exempt from being marked stale. Separate multiple labels with commas (eg. "label1,label2").'
5054
default: ''

dist/index.js

+103-24
Original file line numberDiff line numberDiff line change
@@ -272,6 +272,7 @@ exports.Issue = void 0;
272272
const is_labeled_1 = __nccwpck_require__(6792);
273273
const is_pull_request_1 = __nccwpck_require__(5400);
274274
const operations_1 = __nccwpck_require__(7957);
275+
const owner_repo_1 = __nccwpck_require__(6226);
275276
class Issue {
276277
constructor(options, issue) {
277278
this.operations = new operations_1.Operations();
@@ -287,8 +288,10 @@ class Issue {
287288
this.locked = issue.locked;
288289
this.milestone = issue.milestone;
289290
this.assignees = issue.assignees || [];
291+
this.repository_url = issue.repository_url;
290292
this.isStale = (0, is_labeled_1.isLabeled)(this, this.staleLabel);
291293
this.markedStaleThisRun = false;
294+
this.owner_repo = new owner_repo_1.OwnerRepo(issue.repository_url || '');
292295
}
293296
get isPullRequest() {
294297
return (0, is_pull_request_1.isPullRequest)(this);
@@ -426,7 +429,7 @@ class IssuesProcessor {
426429
var _a, _b;
427430
return __awaiter(this, void 0, void 0, function* () {
428431
// get the next batch of issues
429-
const issues = yield this.getIssues(page);
432+
const issues = yield this.getIssuesWrapper(page);
430433
if (issues.length <= 0) {
431434
this._logger.info(logger_service_1.LoggerService.green(`No more issues found to process. Exiting...`));
432435
(_a = this.statistics) === null || _a === void 0 ? void 0 : _a.setOperationsCount(this.operations.getConsumedOperationsCount()).logStats();
@@ -659,8 +662,8 @@ class IssuesProcessor {
659662
this._consumeIssueOperation(issue);
660663
(_a = this.statistics) === null || _a === void 0 ? void 0 : _a.incrementFetchedItemsCommentsCount();
661664
const comments = yield this.client.rest.issues.listComments({
662-
owner: github_1.context.repo.owner,
663-
repo: github_1.context.repo.repo,
665+
owner: issue.owner_repo.owner,
666+
repo: issue.owner_repo.repo,
664667
issue_number: issue.number,
665668
since: sinceDate
666669
});
@@ -687,13 +690,62 @@ class IssuesProcessor {
687690
page
688691
});
689692
(_a = this.statistics) === null || _a === void 0 ? void 0 : _a.incrementFetchedItemsCount(issueResult.data.length);
693+
this._logger.info(logger_service_1.LoggerService.green(`Retrieved ${issueResult.data.length} issues/PRs for repo ${github_1.context.repo.owner}/${github_1.context.repo.repo}`));
690694
return issueResult.data.map((issue) => new issue_1.Issue(this.options, issue));
691695
}
692696
catch (error) {
693697
throw Error(`Getting issues was blocked by the error: ${error.message}`);
694698
}
695699
});
696700
}
701+
// grab issues and/or prs from github in batches of 100 using search filter
702+
getIssuesByFilter(page, search) {
703+
var _a;
704+
return __awaiter(this, void 0, void 0, function* () {
705+
try {
706+
this.operations.consumeOperation();
707+
const issueResult = yield this.client.rest.search.issuesAndPullRequests({
708+
q: search,
709+
per_page: 100,
710+
direction: this.options.ascending ? 'asc' : 'desc',
711+
page
712+
});
713+
(_a = this.statistics) === null || _a === void 0 ? void 0 : _a.incrementFetchedItemsCount(issueResult.data.total_count);
714+
this._logger.info(logger_service_1.LoggerService.green(`Retrieved ${issueResult.data.total_count} issues/PRs for search '${search}'`));
715+
return issueResult.data.items.map((issue) => new issue_1.Issue(this.options, issue));
716+
}
717+
catch (error) {
718+
throw Error(`Getting issues was blocked by the error: ${error.message}`);
719+
}
720+
});
721+
}
722+
_removeDupIssues(issues) {
723+
return issues.reduce(function (a, b) {
724+
if (!a.find(o => o.number == b.number))
725+
a.push(b);
726+
return a;
727+
}, []);
728+
}
729+
getIssuesWrapper(page) {
730+
return __awaiter(this, void 0, void 0, function* () {
731+
if (!this.options.onlyMatchingFilter) {
732+
return this.getIssues(page);
733+
}
734+
const filter = this.options.onlyMatchingFilter;
735+
const results = [];
736+
for (let term of filter.split('||')) {
737+
if (term.search(/repo:|owner:|org:|user:/) < 0) {
738+
term = `repo:${github_1.context.repo.owner}/${github_1.context.repo.repo} ${this.options.onlyMatchingFilter}`;
739+
}
740+
if (term.search(/is:open/) < 0) {
741+
term += ' is:open';
742+
}
743+
const r = yield this.getIssuesByFilter(page, term);
744+
results.push(...r);
745+
}
746+
return this._removeDupIssues(results);
747+
});
748+
}
697749
// returns the creation date of a given label on an issue (or nothing if no label existed)
698750
///see https://developer.github.com/v3/activity/events/
699751
getLabelCreationDate(issue, label) {
@@ -704,8 +756,8 @@ class IssuesProcessor {
704756
this._consumeIssueOperation(issue);
705757
(_a = this.statistics) === null || _a === void 0 ? void 0 : _a.incrementFetchedItemsEventsCount();
706758
const options = this.client.rest.issues.listEvents.endpoint.merge({
707-
owner: github_1.context.repo.owner,
708-
repo: github_1.context.repo.repo,
759+
owner: issue.owner_repo.owner,
760+
repo: issue.owner_repo.repo,
709761
per_page: 100,
710762
issue_number: issue.number
711763
});
@@ -728,8 +780,8 @@ class IssuesProcessor {
728780
this._consumeIssueOperation(issue);
729781
(_a = this.statistics) === null || _a === void 0 ? void 0 : _a.incrementFetchedPullRequestsCount();
730782
const pullRequest = yield this.client.rest.pulls.get({
731-
owner: github_1.context.repo.owner,
732-
repo: github_1.context.repo.repo,
783+
owner: issue.owner_repo.owner,
784+
repo: issue.owner_repo.repo,
733785
pull_number: issue.number
734786
});
735787
return pullRequest.data;
@@ -848,8 +900,8 @@ class IssuesProcessor {
848900
(_a = this.statistics) === null || _a === void 0 ? void 0 : _a.incrementAddedItemsComment(issue);
849901
if (!this.options.debugOnly) {
850902
yield this.client.rest.issues.createComment({
851-
owner: github_1.context.repo.owner,
852-
repo: github_1.context.repo.repo,
903+
owner: issue.owner_repo.owner,
904+
repo: issue.owner_repo.repo,
853905
issue_number: issue.number,
854906
body: staleMessage
855907
});
@@ -865,8 +917,8 @@ class IssuesProcessor {
865917
(_c = this.statistics) === null || _c === void 0 ? void 0 : _c.incrementStaleItemsCount(issue);
866918
if (!this.options.debugOnly) {
867919
yield this.client.rest.issues.addLabels({
868-
owner: github_1.context.repo.owner,
869-
repo: github_1.context.repo.repo,
920+
owner: issue.owner_repo.owner,
921+
repo: issue.owner_repo.repo,
870922
issue_number: issue.number,
871923
labels: [staleLabel]
872924
});
@@ -891,8 +943,8 @@ class IssuesProcessor {
891943
this.addedCloseCommentIssues.push(issue);
892944
if (!this.options.debugOnly) {
893945
yield this.client.rest.issues.createComment({
894-
owner: github_1.context.repo.owner,
895-
repo: github_1.context.repo.repo,
946+
owner: issue.owner_repo.owner,
947+
repo: issue.owner_repo.repo,
896948
issue_number: issue.number,
897949
body: closeMessage
898950
});
@@ -908,8 +960,8 @@ class IssuesProcessor {
908960
(_b = this.statistics) === null || _b === void 0 ? void 0 : _b.incrementAddedItemsLabel(issue);
909961
if (!this.options.debugOnly) {
910962
yield this.client.rest.issues.addLabels({
911-
owner: github_1.context.repo.owner,
912-
repo: github_1.context.repo.repo,
963+
owner: issue.owner_repo.owner,
964+
repo: issue.owner_repo.repo,
913965
issue_number: issue.number,
914966
labels: [closeLabel]
915967
});
@@ -924,8 +976,8 @@ class IssuesProcessor {
924976
(_c = this.statistics) === null || _c === void 0 ? void 0 : _c.incrementClosedItemsCount(issue);
925977
if (!this.options.debugOnly) {
926978
yield this.client.rest.issues.update({
927-
owner: github_1.context.repo.owner,
928-
repo: github_1.context.repo.repo,
979+
owner: issue.owner_repo.owner,
980+
repo: issue.owner_repo.repo,
929981
issue_number: issue.number,
930982
state: 'closed',
931983
state_reason: this.options.closeIssueReason || undefined
@@ -955,15 +1007,15 @@ class IssuesProcessor {
9551007
const branch = pullRequest.head.ref;
9561008
if (pullRequest.head.repo === null ||
9571009
pullRequest.head.repo.full_name ===
958-
`${github_1.context.repo.owner}/${github_1.context.repo.repo}`) {
1010+
`${issue.owner_repo.owner}/${issue.owner_repo.repo}`) {
9591011
issueLogger.info(`Deleting the branch "${logger_service_1.LoggerService.cyan(branch)}" from closed $$type`);
9601012
try {
9611013
this._consumeIssueOperation(issue);
9621014
(_a = this.statistics) === null || _a === void 0 ? void 0 : _a.incrementDeletedBranchesCount();
9631015
if (!this.options.debugOnly) {
9641016
yield this.client.rest.git.deleteRef({
965-
owner: github_1.context.repo.owner,
966-
repo: github_1.context.repo.repo,
1017+
owner: issue.owner_repo.owner,
1018+
repo: issue.owner_repo.repo,
9671019
ref: `heads/${branch}`
9681020
});
9691021
}
@@ -989,8 +1041,8 @@ class IssuesProcessor {
9891041
(_a = this.statistics) === null || _a === void 0 ? void 0 : _a.incrementDeletedItemsLabelsCount(issue);
9901042
if (!this.options.debugOnly) {
9911043
yield this.client.rest.issues.removeLabel({
992-
owner: github_1.context.repo.owner,
993-
repo: github_1.context.repo.repo,
1044+
owner: issue.owner_repo.owner,
1045+
repo: issue.owner_repo.repo,
9941046
issue_number: issue.number,
9951047
name: label
9961048
});
@@ -1089,8 +1141,8 @@ class IssuesProcessor {
10891141
(_a = this.statistics) === null || _a === void 0 ? void 0 : _a.incrementAddedItemsLabel(issue);
10901142
if (!this.options.debugOnly) {
10911143
yield this.client.rest.issues.addLabels({
1092-
owner: github_1.context.repo.owner,
1093-
repo: github_1.context.repo.repo,
1144+
owner: issue.owner_repo.owner,
1145+
repo: issue.owner_repo.repo,
10941146
issue_number: issue.number,
10951147
labels: labelsToAdd
10961148
});
@@ -1499,6 +1551,31 @@ class Operations {
14991551
exports.Operations = Operations;
15001552

15011553

1554+
/***/ }),
1555+
1556+
/***/ 6226:
1557+
/***/ ((__unused_webpack_module, exports) => {
1558+
1559+
"use strict";
1560+
1561+
Object.defineProperty(exports, "__esModule", ({ value: true }));
1562+
exports.OwnerRepo = void 0;
1563+
class OwnerRepo {
1564+
constructor(repo_url) {
1565+
const m = repo_url.match(/.*\/([^/]+)\/(.+)$/);
1566+
if (!m) {
1567+
this.owner = '';
1568+
this.repo = '';
1569+
}
1570+
else {
1571+
this.owner = m[1];
1572+
this.repo = m[2];
1573+
}
1574+
}
1575+
}
1576+
exports.OwnerRepo = OwnerRepo;
1577+
1578+
15021579
/***/ }),
15031580

15041581
/***/ 7069:
@@ -2185,6 +2262,7 @@ var Option;
21852262
Option["DaysBeforePrClose"] = "days-before-pr-close";
21862263
Option["StaleIssueLabel"] = "stale-issue-label";
21872264
Option["CloseIssueLabel"] = "close-issue-label";
2265+
Option["OnlyMatchingFilter"] = "only-matching-filter";
21882266
Option["ExemptIssueLabels"] = "exempt-issue-labels";
21892267
Option["StalePrLabel"] = "stale-pr-label";
21902268
Option["ClosePrLabel"] = "close-pr-label";
@@ -2526,6 +2604,7 @@ function _getAndValidateArgs() {
25262604
daysBeforePrClose: parseInt(core.getInput('days-before-pr-close')),
25272605
staleIssueLabel: core.getInput('stale-issue-label', { required: true }),
25282606
closeIssueLabel: core.getInput('close-issue-label'),
2607+
onlyMatchingFilter: core.getInput('only-matching-filter'),
25292608
exemptIssueLabels: core.getInput('exempt-issue-labels'),
25302609
stalePrLabel: core.getInput('stale-pr-label', { required: true }),
25312610
closePrLabel: core.getInput('close-pr-label'),

src/classes/issue.spec.ts

+4-1
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import {IUserAssignee} from '../interfaces/assignee';
22
import {IIssue} from '../interfaces/issue';
33
import {IIssuesProcessorOptions} from '../interfaces/issues-processor-options';
44
import {ILabel} from '../interfaces/label';
5+
import {IOwnerRepo} from '../interfaces/owner-repo';
56
import {IMilestone} from '../interfaces/milestone';
67
import {Issue} from './issue';
78

@@ -29,6 +30,7 @@ describe('Issue', (): void => {
2930
exemptPrLabels: '',
3031
onlyLabels: '',
3132
onlyIssueLabels: '',
33+
onlyMatchingFilter: '',
3234
onlyPrLabels: '',
3335
anyOfLabels: '',
3436
anyOfIssueLabels: '',
@@ -88,7 +90,8 @@ describe('Issue', (): void => {
8890
login: 'dummy-login',
8991
type: 'User'
9092
}
91-
]
93+
],
94+
repository_url: 'https://api.github.com/repos/dummy/dummy'
9295
};
9396
issue = new Issue(optionsInterface, issueInterface);
9497
});

src/classes/issue.ts

+6
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,11 @@ import {Assignee} from '../interfaces/assignee';
44
import {IIssue, OctokitIssue} from '../interfaces/issue';
55
import {IIssuesProcessorOptions} from '../interfaces/issues-processor-options';
66
import {ILabel} from '../interfaces/label';
7+
import {IOwnerRepo} from '../interfaces/owner-repo';
78
import {IMilestone} from '../interfaces/milestone';
89
import {IsoDateString} from '../types/iso-date-string';
910
import {Operations} from './operations';
11+
import {OwnerRepo} from './owner-repo';
1012

1113
export class Issue implements IIssue {
1214
readonly title: string;
@@ -20,8 +22,10 @@ export class Issue implements IIssue {
2022
readonly locked: boolean;
2123
readonly milestone?: IMilestone | null;
2224
readonly assignees: Assignee[];
25+
readonly repository_url?: string;
2326
isStale: boolean;
2427
markedStaleThisRun: boolean;
28+
readonly owner_repo: IOwnerRepo;
2529
operations = new Operations();
2630
private readonly _options: IIssuesProcessorOptions;
2731

@@ -41,8 +45,10 @@ export class Issue implements IIssue {
4145
this.locked = issue.locked;
4246
this.milestone = issue.milestone;
4347
this.assignees = issue.assignees || [];
48+
this.repository_url = issue.repository_url;
4449
this.isStale = isLabeled(this, this.staleLabel);
4550
this.markedStaleThisRun = false;
51+
this.owner_repo = new OwnerRepo(issue.repository_url || '');
4652
}
4753

4854
get isPullRequest(): boolean {

0 commit comments

Comments
 (0)