From 4f61ec9d543243e3ce440fd41cc6dabc6d99ec9e Mon Sep 17 00:00:00 2001 From: George Joseph Date: Thu, 14 Mar 2024 13:16:24 -0600 Subject: [PATCH] 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 --- README.md | 19 +++ .../constants/default-processor-options.ts | 1 + __tests__/functions/generate-issue.ts | 3 +- action.yml | 4 + dist/index.js | 127 ++++++++++++++---- src/classes/issue.spec.ts | 5 +- src/classes/issue.ts | 6 + src/classes/issues-processor.ts | 105 +++++++++++---- src/classes/owner-repo.ts | 17 +++ src/enums/option.ts | 1 + src/interfaces/issue.ts | 2 + src/interfaces/issues-processor-options.ts | 1 + src/interfaces/owner-repo.ts | 4 + src/main.ts | 1 + 14 files changed, 246 insertions(+), 50 deletions(-) create mode 100644 src/classes/owner-repo.ts create mode 100644 src/interfaces/owner-repo.ts diff --git a/README.md b/README.md index eb65b46b9..f82dac969 100644 --- a/README.md +++ b/README.md @@ -60,6 +60,7 @@ Every argument is optional. | [close-issue-reason](#close-issue-reason) | Reason to use when closing issues | `not_planned` | | [stale-pr-label](#stale-pr-label) | Label to apply on staled PRs | `Stale` | | [close-pr-label](#close-pr-label) | Label to apply on closed PRs | | +| [only-matching-filter](#only-matching-filter) | Only issues/PRs matching the search filter(s) will be retrieved and tested | | | [exempt-issue-labels](#exempt-issue-labels) | Labels on issues exempted from stale | | | [exempt-pr-labels](#exempt-pr-labels) | Labels on PRs exempted from stale | | | [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 Default value: unset Required Permission: `pull-requests: write` +#### only-matching-filter + +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) +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. + +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 +a limited degree by specifying multiple search requests separated by ` || `. Each request is run separately and the results are accumulated and duplicates +removed before any further processing is done. + +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 +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. + +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: +`only-matching-filter: 'org:myorg is:pr is:open review:changes_requested || org:myorg is:pr is:open label:submitter-action-required'`. +From this set, all of the other label, milestone, date, assignee, etc. filters will be applied before taking any action. + +Default value: unset + #### exempt-issue-labels Comma separated list of labels that can be assigned to issues to exclude them from being marked as stale diff --git a/__tests__/constants/default-processor-options.ts b/__tests__/constants/default-processor-options.ts index 0265b6446..72056694f 100644 --- a/__tests__/constants/default-processor-options.ts +++ b/__tests__/constants/default-processor-options.ts @@ -19,6 +19,7 @@ export const DefaultProcessorOptions: IIssuesProcessorOptions = Object.freeze({ exemptIssueLabels: '', stalePrLabel: 'Stale', closePrLabel: '', + onlyMatchingFilter: '', exemptPrLabels: '', onlyLabels: '', onlyIssueLabels: '', diff --git a/__tests__/functions/generate-issue.ts b/__tests__/functions/generate-issue.ts index 0198c42cb..9fe2f72de 100644 --- a/__tests__/functions/generate-issue.ts +++ b/__tests__/functions/generate-issue.ts @@ -39,6 +39,7 @@ export function generateIssue( login: assignee, type: 'User' }; - }) + }), + repository_url: 'https://api.github.com/repos/dummy/dummy' }); } diff --git a/action.yml b/action.yml index d55f8547c..f943c41e9 100644 --- a/action.yml +++ b/action.yml @@ -45,6 +45,10 @@ inputs: close-issue-label: description: 'The label to apply when an issue is closed.' required: false + only-matching-filter: + description: 'Only issues/PRs matching the search filter(s) will be retrieved and tested' + default: '' + required: false exempt-issue-labels: description: 'The labels that mean an issue is exempt from being marked stale. Separate multiple labels with commas (eg. "label1,label2").' default: '' diff --git a/dist/index.js b/dist/index.js index f2786a0f6..144d379c8 100644 --- a/dist/index.js +++ b/dist/index.js @@ -272,6 +272,7 @@ exports.Issue = void 0; const is_labeled_1 = __nccwpck_require__(6792); const is_pull_request_1 = __nccwpck_require__(5400); const operations_1 = __nccwpck_require__(7957); +const owner_repo_1 = __nccwpck_require__(6226); class Issue { constructor(options, issue) { this.operations = new operations_1.Operations(); @@ -287,8 +288,10 @@ class Issue { this.locked = issue.locked; this.milestone = issue.milestone; this.assignees = issue.assignees || []; + this.repository_url = issue.repository_url; this.isStale = (0, is_labeled_1.isLabeled)(this, this.staleLabel); this.markedStaleThisRun = false; + this.owner_repo = new owner_repo_1.OwnerRepo(issue.repository_url || ''); } get isPullRequest() { return (0, is_pull_request_1.isPullRequest)(this); @@ -426,7 +429,7 @@ class IssuesProcessor { var _a, _b; return __awaiter(this, void 0, void 0, function* () { // get the next batch of issues - const issues = yield this.getIssues(page); + const issues = yield this.getIssuesWrapper(page); if (issues.length <= 0) { this._logger.info(logger_service_1.LoggerService.green(`No more issues found to process. Exiting...`)); (_a = this.statistics) === null || _a === void 0 ? void 0 : _a.setOperationsCount(this.operations.getConsumedOperationsCount()).logStats(); @@ -659,8 +662,8 @@ class IssuesProcessor { this._consumeIssueOperation(issue); (_a = this.statistics) === null || _a === void 0 ? void 0 : _a.incrementFetchedItemsCommentsCount(); const comments = yield this.client.rest.issues.listComments({ - owner: github_1.context.repo.owner, - repo: github_1.context.repo.repo, + owner: issue.owner_repo.owner, + repo: issue.owner_repo.repo, issue_number: issue.number, since: sinceDate }); @@ -687,6 +690,7 @@ class IssuesProcessor { page }); (_a = this.statistics) === null || _a === void 0 ? void 0 : _a.incrementFetchedItemsCount(issueResult.data.length); + 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}`)); return issueResult.data.map((issue) => new issue_1.Issue(this.options, issue)); } catch (error) { @@ -694,6 +698,54 @@ class IssuesProcessor { } }); } + // grab issues and/or prs from github in batches of 100 using search filter + getIssuesByFilter(page, search) { + var _a; + return __awaiter(this, void 0, void 0, function* () { + try { + this.operations.consumeOperation(); + const issueResult = yield this.client.rest.search.issuesAndPullRequests({ + q: search, + per_page: 100, + direction: this.options.ascending ? 'asc' : 'desc', + page + }); + (_a = this.statistics) === null || _a === void 0 ? void 0 : _a.incrementFetchedItemsCount(issueResult.data.total_count); + this._logger.info(logger_service_1.LoggerService.green(`Retrieved ${issueResult.data.total_count} issues/PRs for search '${search}'`)); + return issueResult.data.items.map((issue) => new issue_1.Issue(this.options, issue)); + } + catch (error) { + throw Error(`Getting issues was blocked by the error: ${error.message}`); + } + }); + } + _removeDupIssues(issues) { + return issues.reduce(function (a, b) { + if (!a.find(o => o.number == b.number)) + a.push(b); + return a; + }, []); + } + getIssuesWrapper(page) { + return __awaiter(this, void 0, void 0, function* () { + if (!this.options.onlyMatchingFilter) { + return this.getIssues(page); + } + const filter = this.options.onlyMatchingFilter; + const results = []; + for (let term of filter.split('||')) { + if (term.search(/repo:|owner:|org:|user:/) < 0) { + term = `repo:${github_1.context.repo.owner}/${github_1.context.repo.repo} ${this.options.onlyMatchingFilter}`; + } + if (term.search(/is:open/) < 0) { + term += ' is:open'; + } + const r = yield this.getIssuesByFilter(page, term); + results.push(...r); + } + return this._removeDupIssues(results); + }); + } // 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/ getLabelCreationDate(issue, label) { @@ -704,8 +756,8 @@ class IssuesProcessor { this._consumeIssueOperation(issue); (_a = this.statistics) === null || _a === void 0 ? void 0 : _a.incrementFetchedItemsEventsCount(); const options = this.client.rest.issues.listEvents.endpoint.merge({ - owner: github_1.context.repo.owner, - repo: github_1.context.repo.repo, + owner: issue.owner_repo.owner, + repo: issue.owner_repo.repo, per_page: 100, issue_number: issue.number }); @@ -728,8 +780,8 @@ class IssuesProcessor { this._consumeIssueOperation(issue); (_a = this.statistics) === null || _a === void 0 ? void 0 : _a.incrementFetchedPullRequestsCount(); const pullRequest = yield this.client.rest.pulls.get({ - owner: github_1.context.repo.owner, - repo: github_1.context.repo.repo, + owner: issue.owner_repo.owner, + repo: issue.owner_repo.repo, pull_number: issue.number }); return pullRequest.data; @@ -848,8 +900,8 @@ class IssuesProcessor { (_a = this.statistics) === null || _a === void 0 ? void 0 : _a.incrementAddedItemsComment(issue); if (!this.options.debugOnly) { yield this.client.rest.issues.createComment({ - owner: github_1.context.repo.owner, - repo: github_1.context.repo.repo, + owner: issue.owner_repo.owner, + repo: issue.owner_repo.repo, issue_number: issue.number, body: staleMessage }); @@ -865,8 +917,8 @@ class IssuesProcessor { (_c = this.statistics) === null || _c === void 0 ? void 0 : _c.incrementStaleItemsCount(issue); if (!this.options.debugOnly) { yield this.client.rest.issues.addLabels({ - owner: github_1.context.repo.owner, - repo: github_1.context.repo.repo, + owner: issue.owner_repo.owner, + repo: issue.owner_repo.repo, issue_number: issue.number, labels: [staleLabel] }); @@ -891,8 +943,8 @@ class IssuesProcessor { this.addedCloseCommentIssues.push(issue); if (!this.options.debugOnly) { yield this.client.rest.issues.createComment({ - owner: github_1.context.repo.owner, - repo: github_1.context.repo.repo, + owner: issue.owner_repo.owner, + repo: issue.owner_repo.repo, issue_number: issue.number, body: closeMessage }); @@ -908,8 +960,8 @@ class IssuesProcessor { (_b = this.statistics) === null || _b === void 0 ? void 0 : _b.incrementAddedItemsLabel(issue); if (!this.options.debugOnly) { yield this.client.rest.issues.addLabels({ - owner: github_1.context.repo.owner, - repo: github_1.context.repo.repo, + owner: issue.owner_repo.owner, + repo: issue.owner_repo.repo, issue_number: issue.number, labels: [closeLabel] }); @@ -924,8 +976,8 @@ class IssuesProcessor { (_c = this.statistics) === null || _c === void 0 ? void 0 : _c.incrementClosedItemsCount(issue); if (!this.options.debugOnly) { yield this.client.rest.issues.update({ - owner: github_1.context.repo.owner, - repo: github_1.context.repo.repo, + owner: issue.owner_repo.owner, + repo: issue.owner_repo.repo, issue_number: issue.number, state: 'closed', state_reason: this.options.closeIssueReason || undefined @@ -955,15 +1007,15 @@ class IssuesProcessor { const branch = pullRequest.head.ref; if (pullRequest.head.repo === null || pullRequest.head.repo.full_name === - `${github_1.context.repo.owner}/${github_1.context.repo.repo}`) { + `${issue.owner_repo.owner}/${issue.owner_repo.repo}`) { issueLogger.info(`Deleting the branch "${logger_service_1.LoggerService.cyan(branch)}" from closed $$type`); try { this._consumeIssueOperation(issue); (_a = this.statistics) === null || _a === void 0 ? void 0 : _a.incrementDeletedBranchesCount(); if (!this.options.debugOnly) { yield this.client.rest.git.deleteRef({ - owner: github_1.context.repo.owner, - repo: github_1.context.repo.repo, + owner: issue.owner_repo.owner, + repo: issue.owner_repo.repo, ref: `heads/${branch}` }); } @@ -989,8 +1041,8 @@ class IssuesProcessor { (_a = this.statistics) === null || _a === void 0 ? void 0 : _a.incrementDeletedItemsLabelsCount(issue); if (!this.options.debugOnly) { yield this.client.rest.issues.removeLabel({ - owner: github_1.context.repo.owner, - repo: github_1.context.repo.repo, + owner: issue.owner_repo.owner, + repo: issue.owner_repo.repo, issue_number: issue.number, name: label }); @@ -1089,8 +1141,8 @@ class IssuesProcessor { (_a = this.statistics) === null || _a === void 0 ? void 0 : _a.incrementAddedItemsLabel(issue); if (!this.options.debugOnly) { yield this.client.rest.issues.addLabels({ - owner: github_1.context.repo.owner, - repo: github_1.context.repo.repo, + owner: issue.owner_repo.owner, + repo: issue.owner_repo.repo, issue_number: issue.number, labels: labelsToAdd }); @@ -1499,6 +1551,31 @@ class Operations { exports.Operations = Operations; +/***/ }), + +/***/ 6226: +/***/ ((__unused_webpack_module, exports) => { + +"use strict"; + +Object.defineProperty(exports, "__esModule", ({ value: true })); +exports.OwnerRepo = void 0; +class OwnerRepo { + constructor(repo_url) { + const m = repo_url.match(/.*\/([^/]+)\/(.+)$/); + if (!m) { + this.owner = ''; + this.repo = ''; + } + else { + this.owner = m[1]; + this.repo = m[2]; + } + } +} +exports.OwnerRepo = OwnerRepo; + + /***/ }), /***/ 7069: @@ -2185,6 +2262,7 @@ var Option; Option["DaysBeforePrClose"] = "days-before-pr-close"; Option["StaleIssueLabel"] = "stale-issue-label"; Option["CloseIssueLabel"] = "close-issue-label"; + Option["OnlyMatchingFilter"] = "only-matching-filter"; Option["ExemptIssueLabels"] = "exempt-issue-labels"; Option["StalePrLabel"] = "stale-pr-label"; Option["ClosePrLabel"] = "close-pr-label"; @@ -2526,6 +2604,7 @@ function _getAndValidateArgs() { daysBeforePrClose: parseInt(core.getInput('days-before-pr-close')), staleIssueLabel: core.getInput('stale-issue-label', { required: true }), closeIssueLabel: core.getInput('close-issue-label'), + onlyMatchingFilter: core.getInput('only-matching-filter'), exemptIssueLabels: core.getInput('exempt-issue-labels'), stalePrLabel: core.getInput('stale-pr-label', { required: true }), closePrLabel: core.getInput('close-pr-label'), diff --git a/src/classes/issue.spec.ts b/src/classes/issue.spec.ts index a2c82e268..2c8ea5b3d 100644 --- a/src/classes/issue.spec.ts +++ b/src/classes/issue.spec.ts @@ -2,6 +2,7 @@ import {IUserAssignee} from '../interfaces/assignee'; import {IIssue} from '../interfaces/issue'; import {IIssuesProcessorOptions} from '../interfaces/issues-processor-options'; import {ILabel} from '../interfaces/label'; +import {IOwnerRepo} from '../interfaces/owner-repo'; import {IMilestone} from '../interfaces/milestone'; import {Issue} from './issue'; @@ -29,6 +30,7 @@ describe('Issue', (): void => { exemptPrLabels: '', onlyLabels: '', onlyIssueLabels: '', + onlyMatchingFilter: '', onlyPrLabels: '', anyOfLabels: '', anyOfIssueLabels: '', @@ -88,7 +90,8 @@ describe('Issue', (): void => { login: 'dummy-login', type: 'User' } - ] + ], + repository_url: 'https://api.github.com/repos/dummy/dummy' }; issue = new Issue(optionsInterface, issueInterface); }); diff --git a/src/classes/issue.ts b/src/classes/issue.ts index b90631835..c53cb9ae4 100644 --- a/src/classes/issue.ts +++ b/src/classes/issue.ts @@ -4,9 +4,11 @@ import {Assignee} from '../interfaces/assignee'; import {IIssue, OctokitIssue} from '../interfaces/issue'; import {IIssuesProcessorOptions} from '../interfaces/issues-processor-options'; import {ILabel} from '../interfaces/label'; +import {IOwnerRepo} from '../interfaces/owner-repo'; import {IMilestone} from '../interfaces/milestone'; import {IsoDateString} from '../types/iso-date-string'; import {Operations} from './operations'; +import {OwnerRepo} from './owner-repo'; export class Issue implements IIssue { readonly title: string; @@ -20,8 +22,10 @@ export class Issue implements IIssue { readonly locked: boolean; readonly milestone?: IMilestone | null; readonly assignees: Assignee[]; + readonly repository_url?: string; isStale: boolean; markedStaleThisRun: boolean; + readonly owner_repo: IOwnerRepo; operations = new Operations(); private readonly _options: IIssuesProcessorOptions; @@ -41,8 +45,10 @@ export class Issue implements IIssue { this.locked = issue.locked; this.milestone = issue.milestone; this.assignees = issue.assignees || []; + this.repository_url = issue.repository_url; this.isStale = isLabeled(this, this.staleLabel); this.markedStaleThisRun = false; + this.owner_repo = new OwnerRepo(issue.repository_url || ''); } get isPullRequest(): boolean { diff --git a/src/classes/issues-processor.ts b/src/classes/issues-processor.ts index 486c6a78a..952035e99 100644 --- a/src/classes/issues-processor.ts +++ b/src/classes/issues-processor.ts @@ -106,7 +106,7 @@ export class IssuesProcessor { async processIssues(page: Readonly = 1): Promise { // get the next batch of issues - const issues: Issue[] = await this.getIssues(page); + const issues: Issue[] = await this.getIssuesWrapper(page); if (issues.length <= 0) { this._logger.info( @@ -549,8 +549,8 @@ export class IssuesProcessor { this._consumeIssueOperation(issue); this.statistics?.incrementFetchedItemsCommentsCount(); const comments = await this.client.rest.issues.listComments({ - owner: context.repo.owner, - repo: context.repo.repo, + owner: issue.owner_repo.owner, + repo: issue.owner_repo.repo, issue_number: issue.number, since: sinceDate }); @@ -574,6 +574,11 @@ export class IssuesProcessor { page }); this.statistics?.incrementFetchedItemsCount(issueResult.data.length); + this._logger.info( + LoggerService.green( + `Retrieved ${issueResult.data.length} issues/PRs for repo ${context.repo.owner}/${context.repo.repo}` + ) + ); return issueResult.data.map( (issue): Issue => @@ -584,6 +589,58 @@ export class IssuesProcessor { } } + // grab issues and/or prs from github in batches of 100 using search filter + async getIssuesByFilter(page: number, search: string): Promise { + try { + this.operations.consumeOperation(); + const issueResult = await this.client.rest.search.issuesAndPullRequests({ + q: search, + per_page: 100, + direction: this.options.ascending ? 'asc' : 'desc', + page + }); + this.statistics?.incrementFetchedItemsCount(issueResult.data.total_count); + this._logger.info( + LoggerService.green( + `Retrieved ${issueResult.data.total_count} issues/PRs for search '${search}'` + ) + ); + + return issueResult.data.items.map( + (issue): Issue => + new Issue(this.options, issue as Readonly) + ); + } catch (error) { + throw Error(`Getting issues was blocked by the error: ${error.message}`); + } + } + + private _removeDupIssues(issues: Issue[]): Issue[] { + return issues.reduce(function (a: Issue[], b: Issue) { + if (!a.find(o => o.number == b.number)) a.push(b); + return a; + }, []); + } + + async getIssuesWrapper(page: number): Promise { + if (!this.options.onlyMatchingFilter) { + return this.getIssues(page); + } + const filter = this.options.onlyMatchingFilter; + const results: Issue[] = []; + for (let term of filter.split('||')) { + if (term.search(/repo:|owner:|org:|user:/) < 0) { + term = `repo:${context.repo.owner}/${context.repo.repo} ${this.options.onlyMatchingFilter}`; + } + if (term.search(/is:open/) < 0) { + term += ' is:open'; + } + const r: Issue[] = await this.getIssuesByFilter(page, term); + results.push(...r); + } + return this._removeDupIssues(results); + } + // 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/ async getLabelCreationDate( @@ -597,8 +654,8 @@ export class IssuesProcessor { this._consumeIssueOperation(issue); this.statistics?.incrementFetchedItemsEventsCount(); const options = this.client.rest.issues.listEvents.endpoint.merge({ - owner: context.repo.owner, - repo: context.repo.repo, + owner: issue.owner_repo.owner, + repo: issue.owner_repo.repo, per_page: 100, issue_number: issue.number }); @@ -628,8 +685,8 @@ export class IssuesProcessor { this.statistics?.incrementFetchedPullRequestsCount(); const pullRequest = await this.client.rest.pulls.get({ - owner: context.repo.owner, - repo: context.repo.repo, + owner: issue.owner_repo.owner, + repo: issue.owner_repo.repo, pull_number: issue.number }); @@ -848,8 +905,8 @@ export class IssuesProcessor { if (!this.options.debugOnly) { await this.client.rest.issues.createComment({ - owner: context.repo.owner, - repo: context.repo.repo, + owner: issue.owner_repo.owner, + repo: issue.owner_repo.repo, issue_number: issue.number, body: staleMessage }); @@ -866,8 +923,8 @@ export class IssuesProcessor { if (!this.options.debugOnly) { await this.client.rest.issues.addLabels({ - owner: context.repo.owner, - repo: context.repo.repo, + owner: issue.owner_repo.owner, + repo: issue.owner_repo.repo, issue_number: issue.number, labels: [staleLabel] }); @@ -896,8 +953,8 @@ export class IssuesProcessor { if (!this.options.debugOnly) { await this.client.rest.issues.createComment({ - owner: context.repo.owner, - repo: context.repo.repo, + owner: issue.owner_repo.owner, + repo: issue.owner_repo.repo, issue_number: issue.number, body: closeMessage }); @@ -914,8 +971,8 @@ export class IssuesProcessor { if (!this.options.debugOnly) { await this.client.rest.issues.addLabels({ - owner: context.repo.owner, - repo: context.repo.repo, + owner: issue.owner_repo.owner, + repo: issue.owner_repo.repo, issue_number: issue.number, labels: [closeLabel] }); @@ -931,8 +988,8 @@ export class IssuesProcessor { if (!this.options.debugOnly) { await this.client.rest.issues.update({ - owner: context.repo.owner, - repo: context.repo.repo, + owner: issue.owner_repo.owner, + repo: issue.owner_repo.repo, issue_number: issue.number, state: 'closed', state_reason: this.options.closeIssueReason || undefined @@ -968,7 +1025,7 @@ export class IssuesProcessor { if ( pullRequest.head.repo === null || pullRequest.head.repo.full_name === - `${context.repo.owner}/${context.repo.repo}` + `${issue.owner_repo.owner}/${issue.owner_repo.repo}` ) { issueLogger.info( `Deleting the branch "${LoggerService.cyan(branch)}" from closed $$type` @@ -980,8 +1037,8 @@ export class IssuesProcessor { if (!this.options.debugOnly) { await this.client.rest.git.deleteRef({ - owner: context.repo.owner, - repo: context.repo.repo, + owner: issue.owner_repo.owner, + repo: issue.owner_repo.repo, ref: `heads/${branch}` }); } @@ -1024,8 +1081,8 @@ export class IssuesProcessor { if (!this.options.debugOnly) { await this.client.rest.issues.removeLabel({ - owner: context.repo.owner, - repo: context.repo.repo, + owner: issue.owner_repo.owner, + repo: issue.owner_repo.repo, issue_number: issue.number, name: label }); @@ -1162,8 +1219,8 @@ export class IssuesProcessor { this.statistics?.incrementAddedItemsLabel(issue); if (!this.options.debugOnly) { await this.client.rest.issues.addLabels({ - owner: context.repo.owner, - repo: context.repo.repo, + owner: issue.owner_repo.owner, + repo: issue.owner_repo.repo, issue_number: issue.number, labels: labelsToAdd }); diff --git a/src/classes/owner-repo.ts b/src/classes/owner-repo.ts new file mode 100644 index 000000000..a9479fa19 --- /dev/null +++ b/src/classes/owner-repo.ts @@ -0,0 +1,17 @@ +import {IOwnerRepo} from '../interfaces/owner-repo'; + +export class OwnerRepo implements IOwnerRepo { + readonly owner: string; + readonly repo: string; + + constructor(repo_url: string) { + const m = repo_url.match(/.*\/([^/]+)\/(.+)$/); + if (!m) { + this.owner = ''; + this.repo = ''; + } else { + this.owner = m[1]; + this.repo = m[2]; + } + } +} diff --git a/src/enums/option.ts b/src/enums/option.ts index 7a9bff026..b77fd5fdb 100644 --- a/src/enums/option.ts +++ b/src/enums/option.ts @@ -12,6 +12,7 @@ export enum Option { DaysBeforePrClose = 'days-before-pr-close', StaleIssueLabel = 'stale-issue-label', CloseIssueLabel = 'close-issue-label', + OnlyMatchingFilter = 'only-matching-filter', ExemptIssueLabels = 'exempt-issue-labels', StalePrLabel = 'stale-pr-label', ClosePrLabel = 'close-pr-label', diff --git a/src/interfaces/issue.ts b/src/interfaces/issue.ts index defdb75d6..af77e5657 100644 --- a/src/interfaces/issue.ts +++ b/src/interfaces/issue.ts @@ -3,6 +3,7 @@ import {Assignee} from './assignee'; import {ILabel} from './label'; import {IMilestone} from './milestone'; import {components} from '@octokit/openapi-types'; + export interface IIssue { title: string; number: number; @@ -15,6 +16,7 @@ export interface IIssue { locked: boolean; milestone?: IMilestone | null; assignees?: Assignee[] | null; + repository_url?: string; } export type OctokitIssue = components['schemas']['issue']; diff --git a/src/interfaces/issues-processor-options.ts b/src/interfaces/issues-processor-options.ts index 930992284..a11771681 100644 --- a/src/interfaces/issues-processor-options.ts +++ b/src/interfaces/issues-processor-options.ts @@ -14,6 +14,7 @@ export interface IIssuesProcessorOptions { daysBeforePrClose: number; // Could be NaN staleIssueLabel: string; closeIssueLabel: string; + onlyMatchingFilter: string; exemptIssueLabels: string; stalePrLabel: string; closePrLabel: string; diff --git a/src/interfaces/owner-repo.ts b/src/interfaces/owner-repo.ts new file mode 100644 index 000000000..1d2821ad4 --- /dev/null +++ b/src/interfaces/owner-repo.ts @@ -0,0 +1,4 @@ +export interface IOwnerRepo { + owner: string; + repo: string; +} diff --git a/src/main.ts b/src/main.ts index a7836c160..85435732d 100644 --- a/src/main.ts +++ b/src/main.ts @@ -73,6 +73,7 @@ function _getAndValidateArgs(): IIssuesProcessorOptions { daysBeforePrClose: parseInt(core.getInput('days-before-pr-close')), staleIssueLabel: core.getInput('stale-issue-label', {required: true}), closeIssueLabel: core.getInput('close-issue-label'), + onlyMatchingFilter: core.getInput('only-matching-filter'), exemptIssueLabels: core.getInput('exempt-issue-labels'), stalePrLabel: core.getInput('stale-pr-label', {required: true}), closePrLabel: core.getInput('close-pr-label'),