diff --git a/__tests__/main.test.ts b/__tests__/main.test.ts index 48f15ff9..1dbbac71 100644 --- a/__tests__/main.test.ts +++ b/__tests__/main.test.ts @@ -1,3 +1,61 @@ -describe('TODO - Add a test suite', () => { - it('TODO - Add a test', async () => {}); +import * as core from '@actions/core'; +import * as github from '@actions/github'; +import {Octokit} from '@octokit/rest'; + +import {IssueProcessor, IssueProcessorOptions} from '../src/IssueProcessor'; + +type Issue = Octokit.IssuesListForRepoResponseItem; +type IssueLabel = Octokit.IssuesListForRepoResponseItemLabelsItem; +type IssueList = Octokit.Response; + +const FakeHeaders = { + date: 'none', + 'x-ratelimit-limit': '', + 'x-ratelimit-remaining': '', + 'x-ratelimit-reset': '', + 'x-Octokit-request-id': '', + 'x-Octokit-media-type': '', + link: '', + 'last-modified': '', + etag: '', + status: '' +}; + +const EmptyIssueList: IssueList = { + data: [], + status: 200, + headers: FakeHeaders, + + *[Symbol.iterator]() { + for (let i of this.data) { + yield i; + } + } +}; + +test('empty issue list results in 1 operation', async () => { + const options: IssueProcessorOptions = { + repoToken: 'none', + staleIssueMessage: 'This issue is stale', + stalePrMessage: 'This PR is stale', + daysBeforeStale: 1, + daysBeforeClose: 1, + staleIssueLabel: 'Stale', + exemptIssueLabels: '', + stalePrLabel: 'Stale', + exemptPrLabels: '', + onlyLabels: '', + operationsPerRun: 100, + debugOnly: true + }; + const processor = new IssueProcessor(options); + + // process our fake issue list + const operationsLeft = await processor.processIssues( + 1, + () => new Promise(resolve => resolve(EmptyIssueList)) + ); + + // processing an empty issue list should result in 1 operation + expect(operationsLeft).toEqual(99); }); diff --git a/dist/index.js b/dist/index.js index 9373ff74..7a8b1eba 100644 --- a/dist/index.js +++ b/dist/index.js @@ -3751,9 +3751,9 @@ function getAndValidateArgs() { daysBeforeStale: parseInt(core.getInput('days-before-stale', { required: true })), daysBeforeClose: parseInt(core.getInput('days-before-close', { required: true })), staleIssueLabel: core.getInput('stale-issue-label', { required: true }), - exemptIssueLabel: core.getInput('exempt-issue-label'), + exemptIssueLabels: core.getInput('exempt-issue-labels'), stalePrLabel: core.getInput('stale-pr-label', { required: true }), - exemptPrLabel: core.getInput('exempt-pr-label'), + exemptPrLabels: core.getInput('exempt-pr-labels'), onlyLabels: core.getInput('only-labels'), operationsPerRun: parseInt(core.getInput('operations-per-run', { required: true })), debugOnly: core.getInput('debug-only') === 'true' @@ -8449,11 +8449,14 @@ const github = __importStar(__webpack_require__(469)); class IssueProcessor { constructor(options) { this.operationsLeft = 0; + this.staleIssues = []; + this.closedIssues = []; this.options = options; this.operationsLeft = options.operationsPerRun; this.client = new github.GitHub(options.repoToken); } - processIssues(page = 1) { + processIssues(page = 1, getIssues = this.getIssues.bind(this) // used for injecting issues to test + ) { return __awaiter(this, void 0, void 0, function* () { if (this.options.debugOnly) { core.warning('Executing in debug mode. Debug output will be written but no issues will be processed.'); @@ -8463,7 +8466,8 @@ class IssueProcessor { return 0; } // get the next batch of issues - const issues = yield this.getIssues(page); + const issues = yield getIssues(page); + this.operationsLeft -= 1; if (issues.data.length <= 0) { core.debug('No more issues found to process. Exiting.'); return this.operationsLeft; @@ -8478,15 +8482,13 @@ class IssueProcessor { const staleLabel = isPr ? this.options.stalePrLabel : this.options.staleIssueLabel; - const exemptLabel = isPr - ? this.options.exemptPrLabel - : this.options.exemptIssueLabel; + const exemptLabels = IssueProcessor.parseCommaSeparatedString(isPr ? this.options.exemptPrLabels : this.options.exemptIssueLabels); const issueType = isPr ? 'pr' : 'issue'; if (!staleMessage) { core.debug(`Skipping ${issueType} due to empty stale message`); continue; } - if (exemptLabel && IssueProcessor.isLabeled(issue, exemptLabel)) { + if (exemptLabels.some((exemptLabel) => IssueProcessor.isLabeled(issue, exemptLabel))) { core.debug(`Skipping ${issueType} because it has an exempt label`); continue; // don't process exempt issues } @@ -8509,7 +8511,7 @@ class IssueProcessor { } } // do the next batch - return yield this.processIssues(page + 1); + return this.processIssues(page + 1); }); } // grab issues from github in baches of 100 @@ -8529,6 +8531,7 @@ class IssueProcessor { markStale(issue, staleMessage, staleLabel) { return __awaiter(this, void 0, void 0, function* () { core.debug(`Marking issue #${issue.number} - ${issue.title} as stale`); + this.staleIssues.push(issue); if (this.options.debugOnly) { return; } @@ -8550,6 +8553,7 @@ class IssueProcessor { closeIssue(issue) { return __awaiter(this, void 0, void 0, function* () { core.debug(`Closing issue #${issue.number} - ${issue.title} for being stale`); + this.closedIssues.push(issue); if (this.options.debugOnly) { return; } @@ -8570,6 +8574,13 @@ class IssueProcessor { const millisSinceLastUpdated = new Date().getTime() - new Date(issue.updated_at).getTime(); return millisSinceLastUpdated >= daysInMillis; } + static parseCommaSeparatedString(s) { + // String.prototype.split defaults to [''] when called on an empty string + // In this case, we'd prefer to just return an empty array indicating no labels + if (!s.length) + return []; + return s.split(','); + } } exports.IssueProcessor = IssueProcessor; diff --git a/src/IssueProcessor.ts b/src/IssueProcessor.ts index 126f6cc4..523fb417 100644 --- a/src/IssueProcessor.ts +++ b/src/IssueProcessor.ts @@ -29,6 +29,9 @@ export class IssueProcessor { readonly options: IssueProcessorOptions; private operationsLeft: number = 0; + readonly staleIssues: Issue[] = []; + readonly closedIssues: Issue[] = []; + constructor(options: IssueProcessorOptions) { this.options = options; this.operationsLeft = options.operationsPerRun; @@ -37,8 +40,8 @@ export class IssueProcessor { async processIssues( page: number = 1, - getIssues: (page: number) => Promise = this.getIssues // used for injecting issues to test - ): Promise { + getIssues: (page: number) => Promise = this.getIssues.bind(this) // used for injecting issues to test + ): Promise { if (this.options.debugOnly) { core.warning( 'Executing in debug mode. Debug output will be written but no issues will be processed.' @@ -52,6 +55,7 @@ export class IssueProcessor { // get the next batch of issues const issues: IssueList = await getIssues(page); + this.operationsLeft -= 1; if (issues.data.length <= 0) { core.debug('No more issues found to process. Exiting.'); @@ -73,8 +77,8 @@ export class IssueProcessor { ? this.options.stalePrLabel : this.options.staleIssueLabel; const exemptLabels = IssueProcessor.parseCommaSeparatedString( - isPr ? this.options.exemptPrLabels : this.options.exemptIssueLabels - ); + isPr ? this.options.exemptPrLabels : this.options.exemptIssueLabels + ); const issueType: string = isPr ? 'pr' : 'issue'; if (!staleMessage) { @@ -82,7 +86,11 @@ export class IssueProcessor { continue; } - if (exemptLabels.some((exemptLabel: string) => IssueProcessor.isLabeled(issue, exemptLabel))) { + if ( + exemptLabels.some((exemptLabel: string) => + IssueProcessor.isLabeled(issue, exemptLabel) + ) + ) { core.debug(`Skipping ${issueType} because it has an exempt label`); continue; // don't process exempt issues } @@ -141,6 +149,8 @@ export class IssueProcessor { ): Promise { core.debug(`Marking issue #${issue.number} - ${issue.title} as stale`); + this.staleIssues.push(issue); + if (this.options.debugOnly) { return; } @@ -166,6 +176,8 @@ export class IssueProcessor { `Closing issue #${issue.number} - ${issue.title} for being stale` ); + this.closedIssues.push(issue); + if (this.options.debugOnly) { return; }