diff --git a/README.md b/README.md index d2c2acc36..0bd0f2bcf 100644 --- a/README.md +++ b/README.md @@ -42,61 +42,62 @@ configured for the repo. Every argument is optional. -| Input | Description | Default | -| ------------------------------------------------------------------- | --------------------------------------------------------------------------- | --------------------- | -| [repo-token](#repo-token) | PAT for GitHub API authentication | `${{ github.token }}` | -| [days-before-stale](#days-before-stale) | Idle number of days before marking issues/PRs stale | `60` | -| [days-before-issue-stale](#days-before-issue-stale) | Override [days-before-stale](#days-before-stale) for issues only | | -| [days-before-pr-stale](#days-before-pr-stale) | Override [days-before-stale](#days-before-stale) for PRs only | | -| [days-before-close](#days-before-close) | Idle number of days before closing stale issues/PRs | `7` | -| [days-before-issue-close](#days-before-issue-close) | Override [days-before-close](#days-before-close) for issues only | | -| [days-before-pr-close](#days-before-pr-close) | Override [days-before-close](#days-before-close) for PRs only | | -| [stale-issue-message](#stale-issue-message) | Comment on the staled issues | | -| [stale-pr-message](#stale-pr-message) | Comment on the staled PRs | | -| [close-issue-message](#close-issue-message) | Comment on the staled issues while closed | | -| [close-pr-message](#close-pr-message) | Comment on the staled PRs while closed | | -| [stale-issue-label](#stale-issue-label) | Label to apply on staled issues | `Stale` | -| [close-issue-label](#close-issue-label) | Label to apply on closed issues | | -| [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 | | -| [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 | | -| [only-issue-labels](#only-issue-labels) | Override [only-labels](#only-labels) for issues only | | -| [only-pr-labels](#only-pr-labels) | Override [only-labels](#only-labels) for PRs only | | -| [any-of-labels](#any-of-labels) | Only issues/PRs with ANY of these labels are checked | | -| [any-of-issue-labels](#any-of-issue-labels) | Override [any-of-labels](#any-of-labels) for issues only | | -| [any-of-pr-labels](#any-of-pr-labels) | Override [any-of-labels](#any-of-labels) for PRs only | | -| [operations-per-run](#operations-per-run) | Max number of operations per run | `30` | -| [remove-stale-when-updated](#remove-stale-when-updated) | Remove stale label from issues/PRs on updates | `true` | -| [remove-issue-stale-when-updated](#remove-issue-stale-when-updated) | Remove stale label from issues on updates/comments | | -| [remove-pr-stale-when-updated](#remove-pr-stale-when-updated) | Remove stale label from PRs on updates/comments | | -| [labels-to-add-when-unstale](#labels-to-add-when-unstale) | Add specified labels from issues/PRs when they become unstale | | -| [labels-to-remove-when-stale](#labels-to-remove-when-stale) | Remove specified labels from issues/PRs when they become stale | | -| [labels-to-remove-when-unstale](#labels-to-remove-when-unstale) | Remove specified labels from issues/PRs when they become unstale | | -| [debug-only](#debug-only) | Dry-run | `false` | -| [ascending](#ascending) | Order to get issues/PRs | `false` | -| [start-date](#start-date) | Skip stale action for issues/PRs created before it | | -| [delete-branch](#delete-branch) | Delete branch after closing a stale PR | `false` | -| [exempt-milestones](#exempt-milestones) | Milestones on issues/PRs exempted from stale | | -| [exempt-issue-milestones](#exempt-issue-milestones) | Override [exempt-milestones](#exempt-milestones) for issues only | | -| [exempt-pr-milestones](#exempt-pr-milestones) | Override [exempt-milestones](#exempt-milestones) for PRs only | | -| [exempt-all-milestones](#exempt-all-milestones) | Exempt all issues/PRs with milestones from stale | `false` | -| [exempt-all-issue-milestones](#exempt-all-issue-milestones) | Override [exempt-all-milestones](#exempt-all-milestones) for issues only | | -| [exempt-all-pr-milestones](#exempt-all-pr-milestones) | Override [exempt-all-milestones](#exempt-all-milestones) for PRs only | | -| [exempt-assignees](#exempt-assignees) | Assignees on issues/PRs exempted from stale | | -| [exempt-issue-assignees](#exempt-issue-assignees) | Override [exempt-assignees](#exempt-assignees) for issues only | | -| [exempt-pr-assignees](#exempt-pr-assignees) | Override [exempt-assignees](#exempt-assignees) for PRs only | | -| [exempt-all-assignees](#exempt-all-assignees) | Exempt all issues/PRs with assignees from stale | `false` | -| [exempt-all-issue-assignees](#exempt-all-issue-assignees) | Override [exempt-all-assignees](#exempt-all-assignees) for issues only | | -| [exempt-all-pr-assignees](#exempt-all-pr-assignees) | Override [exempt-all-assignees](#exempt-all-assignees) for PRs only | | -| [exempt-draft-pr](#exempt-draft-pr) | Skip the stale action for draft PRs | `false` | -| [enable-statistics](#enable-statistics) | Display statistics in the logs | `true` | +| Input | Description | Default | +|---------------------------------------------------------------------|----------------------------------------------------------------------------|-----------------------| +| [repo-token](#repo-token) | PAT for GitHub API authentication | `${{ github.token }}` | +| [days-before-stale](#days-before-stale) | Idle number of days before marking issues/PRs stale | `60` | +| [days-before-issue-stale](#days-before-issue-stale) | Override [days-before-stale](#days-before-stale) for issues only | | +| [days-before-pr-stale](#days-before-pr-stale) | Override [days-before-stale](#days-before-stale) for PRs only | | +| [days-before-close](#days-before-close) | Idle number of days before closing stale issues/PRs | `7` | +| [days-before-issue-close](#days-before-issue-close) | Override [days-before-close](#days-before-close) for issues only | | +| [days-before-pr-close](#days-before-pr-close) | Override [days-before-close](#days-before-close) for PRs only | | +| [stale-issue-message](#stale-issue-message) | Comment on the staled issues | | +| [stale-pr-message](#stale-pr-message) | Comment on the staled PRs | | +| [close-issue-message](#close-issue-message) | Comment on the staled issues while closed | | +| [close-pr-message](#close-pr-message) | Comment on the staled PRs while closed | | +| [stale-issue-label](#stale-issue-label) | Label to apply on staled issues | `Stale` | +| [close-issue-label](#close-issue-label) | Label to apply on closed issues | | +| [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 | | +| [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 | | +| [only-issue-labels](#only-issue-labels) | Override [only-labels](#only-labels) for issues only | | +| [only-pr-labels](#only-pr-labels) | Override [only-labels](#only-labels) for PRs only | | +| [any-of-labels](#any-of-labels) | Only issues/PRs with ANY of these labels are checked | | +| [any-of-issue-labels](#any-of-issue-labels) | Override [any-of-labels](#any-of-labels) for issues only | | +| [any-of-pr-labels](#any-of-pr-labels) | Override [any-of-labels](#any-of-labels) for PRs only | | +| [operations-per-run](#operations-per-run) | Max number of operations per run | `30` | +| [remove-stale-when-updated](#remove-stale-when-updated) | Remove stale label from issues/PRs on updates | `true` | +| [remove-issue-stale-when-updated](#remove-issue-stale-when-updated) | Remove stale label from issues on updates/comments | | +| [remove-pr-stale-when-updated](#remove-pr-stale-when-updated) | Remove stale label from PRs on updates/comments | | +| [labels-to-add-when-unstale](#labels-to-add-when-unstale) | Add specified labels from issues/PRs when they become unstale | | +| [labels-to-remove-when-stale](#labels-to-remove-when-stale) | Remove specified labels from issues/PRs when they become stale | | +| [labels-to-remove-when-unstale](#labels-to-remove-when-unstale) | Remove specified labels from issues/PRs when they become unstale | | +| [debug-only](#debug-only) | Dry-run | `false` | +| [ascending](#ascending) | Order to get issues/PRs | `false` | +| [start-date](#start-date) | Skip stale action for issues/PRs created before it | | +| [delete-branch](#delete-branch) | Delete branch after closing a stale PR | `false` | +| [exempt-milestones](#exempt-milestones) | Milestones on issues/PRs exempted from stale | | +| [exempt-issue-milestones](#exempt-issue-milestones) | Override [exempt-milestones](#exempt-milestones) for issues only | | +| [exempt-pr-milestones](#exempt-pr-milestones) | Override [exempt-milestones](#exempt-milestones) for PRs only | | +| [exempt-all-milestones](#exempt-all-milestones) | Exempt all issues/PRs with milestones from stale | `false` | +| [exempt-all-issue-milestones](#exempt-all-issue-milestones) | Override [exempt-all-milestones](#exempt-all-milestones) for issues only | | +| [exempt-all-pr-milestones](#exempt-all-pr-milestones) | Override [exempt-all-milestones](#exempt-all-milestones) for PRs only | | +| [exempt-assignees](#exempt-assignees) | Assignees on issues/PRs exempted from stale | | +| [exempt-issue-assignees](#exempt-issue-assignees) | Override [exempt-assignees](#exempt-assignees) for issues only | | +| [exempt-pr-assignees](#exempt-pr-assignees) | Override [exempt-assignees](#exempt-assignees) for PRs only | | +| [exempt-pinned-issues](#exempt-pinned-issues) | Exempt pinned issues from stale | `false` | +| [exempt-all-assignees](#exempt-all-assignees) | Exempt all issues/PRs with assignees from stale | `false` | +| [exempt-all-issue-assignees](#exempt-all-issue-assignees) | Override [exempt-all-assignees](#exempt-all-assignees) for issues only | | +| [exempt-all-pr-assignees](#exempt-all-pr-assignees) | Override [exempt-all-assignees](#exempt-all-assignees) for PRs only | | +| [exempt-draft-pr](#exempt-draft-pr) | Skip the stale action for draft PRs | `false` | +| [enable-statistics](#enable-statistics) | Display statistics in the logs | `true` | | [ignore-updates](#ignore-updates) | Any update (update/comment) can reset the stale idle time on the issues/PRs | `false` | -| [ignore-issue-updates](#ignore-issue-updates) | Override [ignore-updates](#ignore-updates) for issues only | | -| [ignore-pr-updates](#ignore-pr-updates) | Override [ignore-updates](#ignore-updates) for PRs only | | -| [include-only-assigned](#include-only-assigned) | Process only assigned issues | `false` | +| [ignore-issue-updates](#ignore-issue-updates) | Override [ignore-updates](#ignore-updates) for issues only | | +| [ignore-pr-updates](#ignore-pr-updates) | Override [ignore-updates](#ignore-updates) for PRs only | | +| [include-only-assigned](#include-only-assigned) | Process only assigned issues | `false` | ### List of output options diff --git a/__tests__/classes/issues-processor-mock.ts b/__tests__/classes/issues-processor-mock.ts index 3b2d488fe..bd4c06e11 100644 --- a/__tests__/classes/issues-processor-mock.ts +++ b/__tests__/classes/issues-processor-mock.ts @@ -4,6 +4,7 @@ import {IComment} from '../../src/interfaces/comment'; import {IIssuesProcessorOptions} from '../../src/interfaces/issues-processor-options'; import {IPullRequest} from '../../src/interfaces/pull-request'; import {IState} from '../../src/interfaces/state/state'; +import {IIssueEvent} from '../../src/interfaces/issue-event'; export class IssuesProcessorMock extends IssuesProcessor { constructor( @@ -18,7 +19,8 @@ export class IssuesProcessorMock extends IssuesProcessor { issue: Issue, label: string ) => Promise, - getPullRequest?: (issue: Issue) => Promise + getPullRequest?: (issue: Issue) => Promise, + getIssueEvents?: (issue: Issue) => Promise ) { super(options, state); @@ -37,5 +39,9 @@ export class IssuesProcessorMock extends IssuesProcessor { if (getPullRequest) { this.getPullRequest = getPullRequest; } + + if (getIssueEvents) { + this.getIssueEvents = getIssueEvents; + } } } diff --git a/__tests__/constants/default-processor-options.ts b/__tests__/constants/default-processor-options.ts index 0265b6446..8b78ec3e9 100644 --- a/__tests__/constants/default-processor-options.ts +++ b/__tests__/constants/default-processor-options.ts @@ -17,6 +17,7 @@ export const DefaultProcessorOptions: IIssuesProcessorOptions = Object.freeze({ staleIssueLabel: 'Stale', closeIssueLabel: '', exemptIssueLabels: '', + exemptPinnedIssues: false, stalePrLabel: 'Stale', closePrLabel: '', exemptPrLabels: '', diff --git a/__tests__/pinned.spec.ts b/__tests__/pinned.spec.ts new file mode 100644 index 000000000..fa2a1a2b1 --- /dev/null +++ b/__tests__/pinned.spec.ts @@ -0,0 +1,89 @@ +import {IIssuesProcessorOptions} from '../src/interfaces/issues-processor-options'; +import {DefaultProcessorOptions} from './constants/default-processor-options'; +import {Issue} from '../src/classes/issue'; +import {generateIssue} from './functions/generate-issue'; +import {IssuesProcessorMock} from './classes/issues-processor-mock'; +import {alwaysFalseStateMock} from './classes/state-mock'; +import {IIssueEvent} from '../src/interfaces/issue-event'; + +const opts: IIssuesProcessorOptions = { + ...DefaultProcessorOptions, + daysBeforeClose: 0 +}; +const testIssueList = async (page: number): Promise => { + return page == 1 + ? [ + generateIssue(opts, 1, 'Issue 1', '2020-01-01T17:00:00Z'), + generateIssue(opts, 2, 'Issue 2', '2020-01-01T17:00:00Z') + ] + : []; +}; + +const pinnedEvent: IIssueEvent = { + created_at: '2020-01-01T17:00:00Z', + event: 'pinned', + label: {} +}; +const unpinnedEvent: IIssueEvent = { + created_at: '2020-01-01T17:00:00Z', + event: 'unpinned', + label: {} +}; +describe('exempt-pinned-issues options', (): void => { + it('pinned issues should be skipped if exemptPinnedIssues true', async () => { + const processor = new IssuesProcessorMock( + {...opts, exemptPinnedIssues: true}, + alwaysFalseStateMock, + testIssueList, + async () => [], + async () => new Date().toDateString(), + async (issue: Issue) => undefined, + async (issue: Issue) => (issue.number === 1 ? [pinnedEvent] : []) + ); + await processor.processIssues(1); + expect(processor.staleIssues).toHaveLength(1); + }); + + it('pinned issues should not be skipped if exemptPinnedIssues false', async () => { + const processor = new IssuesProcessorMock( + {...opts, exemptPinnedIssues: false}, + alwaysFalseStateMock, + testIssueList, + async () => [], + async () => new Date().toDateString(), + async (issue: Issue) => undefined, + async (issue: Issue) => (issue.number === 1 ? [pinnedEvent] : []) + ); + await processor.processIssues(1); + expect(processor.staleIssues).toHaveLength(2); + }); + + it('pinned issues should not be skipped if exemptPinnedIssues true but it was unpinned', async () => { + const processor = new IssuesProcessorMock( + {...opts, exemptPinnedIssues: true}, + alwaysFalseStateMock, + testIssueList, + async () => [], + async () => new Date().toDateString(), + async (issue: Issue) => undefined, + async (issue: Issue) => + issue.number === 1 ? [unpinnedEvent, pinnedEvent] : [] + ); + await processor.processIssues(1); + expect(processor.staleIssues).toHaveLength(2); + }); + it('pinned issues should not be skipped if exemptPinnedIssues true and it was unpinned and pinned', async () => { + const processor = new IssuesProcessorMock( + {...opts, exemptPinnedIssues: true}, + alwaysFalseStateMock, + testIssueList, + async () => [], + async () => new Date().toDateString(), + async (issue: Issue) => undefined, + async (issue: Issue) => + issue.number === 1 ? [pinnedEvent, unpinnedEvent, pinnedEvent] : [] + ); + await processor.processIssues(1); + expect(processor.staleIssues).toHaveLength(1); + }); +}); diff --git a/action.yml b/action.yml index 251c93925..1c80b1e5e 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 f964b4530..094fe3547 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/issue.spec.ts b/src/classes/issue.spec.ts index a2c82e268..161cac7d3 100644 --- a/src/classes/issue.spec.ts +++ b/src/classes/issue.spec.ts @@ -26,6 +26,7 @@ describe('Issue', (): void => { debugOnly: false, deleteBranch: false, exemptIssueLabels: '', + exemptPinnedIssues: false, exemptPrLabels: '', onlyLabels: '', onlyIssueLabels: '', diff --git a/src/classes/issues-processor.ts b/src/classes/issues-processor.ts index 821c83e0d..5940a5a29 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 930992284..1f33735df 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 768700415..f3f8b4460 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'),