diff --git a/action.yml b/action.yml index 251c9392..1c80b1e5 100644 --- a/action.yml +++ b/action.yml @@ -164,6 +164,10 @@ inputs: description: 'Exempt all issues with assignees from being marked as stale. Override "exempt-all-assignees" option regarding only the issues.' default: '' required: false + exempt-pinned-issues: + description: 'Exempt pinned issues from being marked as stale. Default to false.' + default: 'false' + required: false exempt-all-pr-assignees: description: 'Exempt all pull requests with assignees from being marked as stale. Override "exempt-all-assignees" option regarding only the pull requests.' default: '' diff --git a/dist/index.js b/dist/index.js index f964b453..094fe354 100644 --- a/dist/index.js +++ b/dist/index.js @@ -408,6 +408,8 @@ class IssuesProcessor { this.addedLabelIssues = []; this.addedCloseCommentIssues = []; this._logger = new logger_1.Logger(); + this._lastIssueEvents = []; + this._lastIssueEventsIssueId = -1; this.options = options; this.state = state; this.client = (0, github_1.getOctokit)(this.options.repoToken, undefined, plugin_retry_1.retry); @@ -464,6 +466,34 @@ class IssuesProcessor { return this.processIssues(page + 1); }); } + getIssueEvents(issue) { + return __awaiter(this, void 0, void 0, function* () { + if (issue.number !== this._lastIssueEventsIssueId) { + const options = this.client.rest.issues.listEvents.endpoint.merge({ + owner: github_1.context.repo.owner, + repo: github_1.context.repo.repo, + per_page: 100, + issue_number: issue.number + }); + const events = yield this.client.paginate(options); + this._lastIssueEvents = events.reverse(); + this._lastIssueEventsIssueId = issue.number; + } + return this._lastIssueEvents; + }); + } + getPinnedStatus(issue) { + return __awaiter(this, void 0, void 0, function* () { + const events = yield this.getIssueEvents(issue); + const pinnedEvent = events.findIndex(event => event.event === 'pinned'); + if (pinnedEvent == -1) + return false; + const unpinnedEvent = events.findIndex(event => event.event === 'unpinned'); + if (unpinnedEvent == -1) + return true; + return pinnedEvent < unpinnedEvent; + }); + } processIssue(issue, labelsToAddWhenUnstale, labelsToRemoveWhenUnstale, labelsToRemoveWhenStale) { var _a; return __awaiter(this, void 0, void 0, function* () { @@ -504,6 +534,11 @@ class IssuesProcessor { IssuesProcessor._endIssueProcessing(issue); return; // If the issue has an 'include-only-assigned' option set, process only issues with nonempty assignees list } + if (yield this._isSkipPinned(issue)) { + issueLogger.info('Skipping this issue because it is pinned'); + IssuesProcessor._endIssueProcessing(issue); + return; // Don't process pinned issues + } const onlyLabels = (0, words_to_list_1.wordsToList)(this._getOnlyLabels(issue)); if (onlyLabels.length > 0) { issueLogger.info(`The option ${issueLogger.createOptionLink(option_1.Option.OnlyLabels)} was specified to only process issues and pull requests with all those labels (${logger_service_1.LoggerService.cyan(onlyLabels.length)})`); @@ -702,15 +737,8 @@ class IssuesProcessor { issueLogger.info(`Checking for label on this $$type`); 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, - per_page: 100, - issue_number: issue.number - }); - const events = yield this.client.paginate(options); - const reversedEvents = events.reverse(); - const staleLabeledEvent = reversedEvents.find(event => event.event === 'labeled' && + const events = yield this.getIssueEvents(issue); + const staleLabeledEvent = events.find(event => event.event === 'labeled' && (0, clean_label_1.cleanLabel)(event.label.name) === (0, clean_label_1.cleanLabel)(label)); if (!staleLabeledEvent) { // Must be old rather than labeled @@ -1025,6 +1053,11 @@ class IssuesProcessor { _isIncludeOnlyAssigned(issue) { return this.options.includeOnlyAssigned && !issue.hasAssignees; } + _isSkipPinned(issue) { + return __awaiter(this, void 0, void 0, function* () { + return (this.options.exemptPinnedIssues && (yield this.getPinnedStatus(issue))); + }); + } _getAnyOfLabels(issue) { if (issue.isPullRequest) { if (this.options.anyOfPrLabels !== '') { @@ -2461,6 +2494,7 @@ function _getAndValidateArgs() { staleIssueLabel: core.getInput('stale-issue-label', { required: true }), closeIssueLabel: core.getInput('close-issue-label'), exemptIssueLabels: core.getInput('exempt-issue-labels'), + exemptPinnedIssues: core.getInput('exempt-pinned-issues') === 'true', stalePrLabel: core.getInput('stale-pr-label', { required: true }), closePrLabel: core.getInput('close-pr-label'), exemptPrLabels: core.getInput('exempt-pr-labels'), diff --git a/src/classes/issues-processor.ts b/src/classes/issues-processor.ts index 821c83e0..5940a5a2 100644 --- a/src/classes/issues-processor.ts +++ b/src/classes/issues-processor.ts @@ -193,6 +193,35 @@ export class IssuesProcessor { return this.processIssues(page + 1); } + private _lastIssueEvents: IIssueEvent[] = []; + private _lastIssueEventsIssueId = -1; + async getIssueEvents(issue: Issue): Promise { + if (issue.number !== this._lastIssueEventsIssueId) { + const options = this.client.rest.issues.listEvents.endpoint.merge({ + owner: context.repo.owner, + repo: context.repo.repo, + per_page: 100, + issue_number: issue.number + }); + const events: IIssueEvent[] = await this.client.paginate(options); + this._lastIssueEvents = events.reverse(); + this._lastIssueEventsIssueId = issue.number; + } + return this._lastIssueEvents; + } + + async getPinnedStatus(issue: Issue): Promise { + const events = await this.getIssueEvents(issue); + const pinnedEvent = events.findIndex(event => event.event === 'pinned'); + + if (pinnedEvent == -1) return false; + + const unpinnedEvent = events.findIndex(event => event.event === 'unpinned'); + if (unpinnedEvent == -1) return true; + + return pinnedEvent < unpinnedEvent; + } + async processIssue( issue: Issue, labelsToAddWhenUnstale: Readonly[], @@ -248,6 +277,12 @@ export class IssuesProcessor { return; // If the issue has an 'include-only-assigned' option set, process only issues with nonempty assignees list } + if (await this._isSkipPinned(issue)) { + issueLogger.info('Skipping this issue because it is pinned'); + IssuesProcessor._endIssueProcessing(issue); + return; // Don't process pinned issues + } + const onlyLabels: string[] = wordsToList(this._getOnlyLabels(issue)); if (onlyLabels.length > 0) { @@ -593,17 +628,9 @@ 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, - per_page: 100, - issue_number: issue.number - }); - const events: IIssueEvent[] = await this.client.paginate(options); - const reversedEvents = events.reverse(); - - const staleLabeledEvent = reversedEvents.find( + const events = await this.getIssueEvents(issue); + const staleLabeledEvent = events.find( event => event.event === 'labeled' && cleanLabel(event.label.name) === cleanLabel(label) @@ -1074,6 +1101,12 @@ export class IssuesProcessor { return this.options.includeOnlyAssigned && !issue.hasAssignees; } + private async _isSkipPinned(issue: Issue): Promise { + return ( + this.options.exemptPinnedIssues && (await this.getPinnedStatus(issue)) + ); + } + private _getAnyOfLabels(issue: Issue): string { if (issue.isPullRequest) { if (this.options.anyOfPrLabels !== '') { diff --git a/src/interfaces/issues-processor-options.ts b/src/interfaces/issues-processor-options.ts index 93099228..1f33735d 100644 --- a/src/interfaces/issues-processor-options.ts +++ b/src/interfaces/issues-processor-options.ts @@ -15,6 +15,7 @@ export interface IIssuesProcessorOptions { staleIssueLabel: string; closeIssueLabel: string; exemptIssueLabels: string; + exemptPinnedIssues: boolean; stalePrLabel: string; closePrLabel: string; exemptPrLabels: string; diff --git a/src/main.ts b/src/main.ts index 76870041..f3f8b446 100644 --- a/src/main.ts +++ b/src/main.ts @@ -47,6 +47,7 @@ function _getAndValidateArgs(): IIssuesProcessorOptions { staleIssueLabel: core.getInput('stale-issue-label', {required: true}), closeIssueLabel: core.getInput('close-issue-label'), exemptIssueLabels: core.getInput('exempt-issue-labels'), + exemptPinnedIssues: core.getInput('exempt-pinned-issues') === 'true', stalePrLabel: core.getInput('stale-pr-label', {required: true}), closePrLabel: core.getInput('close-pr-label'), exemptPrLabels: core.getInput('exempt-pr-labels'),