diff --git a/__tests__/main.test.ts b/__tests__/main.test.ts index a7e341f..ed6d67f 100644 --- a/__tests__/main.test.ts +++ b/__tests__/main.test.ts @@ -47,7 +47,9 @@ const DefaultProcessorOptions: IssueProcessorOptions = Object.freeze({ operationsPerRun: 100, debugOnly: true, removeStaleWhenUpdated: false, - ascending: false + ascending: false, + skipStaleIssueMessage: false, + skipStalePrMessage: false }); test('empty issue list results in 1 operation', async () => { @@ -737,3 +739,157 @@ test('stale issues should not be closed until after the closed number of days (l expect(processor.removedLabelIssues.length).toEqual(0); expect(processor.staleIssues.length).toEqual(1); }); + +test('skips stale message on issues when skip-stale-issue-message is set', async () => { + let lastUpdate = new Date(); + lastUpdate.setDate(lastUpdate.getDate() - 10); + const TestIssueList: Issue[] = [ + generateIssue( + 1, + 'An issue that should be marked stale but not closed', + lastUpdate.toString(), + false + ) + ]; + + const opts = {...DefaultProcessorOptions}; + opts.daysBeforeStale = 5; // stale after 5 days + opts.daysBeforeClose = 20; // closes after 25 days + opts.skipStaleIssueMessage = true; + + const processor = new IssueProcessor( + opts, + async p => (p == 1 ? TestIssueList : []), + async (num, dt) => [], + async (issue, label) => new Date().toDateString() + ); + + // for sake of testing, mocking private function + const markSpy = jest.spyOn(processor as any, 'markStale'); + + await processor.processIssues(1); + + // issue should be staled + expect(processor.closedIssues.length).toEqual(0); + expect(processor.removedLabelIssues.length).toEqual(0); + expect(processor.staleIssues.length).toEqual(1); + + // comment should not be created + expect(markSpy).toHaveBeenCalledWith( + TestIssueList[0], + opts.staleIssueMessage, + opts.staleIssueLabel, + // this option is skipMessage + true + ); +}); + +test('skips stale message on prs when skip-stale-pr-message is set', async () => { + let lastUpdate = new Date(); + lastUpdate.setDate(lastUpdate.getDate() - 10); + const TestIssueList: Issue[] = [ + generateIssue( + 1, + 'An issue that should be marked stale but not closed', + lastUpdate.toString(), + true + ) + ]; + + const opts = {...DefaultProcessorOptions}; + opts.daysBeforeStale = 5; // stale after 5 days + opts.daysBeforeClose = 20; // closes after 25 days + opts.skipStalePrMessage = true; + + const processor = new IssueProcessor( + opts, + async p => (p == 1 ? TestIssueList : []), + async (num, dt) => [], + async (issue, label) => new Date().toDateString() + ); + + // for sake of testing, mocking private function + const markSpy = jest.spyOn(processor as any, 'markStale'); + + await processor.processIssues(1); + + // issue should be staled + expect(processor.closedIssues.length).toEqual(0); + expect(processor.removedLabelIssues.length).toEqual(0); + expect(processor.staleIssues.length).toEqual(1); + + // comment should not be created + expect(markSpy).toHaveBeenCalledWith( + TestIssueList[0], + opts.stalePrMessage, + opts.stalePrLabel, + // this option is skipMessage + true + ); +}); + +test('not providing state takes precedence over skipStaleIssueMessage', async () => { + let lastUpdate = new Date(); + lastUpdate.setDate(lastUpdate.getDate() - 10); + const TestIssueList: Issue[] = [ + generateIssue( + 1, + 'An issue that should be marked stale but not closed', + lastUpdate.toString(), + false + ) + ]; + + const opts = {...DefaultProcessorOptions}; + opts.daysBeforeStale = 5; // stale after 5 days + opts.daysBeforeClose = 20; // closes after 25 days + opts.skipStalePrMessage = true; + opts.staleIssueMessage = ''; + + const processor = new IssueProcessor( + opts, + async p => (p == 1 ? TestIssueList : []), + async (num, dt) => [], + async (issue, label) => new Date().toDateString() + ); + + await processor.processIssues(1); + + // issue should be staled + expect(processor.closedIssues.length).toEqual(0); + expect(processor.removedLabelIssues.length).toEqual(0); + expect(processor.staleIssues.length).toEqual(0); +}); + +test('not providing stalePrMessage takes precedence over skipStalePrMessage', async () => { + let lastUpdate = new Date(); + lastUpdate.setDate(lastUpdate.getDate() - 10); + const TestIssueList: Issue[] = [ + generateIssue( + 1, + 'An issue that should be marked stale but not closed', + lastUpdate.toString(), + true + ) + ]; + + const opts = {...DefaultProcessorOptions}; + opts.daysBeforeStale = 5; // stale after 5 days + opts.daysBeforeClose = 20; // closes after 25 days + opts.skipStalePrMessage = true; + opts.stalePrMessage = ''; + + const processor = new IssueProcessor( + opts, + async p => (p == 1 ? TestIssueList : []), + async (num, dt) => [], + async (issue, label) => new Date().toDateString() + ); + + await processor.processIssues(1); + + // issue should be staled + expect(processor.closedIssues.length).toEqual(0); + expect(processor.removedLabelIssues.length).toEqual(0); + expect(processor.staleIssues.length).toEqual(0); +}); diff --git a/action.yml b/action.yml index 8854e99..3454223 100644 --- a/action.yml +++ b/action.yml @@ -46,6 +46,12 @@ inputs: ascending: description: 'The order to get issues or pull requests. Defaults to false, which is descending' default: false + skip-stale-pr-message: + description: 'Skip adding stale message when marking a pull request as stale.' + default: false + skip-stale-issue-message: + description: 'Skip adding stale message when marking an issue as stale.' + default: false runs: using: 'node12' main: 'dist/index.js' diff --git a/dist/index.js b/dist/index.js index 53c24dc..2d46153 100644 --- a/dist/index.js +++ b/dist/index.js @@ -3548,7 +3548,9 @@ function getAndValidateArgs() { operationsPerRun: parseInt(core.getInput('operations-per-run', { required: true })), removeStaleWhenUpdated: !(core.getInput('remove-stale-when-updated') === 'false'), debugOnly: core.getInput('debug-only') === 'true', - ascending: core.getInput('ascending') === 'true' + ascending: core.getInput('ascending') === 'true', + skipStalePrMessage: core.getInput('skip-stale-pr-message') === 'true', + skipStaleIssueMessage: core.getInput('skip-stale-issue-message') === 'true' }; for (const numberInput of [ 'days-before-stale', @@ -3599,7 +3601,7 @@ exports.getUserAgent = getUserAgent; /***/ 215: /***/ (function(module) { -module.exports = {"_args":[["@octokit/rest@16.43.1","/Users/owenvoke/projects/stale"]],"_from":"@octokit/rest@16.43.1","_id":"@octokit/rest@16.43.1","_inBundle":false,"_integrity":"sha512-gfFKwRT/wFxq5qlNjnW2dh+qh74XgTQ2B179UX5K1HYCluioWj8Ndbgqw2PVqa1NnVJkGHp2ovMpVn/DImlmkw==","_location":"/@octokit/rest","_phantomChildren":{"@octokit/types":"2.8.2","deprecation":"2.3.1","once":"1.4.0"},"_requested":{"type":"version","registry":true,"raw":"@octokit/rest@16.43.1","name":"@octokit/rest","escapedName":"@octokit%2frest","scope":"@octokit","rawSpec":"16.43.1","saveSpec":null,"fetchSpec":"16.43.1"},"_requiredBy":["/","/@actions/github"],"_resolved":"https://registry.npmjs.org/@octokit/rest/-/rest-16.43.1.tgz","_spec":"16.43.1","_where":"/Users/owenvoke/projects/stale","author":{"name":"Gregor Martynus","url":"https://github.com/gr2m"},"bugs":{"url":"https://github.com/octokit/rest.js/issues"},"bundlesize":[{"path":"./dist/octokit-rest.min.js.gz","maxSize":"33 kB"}],"contributors":[{"name":"Mike de Boer","email":"info@mikedeboer.nl"},{"name":"Fabian Jakobs","email":"fabian@c9.io"},{"name":"Joe Gallo","email":"joe@brassafrax.com"},{"name":"Gregor Martynus","url":"https://github.com/gr2m"}],"dependencies":{"@octokit/auth-token":"^2.4.0","@octokit/plugin-paginate-rest":"^1.1.1","@octokit/plugin-request-log":"^1.0.0","@octokit/plugin-rest-endpoint-methods":"2.4.0","@octokit/request":"^5.2.0","@octokit/request-error":"^1.0.2","atob-lite":"^2.0.0","before-after-hook":"^2.0.0","btoa-lite":"^1.0.0","deprecation":"^2.0.0","lodash.get":"^4.4.2","lodash.set":"^4.3.2","lodash.uniq":"^4.5.0","octokit-pagination-methods":"^1.1.0","once":"^1.4.0","universal-user-agent":"^4.0.0"},"description":"GitHub REST API client for Node.js","devDependencies":{"@gimenete/type-writer":"^0.1.3","@octokit/auth":"^1.1.1","@octokit/fixtures-server":"^5.0.6","@octokit/graphql":"^4.2.0","@types/node":"^13.1.0","bundlesize":"^0.18.0","chai":"^4.1.2","compression-webpack-plugin":"^3.1.0","cypress":"^3.0.0","glob":"^7.1.2","http-proxy-agent":"^4.0.0","lodash.camelcase":"^4.3.0","lodash.merge":"^4.6.1","lodash.upperfirst":"^4.3.1","lolex":"^5.1.2","mkdirp":"^1.0.0","mocha":"^7.0.1","mustache":"^4.0.0","nock":"^11.3.3","npm-run-all":"^4.1.2","nyc":"^15.0.0","prettier":"^1.14.2","proxy":"^1.0.0","semantic-release":"^17.0.0","sinon":"^8.0.0","sinon-chai":"^3.0.0","sort-keys":"^4.0.0","string-to-arraybuffer":"^1.0.0","string-to-jsdoc-comment":"^1.0.0","typescript":"^3.3.1","webpack":"^4.0.0","webpack-bundle-analyzer":"^3.0.0","webpack-cli":"^3.0.0"},"files":["index.js","index.d.ts","lib","plugins"],"homepage":"https://github.com/octokit/rest.js#readme","keywords":["octokit","github","rest","api-client"],"license":"MIT","name":"@octokit/rest","nyc":{"ignore":["test"]},"publishConfig":{"access":"public"},"release":{"publish":["@semantic-release/npm",{"path":"@semantic-release/github","assets":["dist/*","!dist/*.map.gz"]}]},"repository":{"type":"git","url":"git+https://github.com/octokit/rest.js.git"},"scripts":{"build":"npm-run-all build:*","build:browser":"npm-run-all build:browser:*","build:browser:development":"webpack --mode development --entry . --output-library=Octokit --output=./dist/octokit-rest.js --profile --json > dist/bundle-stats.json","build:browser:production":"webpack --mode production --entry . --plugin=compression-webpack-plugin --output-library=Octokit --output-path=./dist --output-filename=octokit-rest.min.js --devtool source-map","build:ts":"npm run -s update-endpoints:typescript","coverage":"nyc report --reporter=html && open coverage/index.html","generate-bundle-report":"webpack-bundle-analyzer dist/bundle-stats.json --mode=static --no-open --report dist/bundle-report.html","lint":"prettier --check '{lib,plugins,scripts,test}/**/*.{js,json,ts}' 'docs/*.{js,json}' 'docs/src/**/*' index.js README.md package.json","lint:fix":"prettier --write '{lib,plugins,scripts,test}/**/*.{js,json,ts}' 'docs/*.{js,json}' 'docs/src/**/*' index.js README.md package.json","postvalidate:ts":"tsc --noEmit --target es6 test/typescript-validate.ts","prebuild:browser":"mkdirp dist/","pretest":"npm run -s lint","prevalidate:ts":"npm run -s build:ts","start-fixtures-server":"octokit-fixtures-server","test":"nyc mocha test/mocha-node-setup.js \"test/*/**/*-test.js\"","test:browser":"cypress run --browser chrome","update-endpoints":"npm-run-all update-endpoints:*","update-endpoints:fetch-json":"node scripts/update-endpoints/fetch-json","update-endpoints:typescript":"node scripts/update-endpoints/typescript","validate:ts":"tsc --target es6 --noImplicitAny index.d.ts"},"types":"index.d.ts","version":"16.43.1"}; +module.exports = {"_args":[["@octokit/rest@16.43.1","/Users/harshit/code/stale"]],"_from":"@octokit/rest@16.43.1","_id":"@octokit/rest@16.43.1","_inBundle":false,"_integrity":"sha512-gfFKwRT/wFxq5qlNjnW2dh+qh74XgTQ2B179UX5K1HYCluioWj8Ndbgqw2PVqa1NnVJkGHp2ovMpVn/DImlmkw==","_location":"/@octokit/rest","_phantomChildren":{"@octokit/types":"2.8.2","deprecation":"2.3.1","once":"1.4.0"},"_requested":{"type":"version","registry":true,"raw":"@octokit/rest@16.43.1","name":"@octokit/rest","escapedName":"@octokit%2frest","scope":"@octokit","rawSpec":"16.43.1","saveSpec":null,"fetchSpec":"16.43.1"},"_requiredBy":["/","/@actions/github"],"_resolved":"https://registry.npmjs.org/@octokit/rest/-/rest-16.43.1.tgz","_spec":"16.43.1","_where":"/Users/harshit/code/stale","author":{"name":"Gregor Martynus","url":"https://github.com/gr2m"},"bugs":{"url":"https://github.com/octokit/rest.js/issues"},"bundlesize":[{"path":"./dist/octokit-rest.min.js.gz","maxSize":"33 kB"}],"contributors":[{"name":"Mike de Boer","email":"info@mikedeboer.nl"},{"name":"Fabian Jakobs","email":"fabian@c9.io"},{"name":"Joe Gallo","email":"joe@brassafrax.com"},{"name":"Gregor Martynus","url":"https://github.com/gr2m"}],"dependencies":{"@octokit/auth-token":"^2.4.0","@octokit/plugin-paginate-rest":"^1.1.1","@octokit/plugin-request-log":"^1.0.0","@octokit/plugin-rest-endpoint-methods":"2.4.0","@octokit/request":"^5.2.0","@octokit/request-error":"^1.0.2","atob-lite":"^2.0.0","before-after-hook":"^2.0.0","btoa-lite":"^1.0.0","deprecation":"^2.0.0","lodash.get":"^4.4.2","lodash.set":"^4.3.2","lodash.uniq":"^4.5.0","octokit-pagination-methods":"^1.1.0","once":"^1.4.0","universal-user-agent":"^4.0.0"},"description":"GitHub REST API client for Node.js","devDependencies":{"@gimenete/type-writer":"^0.1.3","@octokit/auth":"^1.1.1","@octokit/fixtures-server":"^5.0.6","@octokit/graphql":"^4.2.0","@types/node":"^13.1.0","bundlesize":"^0.18.0","chai":"^4.1.2","compression-webpack-plugin":"^3.1.0","cypress":"^3.0.0","glob":"^7.1.2","http-proxy-agent":"^4.0.0","lodash.camelcase":"^4.3.0","lodash.merge":"^4.6.1","lodash.upperfirst":"^4.3.1","lolex":"^5.1.2","mkdirp":"^1.0.0","mocha":"^7.0.1","mustache":"^4.0.0","nock":"^11.3.3","npm-run-all":"^4.1.2","nyc":"^15.0.0","prettier":"^1.14.2","proxy":"^1.0.0","semantic-release":"^17.0.0","sinon":"^8.0.0","sinon-chai":"^3.0.0","sort-keys":"^4.0.0","string-to-arraybuffer":"^1.0.0","string-to-jsdoc-comment":"^1.0.0","typescript":"^3.3.1","webpack":"^4.0.0","webpack-bundle-analyzer":"^3.0.0","webpack-cli":"^3.0.0"},"files":["index.js","index.d.ts","lib","plugins"],"homepage":"https://github.com/octokit/rest.js#readme","keywords":["octokit","github","rest","api-client"],"license":"MIT","name":"@octokit/rest","nyc":{"ignore":["test"]},"publishConfig":{"access":"public"},"release":{"publish":["@semantic-release/npm",{"path":"@semantic-release/github","assets":["dist/*","!dist/*.map.gz"]}]},"repository":{"type":"git","url":"git+https://github.com/octokit/rest.js.git"},"scripts":{"build":"npm-run-all build:*","build:browser":"npm-run-all build:browser:*","build:browser:development":"webpack --mode development --entry . --output-library=Octokit --output=./dist/octokit-rest.js --profile --json > dist/bundle-stats.json","build:browser:production":"webpack --mode production --entry . --plugin=compression-webpack-plugin --output-library=Octokit --output-path=./dist --output-filename=octokit-rest.min.js --devtool source-map","build:ts":"npm run -s update-endpoints:typescript","coverage":"nyc report --reporter=html && open coverage/index.html","generate-bundle-report":"webpack-bundle-analyzer dist/bundle-stats.json --mode=static --no-open --report dist/bundle-report.html","lint":"prettier --check '{lib,plugins,scripts,test}/**/*.{js,json,ts}' 'docs/*.{js,json}' 'docs/src/**/*' index.js README.md package.json","lint:fix":"prettier --write '{lib,plugins,scripts,test}/**/*.{js,json,ts}' 'docs/*.{js,json}' 'docs/src/**/*' index.js README.md package.json","postvalidate:ts":"tsc --noEmit --target es6 test/typescript-validate.ts","prebuild:browser":"mkdirp dist/","pretest":"npm run -s lint","prevalidate:ts":"npm run -s build:ts","start-fixtures-server":"octokit-fixtures-server","test":"nyc mocha test/mocha-node-setup.js \"test/*/**/*-test.js\"","test:browser":"cypress run --browser chrome","update-endpoints":"npm-run-all update-endpoints:*","update-endpoints:fetch-json":"node scripts/update-endpoints/fetch-json","update-endpoints:typescript":"node scripts/update-endpoints/typescript","validate:ts":"tsc --target es6 --noImplicitAny index.d.ts"},"types":"index.d.ts","version":"16.43.1"}; /***/ }), @@ -8673,8 +8675,12 @@ class IssueProcessor { ? this.options.stalePrLabel : this.options.staleIssueLabel; const exemptLabels = IssueProcessor.parseCommaSeparatedString(isPr ? this.options.exemptPrLabels : this.options.exemptIssueLabels); + const skipMessage = isPr + ? this.options.skipStalePrMessage + : this.options.skipStaleIssueMessage; const issueType = isPr ? 'pr' : 'issue'; - if (!staleMessage) { + const shouldMarkWhenStale = this.options.daysBeforeStale > -1; + if (!staleMessage && shouldMarkWhenStale) { core.info(`Skipping ${issueType} due to empty stale message`); continue; } @@ -8695,9 +8701,9 @@ class IssueProcessor { // should this issue be marked stale? const shouldBeStale = !IssueProcessor.updatedSince(issue.updated_at, this.options.daysBeforeStale); // determine if this issue needs to be marked stale first - if (!isStale && shouldBeStale) { + if (!isStale && shouldBeStale && shouldMarkWhenStale) { core.info(`Marking ${issueType} stale because it was last updated on ${issue.updated_at} and it does not have a stale label`); - yield this.markStale(issue, staleMessage, staleLabel); + yield this.markStale(issue, staleMessage, staleLabel, skipMessage); isStale = true; // this issue is now considered stale } // process the issue if it was marked stale @@ -8798,7 +8804,7 @@ class IssueProcessor { }); } // Mark an issue as stale with a comment and a label - markStale(issue, staleMessage, staleLabel) { + markStale(issue, staleMessage, staleLabel, skipMessage) { return __awaiter(this, void 0, void 0, function* () { core.info(`Marking issue #${issue.number} - ${issue.title} as stale`); this.staleIssues.push(issue); @@ -8810,16 +8816,18 @@ class IssueProcessor { if (this.options.debugOnly) { return; } - try { - yield this.client.issues.createComment({ - owner: github.context.repo.owner, - repo: github.context.repo.repo, - issue_number: issue.number, - body: staleMessage - }); - } - catch (error) { - core.error(`Error creating a comment: ${error.message}`); + if (!skipMessage) { + try { + yield this.client.issues.createComment({ + owner: github.context.repo.owner, + repo: github.context.repo.repo, + issue_number: issue.number, + body: staleMessage + }); + } + catch (error) { + core.error(`Error creating a comment: ${error.message}`); + } } try { yield this.client.issues.addLabels({ diff --git a/src/IssueProcessor.ts b/src/IssueProcessor.ts index 3310e88..9d4125f 100644 --- a/src/IssueProcessor.ts +++ b/src/IssueProcessor.ts @@ -50,6 +50,8 @@ export interface IssueProcessorOptions { removeStaleWhenUpdated: boolean; debugOnly: boolean; ascending: boolean; + skipStaleIssueMessage: boolean; + skipStalePrMessage: boolean; } /*** @@ -129,6 +131,9 @@ export class IssueProcessor { const exemptLabels = IssueProcessor.parseCommaSeparatedString( isPr ? this.options.exemptPrLabels : this.options.exemptIssueLabels ); + const skipMessage = isPr + ? this.options.skipStalePrMessage + : this.options.skipStaleIssueMessage; const issueType: string = isPr ? 'pr' : 'issue'; const shouldMarkWhenStale = this.options.daysBeforeStale > -1; @@ -170,7 +175,7 @@ export class IssueProcessor { core.info( `Marking ${issueType} stale because it was last updated on ${issue.updated_at} and it does not have a stale label` ); - await this.markStale(issue, staleMessage, staleLabel); + await this.markStale(issue, staleMessage, staleLabel, skipMessage); isStale = true; // this issue is now considered stale } @@ -320,7 +325,8 @@ export class IssueProcessor { private async markStale( issue: Issue, staleMessage: string, - staleLabel: string + staleLabel: string, + skipMessage: boolean ): Promise { core.info(`Marking issue #${issue.number} - ${issue.title} as stale`); @@ -337,15 +343,17 @@ export class IssueProcessor { return; } - try { - await this.client.issues.createComment({ - owner: github.context.repo.owner, - repo: github.context.repo.repo, - issue_number: issue.number, - body: staleMessage - }); - } catch (error) { - core.error(`Error creating a comment: ${error.message}`); + if (!skipMessage) { + try { + await this.client.issues.createComment({ + owner: github.context.repo.owner, + repo: github.context.repo.repo, + issue_number: issue.number, + body: staleMessage + }); + } catch (error) { + core.error(`Error creating a comment: ${error.message}`); + } } try { diff --git a/src/main.ts b/src/main.ts index 3c391da..550509f 100644 --- a/src/main.ts +++ b/src/main.ts @@ -38,7 +38,9 @@ function getAndValidateArgs(): IssueProcessorOptions { core.getInput('remove-stale-when-updated') === 'false' ), debugOnly: core.getInput('debug-only') === 'true', - ascending: core.getInput('ascending') === 'true' + ascending: core.getInput('ascending') === 'true', + skipStalePrMessage: core.getInput('skip-stale-pr-message') === 'true', + skipStaleIssueMessage: core.getInput('skip-stale-issue-message') === 'true' }; for (const numberInput of [