Repository: actions/stale
Pull request #282 (2021-01-19 05:08:44)

Bump @typescript-eslint/parser from 4.13.0 to 4.14.0
Bumps [@typescript-eslint/parser](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/parser) from 4.13.0 to 4.14.0.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a href="https://github.com/typescript-eslint/typescript-eslint/releases"><code>@typescript-eslint/parser's releases</code></a>.</em></p>
<blockquote>
<h2>v4.14.0</h2>
<h1><a href="https://github.com/typescript-eslint/typescript-eslint/compare/v4.13.0...v4.14.0">4.14.0</a> (2021-01-18)</h1>
<h3>Features</h3>
<ul>
<li>add support for decorator metadata in scope analysis and in consistent-type-imports (<a href="https://github-redirect.dependabot.com/typescript-eslint/typescript-eslint/issues/2751">#2751</a>) (<a href="https://github.com/typescript-eslint/typescript-eslint/commit/445e416878b27a54bf07c2d3b84dabd7b06e51bc">445e416</a>), closes <a href="https://github-redirect.dependabot.com/typescript-eslint/typescript-eslint/issues/2559">#2559</a></li>
<li><strong>eslint-plugin:</strong> add <code>object-curly-spacing</code> rule (<a href="https://github-redirect.dependabot.com/typescript-eslint/typescript-eslint/issues/2892">#2892</a>) (<a href="https://github.com/typescript-eslint/typescript-eslint/commit/32bd18de80f4f8388717d0f0c16d493234362aa5">32bd18d</a>)</li>
</ul>
</blockquote>
</details>
<details>
<summary>Changelog</summary>
<p><em>Sourced from <a href="https://github.com/typescript-eslint/typescript-eslint/blob/master/packages/parser/CHANGELOG.md"><code>@typescript-eslint/parser's changelog</code></a>.</em></p>
<blockquote>
<h1><a href="https://github.com/typescript-eslint/typescript-eslint/compare/v4.13.0...v4.14.0">4.14.0</a> (2021-01-18)</h1>
<h3>Features</h3>
<ul>
<li>add support for decorator metadata in scope analysis and in consistent-type-imports (<a href="https://github-redirect.dependabot.com/typescript-eslint/typescript-eslint/issues/2751">#2751</a>) (<a href="https://github.com/typescript-eslint/typescript-eslint/commit/445e416878b27a54bf07c2d3b84dabd7b06e51bc">445e416</a>), closes <a href="https://github-redirect.dependabot.com/typescript-eslint/typescript-eslint/issues/2559">#2559</a></li>
</ul>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a href="https://github.com/typescript-eslint/typescript-eslint/commit/60c5dcd6868bc08f5c8cd47b89e0ed327f17c576"><code>60c5dcd</code></a> chore: publish v4.14.0</li>
<li><a href="https://github.com/typescript-eslint/typescript-eslint/commit/445e416878b27a54bf07c2d3b84dabd7b06e51bc"><code>445e416</code></a> feat: add support for decorator metadata in scope analysis and in consistent-...</li>
<li>See full diff in <a href="https://github.com/typescript-eslint/typescript-eslint/commits/v4.14.0/packages/parser">compare view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=@typescript-eslint/parser&package-manager=npm_and_yarn&previous-version=4.13.0&new-version=4.14.0)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)

Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it
- `@dependabot merge` will merge this PR after your CI passes on it
- `@dependabot squash and merge` will squash and merge this PR after your CI passes on it
- `@dependabot cancel merge` will cancel a previously requested merge and block automerging
- `@dependabot reopen` will reopen this PR if it is closed
- `@dependabot close` will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually
- `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself)


</details>


Patch:
diff --git a/README.md b/README.md
index dd19ebf..3100c8b 100644
--- a/README.md
+++ b/README.md
@@ -43,6 +43,9 @@ $ npm test
 | `close-pr-label`            | Label to apply on the closing pr.                                                            | Optional |
 | `exempt-issue-labels`       | Labels on an issue exempted from being marked as stale.                                      | Optional |
 | `exempt-pr-labels`          | Labels on the pr exempted from being marked as stale.                                        | Optional |
+| `exempt-milestones`         | Milestones on an issue or a pr exempted from being marked as stale.                          | Optional |
+| `exempt-issue-milestones`   | Milestones on an issue exempted from being marked as stale (override `exempt-milestones`).   | Optional |
+| `exempt-pr-milestones`      | Milestones on the pr exempted from being marked as stale (override `exempt-milestones`).     | Optional |
 | `only-labels`               | Only labels checked for stale issue/pr.                                                      | Optional |
 | `operations-per-run`        | Maximum number of operations per run (GitHub API CRUD related). _Defaults to **30**_         | Optional |
 | `remove-stale-when-updated` | Remove stale label from issue/pr on updates or comments. _Defaults to **true**_              | Optional |
@@ -181,6 +184,24 @@ jobs:
           start-date: '2020-18-04T00:00:00Z' // ISO 8601 or RFC 2822
 ```
 
+Avoid stale for specific milestones:
+
+```yaml
+name: 'Close stale issues and PRs'
+on:
+  schedule:
+    - cron: '30 1 * * *'
+
+jobs:
+  stale:
+    runs-on: ubuntu-latest
+    steps:
+      - uses: actions/stale@v3
+        with:
+          exempt-issue-milestones: 'future,alpha,beta'
+          exempt-pr-milestones: 'bugfix,improvement'
+```
+
 ### Debugging
 
 To see debug output from this action, you must set the secret `ACTIONS_STEP_DEBUG` to `true` in your repository. You can run this action in debug only mode (no actions will be taken on your issues) by passing `debug-only` `true` as an argument to the action.
diff --git a/action.yml b/action.yml
index 1c90b61..41f667a 100644
--- a/action.yml
+++ b/action.yml
@@ -23,20 +23,20 @@ inputs:
     required: false
     default: '60'
   days-before-issue-stale:
-    description: 'The number of days old an issue can be before marking it stale. Set to -1 to never mark issues as stale automatically. Override "days-before-stale" option regarding the issues only.'
+    description: 'The number of days old an issue can be before marking it stale. Set to -1 to never mark issues as stale automatically. Override "days-before-stale" option regarding only the issues.'
     required: false
   days-before-pr-stale:
-    description: 'The number of days old a pull request can be before marking it stale. Set to -1 to never mark pull requests as stale automatically. Override "days-before-stale" option regarding the pull requests only.'
+    description: 'The number of days old a pull request can be before marking it stale. Set to -1 to never mark pull requests as stale automatically. Override "days-before-stale" option regarding only the pull requests.'
     required: false
   days-before-close:
     description: 'The number of days to wait to close an issue or a pull request after it being marked stale. Set to -1 to never close stale issues or pull requests.'
     required: false
     default: '7'
   days-before-issue-close:
-    description: 'The number of days to wait to close an issue after it being marked stale. Set to -1 to never close stale issues. Override "days-before-close" option regarding the issues only.'
+    description: 'The number of days to wait to close an issue after it being marked stale. Set to -1 to never close stale issues. Override "days-before-close" option regarding only the issues.'
     required: false
   days-before-pr-close:
-    description: 'The number of days to wait to close a pull request after it being marked stale. Set to -1 to never close stale pull requests. Override "days-before-close" option regarding the pull requests only.'
+    description: 'The number of days to wait to close a pull request after it being marked stale. Set to -1 to never close stale pull requests. Override "days-before-close" option regarding only the pull requests.'
     required: false
   stale-issue-label:
     description: 'The label to apply when an issue is stale.'
@@ -60,6 +60,18 @@ inputs:
     description: 'The labels that mean a pull request is exempt from being marked stale. Separate multiple labels with commas (eg. "label1,label2")'
     default: ''
     required: false
+  exempt-milestones:
+    description: 'The milestones that mean an issue or a pr is exempt from being marked stale. Separate multiple milestones with commas (eg. "milestone1,milestone2")'
+    default: ''
+    required: false
+  exempt-issue-milestones:
+    description: 'The milestones that mean an issue is exempt from being marked stale. Separate multiple milestones with commas (eg. "milestone1,milestone2"). Override "exempt-milestones" option regarding only the issue.'
+    default: ''
+    required: false
+  exempt-pr-milestones:
+    description: 'The milestones that mean a pull request is exempt from being marked stale. Separate multiple milestones with commas (eg. "milestone1,milestone2"). Override "exempt-milestones" option regarding only the pull requests.'
+    default: ''
+    required: false
   only-labels:
     description: 'Only issues or pull requests with all of these labels are checked if stale. Defaults to `[]` (disabled) and can be a comma-separated list of labels.'
     default: ''
diff --git a/dist/index.js b/dist/index.js
index 19c0022..341822c 100644
--- a/dist/index.js
+++ b/dist/index.js
@@ -19,29 +19,31 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
 Object.defineProperty(exports, "__esModule", ({ value: true }));
 exports.IssueProcessor = void 0;
 const github_1 = __nccwpck_require__(5438);
+const issue_1 = __nccwpck_require__(4783);
+const issue_logger_1 = __nccwpck_require__(2984);
+const logger_1 = __nccwpck_require__(6212);
+const milestones_1 = __nccwpck_require__(4601);
 const get_humanized_date_1 = __nccwpck_require__(965);
 const is_date_more_recent_than_1 = __nccwpck_require__(1473);
 const is_valid_date_1 = __nccwpck_require__(891);
 const get_issue_type_1 = __nccwpck_require__(5153);
-const issue_logger_1 = __nccwpck_require__(1699);
-const logger_1 = __nccwpck_require__(8236);
 const is_labeled_1 = __nccwpck_require__(6792);
 const is_pull_request_1 = __nccwpck_require__(5400);
-const labels_to_list_1 = __nccwpck_require__(9107);
 const should_mark_when_stale_1 = __nccwpck_require__(2461);
-const logger = new logger_1.Logger();
+const words_to_list_1 = __nccwpck_require__(1883);
 /***
  * Handle processing of issues for staleness/closure.
  */
 class IssueProcessor {
     constructor(options, getActor, getIssues, listIssueComments, getLabelCreationDate) {
+        this._logger = new logger_1.Logger();
+        this._operationsLeft = 0;
         this.staleIssues = [];
         this.closedIssues = [];
         this.deletedBranchIssues = [];
         this.removedLabelIssues = [];
-        this.operationsLeft = 0;
         this.options = options;
-        this.operationsLeft = options.operationsPerRun;
+        this._operationsLeft = options.operationsPerRun;
         this.client = github_1.getOctokit(options.repoToken);
         if (getActor) {
             this._getActor = getActor;
@@ -56,7 +58,7 @@ class IssueProcessor {
             this._getLabelCreationDate = getLabelCreationDate;
         }
         if (this.options.debugOnly) {
-            logger.warning('Executing in debug mode. Debug output will be written but no issues will be processed.');
+            this._logger.warning('Executing in debug mode. Debug output will be written but no issues will be processed.');
         }
     }
     static _updatedSince(timestamp, num_days) {
@@ -68,39 +70,37 @@ class IssueProcessor {
         return __awaiter(this, void 0, void 0, function* () {
             // get the next batch of issues
             const issues = yield this._getIssues(page);
-            this.operationsLeft -= 1;
+            this._operationsLeft -= 1;
             const actor = yield this._getActor();
             if (issues.length <= 0) {
-                logger.info('---');
-                logger.info('No more issues found to process. Exiting.');
-                return this.operationsLeft;
+                this._logger.info('---');
+                this._logger.info('No more issues found to process. Exiting.');
+                return this._operationsLeft;
             }
             for (const issue of issues.values()) {
                 const issueLogger = new issue_logger_1.IssueLogger(issue);
-                const isPr = is_pull_request_1.isPullRequest(issue);
-                issueLogger.info(`Found issue: issue #${issue.number} last updated ${issue.updated_at} (is pr? ${isPr})`);
+                issueLogger.info(`Found issue: issue #${issue.number} last updated ${issue.updated_at} (is pr? ${issue.isPullRequest})`);
                 // calculate string based messages for this issue
-                const staleMessage = isPr
+                const staleMessage = issue.isPullRequest
                     ? this.options.stalePrMessage
                     : this.options.staleIssueMessage;
-                const closeMessage = isPr
+                const closeMessage = issue.isPullRequest
                     ? this.options.closePrMessage
                     : this.options.closeIssueMessage;
-                const staleLabel = isPr
+                const staleLabel = issue.isPullRequest
                     ? this.options.stalePrLabel
                     : this.options.staleIssueLabel;
-                const closeLabel = isPr
+                const closeLabel = issue.isPullRequest
                     ? this.options.closePrLabel
                     : this.options.closeIssueLabel;
-                const exemptLabels = labels_to_list_1.labelsToList(isPr ? this.options.exemptPrLabels : this.options.exemptIssueLabels);
-                const skipMessage = isPr
+                const skipMessage = issue.isPullRequest
                     ? this.options.skipStalePrMessage
                     : this.options.skipStaleIssueMessage;
-                const issueType = get_issue_type_1.getIssueType(isPr);
-                const daysBeforeStale = isPr
+                const issueType = get_issue_type_1.getIssueType(issue.isPullRequest);
+                const daysBeforeStale = issue.isPullRequest
                     ? this._getDaysBeforePrStale()
                     : this._getDaysBeforeIssueStale();
-                if (isPr) {
+                if (issue.isPullRequest) {
                     issueLogger.info(`Days before pull request stale: ${daysBeforeStale}`);
                 }
                 else {
@@ -134,38 +134,44 @@ class IssueProcessor {
                         continue; // don't process issues which were created before the start date
                     }
                 }
-                // Does this issue have a stale label?
-                let isStale = is_labeled_1.isLabeled(issue, staleLabel);
-                if (isStale) {
+                if (issue.isStale) {
                     issueLogger.info(`This issue has a stale label`);
                 }
                 else {
                     issueLogger.info(`This issue hasn't a stale label`);
                 }
+                const exemptLabels = words_to_list_1.wordsToList(issue.isPullRequest
+                    ? this.options.exemptPrLabels
+                    : this.options.exemptIssueLabels);
                 if (exemptLabels.some((exemptLabel) => is_labeled_1.isLabeled(issue, exemptLabel))) {
-                    if (isStale) {
+                    if (issue.isStale) {
                         issueLogger.info(`An exempt label was added after the stale label.`);
                         yield this._removeStaleLabel(issue, staleLabel);
                     }
                     issueLogger.info(`Skipping ${issueType} because it has an exempt label`);
                     continue; // don't process exempt issues
                 }
+                const milestones = new milestones_1.Milestones(this.options, issue);
+                if (milestones.shouldExemptMilestones()) {
+                    issueLogger.info(`Skipping ${issueType} because it has an exempt milestone`);
+                    continue; // don't process exempt milestones
+                }
                 // 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 && shouldMarkAsStale) {
+                if (!issue.isStale && shouldBeStale && shouldMarkAsStale) {
                     issueLogger.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, skipMessage);
-                    isStale = true; // this issue is now considered stale
+                    issue.isStale = true; // this issue is now considered stale
                 }
                 // process the issue if it was marked stale
-                if (isStale) {
+                if (issue.isStale) {
                     issueLogger.info(`Found a stale ${issueType}`);
                     yield this._processStaleIssue(issue, issueType, staleLabel, actor, closeMessage, closeLabel);
                 }
             }
-            if (this.operationsLeft <= 0) {
-                logger.warning('Reached max number of operations to process. Exiting.');
+            if (this._operationsLeft <= 0) {
+                this._logger.warning('Reached max number of operations to process. Exiting.');
                 return 0;
             }
             // do the next batch
@@ -244,7 +250,7 @@ class IssueProcessor {
                 return comments.data;
             }
             catch (error) {
-                logger.error(`List issue comments error: ${error.message}`);
+                this._logger.error(`List issue comments error: ${error.message}`);
                 return Promise.resolve([]);
             }
         });
@@ -262,7 +268,7 @@ class IssueProcessor {
             return actor.data.login;
         });
     }
-    // grab issues from github in baches of 100
+    // grab issues from github in batches of 100
     _getIssues(page) {
         return __awaiter(this, void 0, void 0, function* () {
             // generate type for response
@@ -277,10 +283,10 @@ class IssueProcessor {
                     direction: this.options.ascending ? 'asc' : 'desc',
                     page
                 });
-                return issueResult.data;
+                return issueResult.data.map((issue) => new issue_1.Issue(this.options, issue));
             }
             catch (error) {
-                logger.error(`Get issues for repo error: ${error.message}`);
+                this._logger.error(`Get issues for repo error: ${error.message}`);
                 return Promise.resolve([]);
             }
         });
@@ -291,7 +297,7 @@ class IssueProcessor {
             const issueLogger = new issue_logger_1.IssueLogger(issue);
             issueLogger.info(`Marking issue #${issue.number} as stale`);
             this.staleIssues.push(issue);
-            this.operationsLeft -= 2;
+            this._operationsLeft -= 2;
             // if the issue is being marked stale, the updated date should be changed to right now
             // so that close calculations work correctly
             const newUpdatedAtDate = new Date();
@@ -331,7 +337,7 @@ class IssueProcessor {
             const issueLogger = new issue_logger_1.IssueLogger(issue);
             issueLogger.info(`Closing issue #${issue.number} for being stale`);
             this.closedIssues.push(issue);
-            this.operationsLeft -= 1;
+            this._operationsLeft -= 1;
             if (this.options.debugOnly) {
                 return;
             }
@@ -377,7 +383,7 @@ class IssueProcessor {
     _getPullRequest(issue) {
         return __awaiter(this, void 0, void 0, function* () {
             const issueLogger = new issue_logger_1.IssueLogger(issue);
-            this.operationsLeft -= 1;
+            this._operationsLeft -= 1;
             try {
                 const pullRequest = yield this.client.pulls.get({
                     owner: github_1.context.repo.owner,
@@ -406,7 +412,7 @@ class IssueProcessor {
             }
             const branch = pullRequest.head.ref;
             issueLogger.info(`Deleting branch ${branch} from closed issue #${issue.number}`);
-            this.operationsLeft -= 1;
+            this._operationsLeft -= 1;
             try {
                 yield this.client.git.deleteRef({
                     owner: github_1.context.repo.owner,
@@ -425,7 +431,7 @@ class IssueProcessor {
             const issueLogger = new issue_logger_1.IssueLogger(issue);
             issueLogger.info(`Removing label "${label}" from issue #${issue.number}`);
             this.removedLabelIssues.push(issue);
-            this.operationsLeft -= 1;
+            this._operationsLeft -= 1;
             // @todo remove the debug only to be able to test the code below
             if (this.options.debugOnly) {
                 return;
@@ -449,7 +455,7 @@ class IssueProcessor {
         return __awaiter(this, void 0, void 0, function* () {
             const issueLogger = new issue_logger_1.IssueLogger(issue);
             issueLogger.info(`Checking for label on issue #${issue.number}`);
-            this.operationsLeft -= 1;
+            this._operationsLeft -= 1;
             const options = this.client.issues.listEvents.endpoint.merge({
                 owner: github_1.context.repo.owner,
                 repo: github_1.context.repo.repo,
@@ -499,7 +505,43 @@ exports.IssueProcessor = IssueProcessor;
 
 /***/ }),
 
-/***/ 1699:
+/***/ 4783:
+/***/ ((__unused_webpack_module, exports, __nccwpck_require__) => {
+
+"use strict";
+
+Object.defineProperty(exports, "__esModule", ({ value: true }));
+exports.Issue = void 0;
+const is_labeled_1 = __nccwpck_require__(6792);
+const is_pull_request_1 = __nccwpck_require__(5400);
+class Issue {
+    constructor(options, issue) {
+        this._options = options;
+        this.title = issue.title;
+        this.number = issue.number;
+        this.created_at = issue.created_at;
+        this.updated_at = issue.updated_at;
+        this.labels = issue.labels;
+        this.pull_request = issue.pull_request;
+        this.state = issue.state;
+        this.locked = issue.locked;
+        this.milestone = issue.milestone;
+        this.isPullRequest = is_pull_request_1.isPullRequest(this);
+        this.staleLabel = this._getStaleLabel();
+        this.isStale = is_labeled_1.isLabeled(this, this.staleLabel);
+    }
+    _getStaleLabel() {
+        return this.isPullRequest
+            ? this._options.stalePrLabel
+            : this._options.staleIssueLabel;
+    }
+}
+exports.Issue = Issue;
+
+
+/***/ }),
+
+/***/ 2984:
 /***/ (function(__unused_webpack_module, exports, __nccwpck_require__) {
 
 "use strict";
@@ -551,7 +593,7 @@ exports.IssueLogger = IssueLogger;
 
 /***/ }),
 
-/***/ 8236:
+/***/ 6212:
 /***/ (function(__unused_webpack_module, exports, __nccwpck_require__) {
 
 "use strict";
@@ -592,6 +634,58 @@ class Logger {
 exports.Logger = Logger;
 
 
+/***/ }),
+
+/***/ 4601:
+/***/ (function(__unused_webpack_module, exports, __nccwpck_require__) {
+
+"use strict";
+
+var __importDefault = (this && this.__importDefault) || function (mod) {
+    return (mod && mod.__esModule) ? mod : { "default": mod };
+};
+Object.defineProperty(exports, "__esModule", ({ value: true }));
+exports.Milestones = void 0;
+const lodash_deburr_1 = __importDefault(__nccwpck_require__(1601));
+const words_to_list_1 = __nccwpck_require__(1883);
+class Milestones {
+    constructor(options, issue) {
+        this._options = options;
+        this._issue = issue;
+    }
+    static _cleanMilestone(label) {
+        return lodash_deburr_1.default(label.toLowerCase());
+    }
+    shouldExemptMilestones() {
+        const exemptMilestones = this._getExemptMilestones();
+        return exemptMilestones.some((exemptMilestone) => this._hasMilestone(exemptMilestone));
+    }
+    _getExemptMilestones() {
+        return words_to_list_1.wordsToList(this._issue.isPullRequest
+            ? this._getExemptPullRequestMilestones()
+            : this._getExemptIssueMilestones());
+    }
+    _getExemptIssueMilestones() {
+        return this._options.exemptIssueMilestones !== ''
+            ? this._options.exemptIssueMilestones
+            : this._options.exemptMilestones;
+    }
+    _getExemptPullRequestMilestones() {
+        return this._options.exemptPrMilestones !== ''
+            ? this._options.exemptPrMilestones
+            : this._options.exemptMilestones;
+    }
+    _hasMilestone(milestone) {
+        if (!this._issue.milestone) {
+            return false;
+        }
+        return (Milestones._cleanMilestone(milestone) ===
+            Milestones._cleanMilestone(this._issue.milestone.title));
+    }
+}
+exports.Milestones = Milestones;
+
+
 /***/ }),
 
 /***/ 9639:
@@ -707,12 +801,12 @@ exports.isLabeled = void 0;
 const lodash_deburr_1 = __importDefault(__nccwpck_require__(1601));
 /**
  * @description
- * Check if the label is listed as a label of the issue
+ * Check if the given label is listed as a label of the given issue
  *
  * @param {Readonly<Issue>} issue A GitHub issue containing some labels
  * @param {Readonly<string>} label The label to check the presence with
  *
- * @return {boolean} Return true when the given label is also in the issue labels
+ * @return {boolean} Return true when the given label is also in the given issue labels
  */
 function isLabeled(issue, label) {
     return !!issue.labels.find((issueLabel) => {
@@ -742,51 +836,51 @@ exports.isPullRequest = isPullRequest;
 
 /***/ }),
 
-/***/ 9107:
+/***/ 2461:
 /***/ ((__unused_webpack_module, exports) => {
 
 "use strict";
 
 Object.defineProperty(exports, "__esModule", ({ value: true }));
-exports.labelsToList = void 0;
-/**
- * @description
- * Transform a string of comma separated labels
- * to an array of labels
- *
- * @example
- * labelsToList('label') => ['label']
- * labelsToList('label,label') => ['label', 'label']
- * labelsToList('kebab-label') => ['kebab-label']
- * labelsToList('kebab%20label') => ['kebab%20label']
- * labelsToList('label with words') => ['label with words']
- *
- * @param {Readonly<string>} labels A string of comma separated labels
- *
- * @return {string[]} A list of labels
- */
-function labelsToList(labels) {
-    if (!labels.length) {
-        return [];
-    }
-    return labels.split(',').map(l => l.trim());
+exports.shouldMarkWhenStale = void 0;
+function shouldMarkWhenStale(daysBeforeStale) {
+    return daysBeforeStale >= 0;
 }
-exports.labelsToList = labelsToList;
+exports.shouldMarkWhenStale = shouldMarkWhenStale;
 
 
 /***/ }),
 
-/***/ 2461:
+/***/ 1883:
 /***/ ((__unused_webpack_module, exports) => {
 
 "use strict";
 
 Object.defineProperty(exports, "__esModule", ({ value: true }));
-exports.shouldMarkWhenStale = void 0;
-function shouldMarkWhenStale(daysBeforeStale) {
-    return daysBeforeStale >= 0;
+exports.wordsToList = void 0;
+/**
+ * @description
+ * Transform a string of comma separated words
+ * to an array of words
+ *
+ * @example
+ * wordsToList('label') => ['label']
+ * wordsToList('label,label') => ['label', 'label']
+ * wordsToList('kebab-label') => ['kebab-label']
+ * wordsToList('kebab%20label') => ['kebab%20label']
+ * wordsToList('label with words') => ['label with words']
+ *
+ * @param {Readonly<string>} words A string of comma separated words
+ *
+ * @return {string[]} A list of words
+ */
+function wordsToList(words) {
+    if (!words.length) {
+        return [];
+    }
+    return words.split(',').map((word) => word.trim());
 }
-exports.shouldMarkWhenStale = shouldMarkWhenStale;
+exports.wordsToList = wordsToList;
 
 
 /***/ }),
@@ -870,15 +964,14 @@ function getAndValidateArgs() {
         deleteBranch: core.getInput('delete-branch') === 'true',
         startDate: core.getInput('start-date') !== ''
             ? core.getInput('start-date')
-            : undefined
+            : undefined,
+        exemptMilestones: core.getInput('exempt-milestones'),
+        exemptIssueMilestones: core.getInput('exempt-issue-milestones'),
+        exemptPrMilestones: core.getInput('exempt-pr-milestones')
     };
     for (const numberInput of [
         'days-before-stale',
-        'days-before-issue-stale',
-        'days-before-pr-stale',
         'days-before-close',
-        'days-before-issue-close',
-        'days-before-pr-close',
         'operations-per-run'
     ]) {
         if (isNaN(parseInt(core.getInput(numberInput)))) {
diff --git a/package-lock.json b/package-lock.json
index f2385c6..d32d1e1 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -2101,35 +2101,41 @@
       }
     },
     "@typescript-eslint/parser": {
-      "version": "4.13.0",
-      "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-4.13.0.tgz",
-      "integrity": "sha512-KO0J5SRF08pMXzq9+abyHnaGQgUJZ3Z3ax+pmqz9vl81JxmTTOUfQmq7/4awVfq09b6C4owNlOgOwp61pYRBSg==",
+      "version": "4.14.0",
+      "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-4.14.0.tgz",
+      "integrity": "sha512-sUDeuCjBU+ZF3Lzw0hphTyScmDDJ5QVkyE21pRoBo8iDl7WBtVFS+WDN3blY1CH3SBt7EmYCw6wfmJjF0l/uYg==",
       "dev": true,
       "requires": {
-        "@typescript-eslint/scope-manager": "4.13.0",
-        "@typescript-eslint/types": "4.13.0",
-        "@typescript-eslint/typescript-estree": "4.13.0",
+        "@typescript-eslint/scope-manager": "4.14.0",
+        "@typescript-eslint/types": "4.14.0",
+        "@typescript-eslint/typescript-estree": "4.14.0",
         "debug": "^4.1.1"
       },
       "dependencies": {
         "@typescript-eslint/scope-manager": {
-          "version": "4.13.0",
-          "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-4.13.0.tgz",
-          "integrity": "sha512-UpK7YLG2JlTp/9G4CHe7GxOwd93RBf3aHO5L+pfjIrhtBvZjHKbMhBXTIQNkbz7HZ9XOe++yKrXutYm5KmjWgQ==",
+          "version": "4.14.0",
+          "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-4.14.0.tgz",
+          "integrity": "sha512-/J+LlRMdbPh4RdL4hfP1eCwHN5bAhFAGOTsvE6SxsrM/47XQiPSgF5MDgLyp/i9kbZV9Lx80DW0OpPkzL+uf8Q==",
           "dev": true,
           "requires": {
-            "@typescript-eslint/types": "4.13.0",
-            "@typescript-eslint/visitor-keys": "4.13.0"
+            "@typescript-eslint/types": "4.14.0",
+            "@typescript-eslint/visitor-keys": "4.14.0"
           }
         },
+        "@typescript-eslint/types": {
+          "version": "4.14.0",
+          "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-4.14.0.tgz",
+          "integrity": "sha512-VsQE4VvpldHrTFuVPY1ZnHn/Txw6cZGjL48e+iBxTi2ksa9DmebKjAeFmTVAYoSkTk7gjA7UqJ7pIsyifTsI4A==",
+          "dev": true
+        },
         "@typescript-eslint/typescript-estree": {
-          "version": "4.13.0",
-          "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-4.13.0.tgz",
-          "integrity": "sha512-9A0/DFZZLlGXn5XA349dWQFwPZxcyYyCFX5X88nWs2uachRDwGeyPz46oTsm9ZJE66EALvEns1lvBwa4d9QxMg==",
+          "version": "4.14.0",
+          "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-4.14.0.tgz",
+          "integrity": "sha512-wRjZ5qLao+bvS2F7pX4qi2oLcOONIB+ru8RGBieDptq/SudYwshveORwCVU4/yMAd4GK7Fsf8Uq1tjV838erag==",
           "dev": true,
           "requires": {
-            "@typescript-eslint/types": "4.13.0",
-            "@typescript-eslint/visitor-keys": "4.13.0",
+            "@typescript-eslint/types": "4.14.0",
+            "@typescript-eslint/visitor-keys": "4.14.0",
             "debug": "^4.1.1",
             "globby": "^11.0.1",
             "is-glob": "^4.0.1",
@@ -2137,6 +2143,22 @@
             "semver": "^7.3.2",
             "tsutils": "^3.17.1"
           }
+        },
+        "@typescript-eslint/visitor-keys": {
+          "version": "4.14.0",
+          "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-4.14.0.tgz",
+          "integrity": "sha512-MeHHzUyRI50DuiPgV9+LxcM52FCJFYjJiWHtXlbyC27b80mfOwKeiKI+MHOTEpcpfmoPFm/vvQS88bYIx6PZTA==",
+          "dev": true,
+          "requires": {
+            "@typescript-eslint/types": "4.14.0",
+            "eslint-visitor-keys": "^2.0.0"
+          }
+        },
+        "eslint-visitor-keys": {
+          "version": "2.0.0",
+          "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-2.0.0.tgz",
+          "integrity": "sha512-QudtT6av5WXels9WjIM7qz1XD1cWGvX4gGXvp/zBn9nXG02D0utdU3Em2m/QjTnrsk6bBjmCygl3rmj118msQQ==",
+          "dev": true
         }
       }
     },
diff --git a/package.json b/package.json
index ef50666..e9dc53f 100644
--- a/package.json
+++ b/package.json
@@ -6,10 +6,12 @@
   "main": "lib/main.js",
   "scripts": {
     "build": "tsc",
-    "format": "prettier --write **/*.ts",
-    "format-check": "prettier --check **/*.ts",
-    "lint": "prettier --check --ignore-unknown **/*.{md,json,yml,ts} && eslint src/**/*.ts",
-    "lint:fix": "prettier --write --ignore-unknown **/*.{md,json,yml,ts} && eslint src/**/*.ts --fix",
+    "format": "prettier --write --ignore-unknown **/*.{md,json,yml,ts}",
+    "format-check": "prettier --check --ignore-unknown **/*.{md,json,yml,ts}",
+    "lint": "eslint src/**/*.ts",
+    "lint:fix": "eslint src/**/*.ts --fix",
+    "lint:all": "npm run format-check && npm run lint",
+    "lint:all:fix": "npm run format && npm run lint:fix",
     "pack": "ncc build",
     "test": "jest",
     "all": "npm run build && npm run format && npm run lint && npm run pack && npm test"
@@ -38,7 +40,7 @@
     "@types/node": "^14.14.21",
     "@types/semver": "^7.3.4",
     "@typescript-eslint/eslint-plugin": "^4.13.0",
-    "@typescript-eslint/parser": "^4.13.0",
+    "@typescript-eslint/parser": "^4.14.0",
     "@vercel/ncc": "^0.27.0",
     "eslint": "^7.18.0",
     "eslint-plugin-github": "^4.0.1",
diff --git a/src/IssueProcessor.ts b/src/IssueProcessor.ts
index e8a3a0e..f1c2d93 100644
--- a/src/IssueProcessor.ts
+++ b/src/IssueProcessor.ts
@@ -1,30 +1,21 @@
 import {context, getOctokit} from '@actions/github';
 import {GitHub} from '@actions/github/lib/utils';
 import {GetResponseTypeFromEndpointMethod} from '@octokit/types';
+import {Issue} from './classes/issue';
+import {IssueLogger} from './classes/loggers/issue-logger';
+import {Logger} from './classes/loggers/logger';
+import {Milestones} from './classes/milestones';
 import {IssueType} from './enums/issue-type';
 import {getHumanizedDate} from './functions/dates/get-humanized-date';
 import {isDateMoreRecentThan} from './functions/dates/is-date-more-recent-than';
 import {isValidDate} from './functions/dates/is-valid-date';
 import {getIssueType} from './functions/get-issue-type';
-import {IssueLogger} from './classes/issue-logger';
-import {Logger} from './classes/logger';
 import {isLabeled} from './functions/is-labeled';
 import {isPullRequest} from './functions/is-pull-request';
-import {labelsToList} from './functions/labels-to-list';
 import {shouldMarkWhenStale} from './functions/should-mark-when-stale';
-import {IsoDateString} from './types/iso-date-string';
 import {IsoOrRfcDateString} from './types/iso-or-rfc-date-string';
-
-export interface Issue {
-  title: string;
-  number: number;
-  created_at: IsoDateString;
-  updated_at: IsoDateString;
-  labels: Label[];
-  pull_request: any;
-  state: string;
-  locked: boolean;
-}
+import {wordsToList} from './functions/words-to-list';
+import {IIssue} from './interfaces/issue';
 
 export interface PullRequest {
   number: number;
@@ -79,10 +70,11 @@ export interface IssueProcessorOptions {
   skipStalePrMessage: boolean;
   deleteBranch: boolean;
   startDate: IsoOrRfcDateString | undefined; // Should be ISO 8601 or RFC 2822
+  exemptMilestones: string;
+  exemptIssueMilestones: string;
+  exemptPrMilestones: string;
 }
 
-const logger: Logger = new Logger();
-
 /***
  * Handle processing of issues for staleness/closure.
  */
@@ -95,13 +87,14 @@ export class IssueProcessor {
     return millisSinceLastUpdated <= daysInMillis;
   }
 
+  private readonly _logger: Logger = new Logger();
+  private _operationsLeft = 0;
   readonly client: InstanceType<typeof GitHub>;
   readonly options: IssueProcessorOptions;
   readonly staleIssues: Issue[] = [];
   readonly closedIssues: Issue[] = [];
   readonly deletedBranchIssues: Issue[] = [];
   readonly removedLabelIssues: Issue[] = [];
-  private operationsLeft = 0;
 
   constructor(
     options: IssueProcessorOptions,
@@ -117,7 +110,7 @@ export class IssueProcessor {
     ) => Promise<string | undefined>
   ) {
     this.options = options;
-    this.operationsLeft = options.operationsPerRun;
+    this._operationsLeft = options.operationsPerRun;
     this.client = getOctokit(options.repoToken);
 
     if (getActor) {
@@ -137,7 +130,7 @@ export class IssueProcessor {
     }
 
     if (this.options.debugOnly) {
-      logger.warning(
+      this._logger.warning(
         'Executing in debug mode. Debug output will be written but no issues will be processed.'
       );
     }
@@ -146,49 +139,45 @@ export class IssueProcessor {
   async processIssues(page = 1): Promise<number> {
     // get the next batch of issues
     const issues: Issue[] = await this._getIssues(page);
-    this.operationsLeft -= 1;
+    this._operationsLeft -= 1;
 
     const actor: string = await this._getActor();
 
     if (issues.length <= 0) {
-      logger.info('---');
-      logger.info('No more issues found to process. Exiting.');
-      return this.operationsLeft;
+      this._logger.info('---');
+      this._logger.info('No more issues found to process. Exiting.');
+      return this._operationsLeft;
     }
 
     for (const issue of issues.values()) {
       const issueLogger: IssueLogger = new IssueLogger(issue);
-      const isPr = isPullRequest(issue);
 
       issueLogger.info(
-        `Found issue: issue #${issue.number} last updated ${issue.updated_at} (is pr? ${isPr})`
+        `Found issue: issue #${issue.number} last updated ${issue.updated_at} (is pr? ${issue.isPullRequest})`
       );
 
       // calculate string based messages for this issue
-      const staleMessage: string = isPr
+      const staleMessage: string = issue.isPullRequest
         ? this.options.stalePrMessage
         : this.options.staleIssueMessage;
-      const closeMessage: string = isPr
+      const closeMessage: string = issue.isPullRequest
         ? this.options.closePrMessage
         : this.options.closeIssueMessage;
-      const staleLabel: string = isPr
+      const staleLabel: string = issue.isPullRequest
         ? this.options.stalePrLabel
         : this.options.staleIssueLabel;
-      const closeLabel: string = isPr
+      const closeLabel: string = issue.isPullRequest
         ? this.options.closePrLabel
         : this.options.closeIssueLabel;
-      const exemptLabels: string[] = labelsToList(
-        isPr ? this.options.exemptPrLabels : this.options.exemptIssueLabels
-      );
-      const skipMessage = isPr
+      const skipMessage = issue.isPullRequest
         ? this.options.skipStalePrMessage
         : this.options.skipStaleIssueMessage;
-      const issueType: IssueType = getIssueType(isPr);
-      const daysBeforeStale: number = isPr
+      const issueType: IssueType = getIssueType(issue.isPullRequest);
+      const daysBeforeStale: number = issue.isPullRequest
         ? this._getDaysBeforePrStale()
         : this._getDaysBeforeIssueStale();
 
-      if (isPr) {
+      if (issue.isPullRequest) {
         issueLogger.info(`Days before pull request stale: ${daysBeforeStale}`);
       } else {
         issueLogger.info(`Days before issue stale: ${daysBeforeStale}`);
@@ -244,21 +233,24 @@ export class IssueProcessor {
         }
       }
 
-      // Does this issue have a stale label?
-      let isStale: boolean = isLabeled(issue, staleLabel);
-
-      if (isStale) {
+      if (issue.isStale) {
         issueLogger.info(`This issue has a stale label`);
       } else {
         issueLogger.info(`This issue hasn't a stale label`);
       }
 
+      const exemptLabels: string[] = wordsToList(
+        issue.isPullRequest
+          ? this.options.exemptPrLabels
+          : this.options.exemptIssueLabels
+      );
+
       if (
         exemptLabels.some((exemptLabel: Readonly<string>): boolean =>
           isLabeled(issue, exemptLabel)
         )
       ) {
-        if (isStale) {
+        if (issue.isStale) {
           issueLogger.info(`An exempt label was added after the stale label.`);
           await this._removeStaleLabel(issue, staleLabel);
         }
@@ -269,6 +261,15 @@ export class IssueProcessor {
         continue; // don't process exempt issues
       }
 
+      const milestones: Milestones = new Milestones(this.options, issue);
+
+      if (milestones.shouldExemptMilestones()) {
+        issueLogger.info(
+          `Skipping ${issueType} because it has an exempt milestone`
+        );
+        continue; // don't process exempt milestones
+      }
+
       // should this issue be marked stale?
       const shouldBeStale = !IssueProcessor._updatedSince(
         issue.updated_at,
@@ -276,16 +277,16 @@ export class IssueProcessor {
       );
 
       // determine if this issue needs to be marked stale first
-      if (!isStale && shouldBeStale && shouldMarkAsStale) {
+      if (!issue.isStale && shouldBeStale && shouldMarkAsStale) {
         issueLogger.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, skipMessage);
-        isStale = true; // this issue is now considered stale
+        issue.isStale = true; // this issue is now considered stale
       }
 
       // process the issue if it was marked stale
-      if (isStale) {
+      if (issue.isStale) {
         issueLogger.info(`Found a stale ${issueType}`);
         await this._processStaleIssue(
           issue,
@@ -298,8 +299,10 @@ export class IssueProcessor {
       }
     }
 
-    if (this.operationsLeft <= 0) {
-      logger.warning('Reached max number of operations to process. Exiting.');
+    if (this._operationsLeft <= 0) {
+      this._logger.warning(
+        'Reached max number of operations to process. Exiting.'
+      );
       return 0;
     }
 
@@ -427,7 +430,7 @@ export class IssueProcessor {
       });
       return comments.data;
     } catch (error) {
-      logger.error(`List issue comments error: ${error.message}`);
+      this._logger.error(`List issue comments error: ${error.message}`);
       return Promise.resolve([]);
     }
   }
@@ -444,7 +447,7 @@ export class IssueProcessor {
     return actor.data.login;
   }
 
-  // grab issues from github in baches of 100
+  // grab issues from github in batches of 100
   private async _getIssues(page: number): Promise<Issue[]> {
     // generate type for response
     const endpoint = this.client.issues.listForRepo;
@@ -462,9 +465,12 @@ export class IssueProcessor {
           page
         }
       );
-      return issueResult.data;
+
+      return issueResult.data.map(
+        (issue: Readonly<IIssue>): Issue => new Issue(this.options, issue)
+      );
     } catch (error) {
-      logger.error(`Get issues for repo error: ${error.message}`);
+      this._logger.error(`Get issues for repo error: ${error.message}`);
       return Promise.resolve([]);
     }
   }
@@ -482,7 +488,7 @@ export class IssueProcessor {
 
     this.staleIssues.push(issue);
 
-    this.operationsLeft -= 2;
+    this._operationsLeft -= 2;
 
     // if the issue is being marked stale, the updated date should be changed to right now
     // so that close calculations work correctly
@@ -530,7 +536,7 @@ export class IssueProcessor {
 
     this.closedIssues.push(issue);
 
-    this.operationsLeft -= 1;
+    this._operationsLeft -= 1;
 
     if (this.options.debugOnly) {
       return;
@@ -578,7 +584,7 @@ export class IssueProcessor {
     issue: Issue
   ): Promise<PullRequest | undefined> {
     const issueLogger: IssueLogger = new IssueLogger(issue);
-    this.operationsLeft -= 1;
+    this._operationsLeft -= 1;
 
     try {
       const pullRequest = await this.client.pulls.get({
@@ -621,7 +627,7 @@ export class IssueProcessor {
       `Deleting branch ${branch} from closed issue #${issue.number}`
     );
 
-    this.operationsLeft -= 1;
+    this._operationsLeft -= 1;
 
     try {
       await this.client.git.deleteRef({
@@ -644,7 +650,7 @@ export class IssueProcessor {
 
     this.removedLabelIssues.push(issue);
 
-    this.operationsLeft -= 1;
+    this._operationsLeft -= 1;
 
     // @todo remove the debug only to be able to test the code below
     if (this.options.debugOnly) {
@@ -673,7 +679,7 @@ export class IssueProcessor {
 
     issueLogger.info(`Checking for label on issue #${issue.number}`);
 
-    this.operationsLeft -= 1;
+    this._operationsLeft -= 1;
 
     const options = this.client.issues.listEvents.endpoint.merge({
       owner: context.repo.owner,
@@ -722,7 +728,7 @@ export class IssueProcessor {
   }
 
   private async _removeStaleLabel(
-    issue: Readonly<Issue>,
+    issue: Issue,
     staleLabel: Readonly<string>
   ): Promise<void> {
     const issueLogger: IssueLogger = new IssueLogger(issue);
diff --git a/src/classes/issue.spec.ts b/src/classes/issue.spec.ts
new file mode 100644
index 0000000..0b395cf
--- /dev/null
+++ b/src/classes/issue.spec.ts
@@ -0,0 +1,208 @@
+import {IIssue} from '../interfaces/issue';
+import {IMilestone} from '../interfaces/milestone';
+import {IssueProcessorOptions, Label} from '../IssueProcessor';
+import {Issue} from './issue';
+
+describe('Issue', (): void => {
+  let issue: Issue;
+  let optionsInterface: IssueProcessorOptions;
+  let issueInterface: IIssue;
+
+  beforeEach((): void => {
+    optionsInterface = {
+      ascending: false,
+      closeIssueLabel: '',
+      closeIssueMessage: '',
+      closePrLabel: '',
+      closePrMessage: '',
+      daysBeforeClose: 0,
+      daysBeforeIssueClose: 0,
+      daysBeforeIssueStale: 0,
+      daysBeforePrClose: 0,
+      daysBeforePrStale: 0,
+      daysBeforeStale: 0,
+      debugOnly: false,
+      deleteBranch: false,
+      exemptIssueLabels: '',
+      exemptIssueMilestones: '',
+      exemptMilestones: '',
+      exemptPrLabels: '',
+      exemptPrMilestones: '',
+      onlyLabels: '',
+      operationsPerRun: 0,
+      removeStaleWhenUpdated: false,
+      repoToken: '',
+      skipStaleIssueMessage: false,
+      skipStalePrMessage: false,
+      staleIssueMessage: '',
+      stalePrMessage: '',
+      startDate: undefined,
+      stalePrLabel: 'dummy-stale-pr-label',
+      staleIssueLabel: 'dummy-stale-issue-label'
+    };
+    issueInterface = {
+      title: 'dummy-title',
+      number: 8,
+      created_at: 'dummy-created-at',
+      updated_at: 'dummy-updated-at',
+      labels: [
+        {
+          name: 'dummy-name'
+        }
+      ],
+      pull_request: {},
+      state: 'dummy-state',
+      locked: false,
+      milestone: {
+        title: 'dummy-milestone'
+      }
+    };
+    issue = new Issue(optionsInterface, issueInterface);
+  });
+
+  describe('constructor()', (): void => {
+    it('should set the title with the given issue title', (): void => {
+      expect.assertions(1);
+
+      expect(issue.title).toStrictEqual('dummy-title');
+    });
+
+    it('should set the number with the given issue number', (): void => {
+      expect.assertions(1);
+
+      expect(issue.number).toStrictEqual(8);
+    });
+
+    it('should set the created_at with the given issue created_at', (): void => {
+      expect.assertions(1);
+
+      expect(issue.created_at).toStrictEqual('dummy-created-at');
+    });
+
+    it('should set the updated_at with the given issue updated_at', (): void => {
+      expect.assertions(1);
+
+      expect(issue.updated_at).toStrictEqual('dummy-updated-at');
+    });
+
+    it('should set the labels with the given issue labels', (): void => {
+      expect.assertions(1);
+
+      expect(issue.labels).toStrictEqual([
+        {
+          name: 'dummy-name'
+        } as Label
+      ]);
+    });
+
+    it('should set the pull_request with the given issue pull_request', (): void => {
+      expect.assertions(1);
+
+      expect(issue.pull_request).toStrictEqual({});
+    });
+
+    it('should set the state with the given issue state', (): void => {
+      expect.assertions(1);
+
+      expect(issue.state).toStrictEqual('dummy-state');
+    });
+
+    it('should set the locked with the given issue locked', (): void => {
+      expect.assertions(1);
+
+      expect(issue.locked).toStrictEqual(false);
+    });
+
+    it('should set the milestone with the given issue milestone', (): void => {
+      expect.assertions(1);
+
+      expect(issue.milestone).toStrictEqual({
+        title: 'dummy-milestone'
+      } as IMilestone);
+    });
+
+    describe('when the given issue pull_request is not set', (): void => {
+      beforeEach((): void => {
+        issueInterface.pull_request = undefined;
+        issue = new Issue(optionsInterface, issueInterface);
+      });
+
+      it('should set the isPullRequest to false', (): void => {
+        expect.assertions(1);
+
+        expect(issue.isPullRequest).toStrictEqual(false);
+      });
+    });
+
+    describe('when the given issue pull_request is set', (): void => {
+      beforeEach((): void => {
+        issueInterface.pull_request = {};
+        issue = new Issue(optionsInterface, issueInterface);
+      });
+
+      it('should set the isPullRequest to true', (): void => {
+        expect.assertions(1);
+
+        expect(issue.isPullRequest).toStrictEqual(true);
+      });
+    });
+
+    describe('when the given issue is not a pull request', (): void => {
+      beforeEach((): void => {
+        issueInterface.pull_request = undefined;
+        issue = new Issue(optionsInterface, issueInterface);
+      });
+
+      it('should set the staleLabel with the given option staleIssueLabel', (): void => {
+        expect.assertions(1);
+
+        expect(issue.staleLabel).toStrictEqual('dummy-stale-issue-label');
+      });
+    });
+
+    describe('when the given issue is a pull request', (): void => {
+      beforeEach((): void => {
+        issueInterface.pull_request = {};
+        issue = new Issue(optionsInterface, issueInterface);
+      });
+
+      it('should set the staleLabel with the given option stalePrLabel', (): void => {
+        expect.assertions(1);
+
+        expect(issue.staleLabel).toStrictEqual('dummy-stale-pr-label');
+      });
+    });
+
+    describe('when the given issue does not contains the stale label', (): void => {
+      beforeEach((): void => {
+        issueInterface.pull_request = undefined;
+        issueInterface.labels = [];
+        issue = new Issue(optionsInterface, issueInterface);
+      });
+
+      it('should set the isStale to false', (): void => {
+        expect.assertions(1);
+
+        expect(issue.isStale).toStrictEqual(false);
+      });
+    });
+
+    describe('when the given issue contains the stale label', (): void => {
+      beforeEach((): void => {
+        issueInterface.pull_request = undefined;
+        issueInterface.labels = [
+          {
+            name: 'dummy-stale-issue-label'
+          } as Label
+        ];
+        issue = new Issue(optionsInterface, issueInterface);
+      });
+
+      it('should set the isStale to true', (): void => {
+        expect.assertions(1);
+
+        expect(issue.isStale).toStrictEqual(true);
+      });
+    });
+  });
+});
diff --git a/src/classes/issue.ts b/src/classes/issue.ts
new file mode 100644
index 0000000..31a40c7
--- /dev/null
+++ b/src/classes/issue.ts
@@ -0,0 +1,48 @@
+import {isLabeled} from '../functions/is-labeled';
+import {isPullRequest} from '../functions/is-pull-request';
+import {IIssue} from '../interfaces/issue';
+import {IMilestone} from '../interfaces/milestone';
+import {IssueProcessorOptions, Label} from '../IssueProcessor';
+import {IsoDateString} from '../types/iso-date-string';
+
+export class Issue implements IIssue {
+  private readonly _options: IssueProcessorOptions;
+  readonly title: string;
+  readonly number: number;
+  created_at: IsoDateString;
+  updated_at: IsoDateString;
+  readonly labels: Label[];
+  readonly pull_request: Object | null | undefined;
+  readonly state: string;
+  readonly locked: boolean;
+  readonly milestone: IMilestone | undefined;
+  readonly isPullRequest: boolean;
+  readonly staleLabel: string;
+  isStale: boolean;
+
+  constructor(
+    options: Readonly<IssueProcessorOptions>,
+    issue: Readonly<IIssue>
+  ) {
+    this._options = options;
+    this.title = issue.title;
+    this.number = issue.number;
+    this.created_at = issue.created_at;
+    this.updated_at = issue.updated_at;
+    this.labels = issue.labels;
+    this.pull_request = issue.pull_request;
+    this.state = issue.state;
+    this.locked = issue.locked;
+    this.milestone = issue.milestone;
+
+    this.isPullRequest = isPullRequest(this);
+    this.staleLabel = this._getStaleLabel();
+    this.isStale = isLabeled(this, this.staleLabel);
+  }
+
+  private _getStaleLabel(): string {
+    return this.isPullRequest
+      ? this._options.stalePrLabel
+      : this._options.staleIssueLabel;
+  }
+}
diff --git a/src/classes/issue-logger.spec.ts b/src/classes/loggers/issue-logger.spec.ts
similarity index 97%
rename from src/classes/issue-logger.spec.ts
rename to src/classes/loggers/issue-logger.spec.ts
index 23d4b9a..27e7f90 100644
--- a/src/classes/issue-logger.spec.ts
+++ b/src/classes/loggers/issue-logger.spec.ts
@@ -1,4 +1,4 @@
-import {Issue} from '../IssueProcessor';
+import {Issue} from '../issue';
 import {IssueLogger} from './issue-logger';
 import * as core from '@actions/core';
 
diff --git a/src/classes/issue-logger.ts b/src/classes/loggers/issue-logger.ts
similarity index 89%
rename from src/classes/issue-logger.ts
rename to src/classes/loggers/issue-logger.ts
index bbdbae7..979c231 100644
--- a/src/classes/issue-logger.ts
+++ b/src/classes/loggers/issue-logger.ts
@@ -1,11 +1,11 @@
 import * as core from '@actions/core';
-import {Issue} from '../IssueProcessor';
+import {Issue} from '../issue';
 import {Logger} from './logger';
 
 export class IssueLogger implements Logger {
   private readonly _issue: Issue;
 
-  constructor(issue: Readonly<Issue>) {
+  constructor(issue: Issue) {
     this._issue = issue;
   }
 
diff --git a/src/classes/logger.spec.ts b/src/classes/loggers/logger.spec.ts
similarity index 100%
rename from src/classes/logger.spec.ts
rename to src/classes/loggers/logger.spec.ts
diff --git a/src/classes/logger.ts b/src/classes/loggers/logger.ts
similarity index 100%
rename from src/classes/logger.ts
rename to src/classes/loggers/logger.ts
diff --git a/src/classes/milestones.spec.ts b/src/classes/milestones.spec.ts
new file mode 100644
index 0000000..032c161
--- /dev/null
+++ b/src/classes/milestones.spec.ts
@@ -0,0 +1,596 @@
+import {IIssue} from '../interfaces/issue';
+import {IssueProcessorOptions} from '../IssueProcessor';
+import {Issue} from './issue';
+import {Milestones} from './milestones';
+
+describe('Milestones', (): void => {
+  let milestones: Milestones;
+  let optionsInterface: IssueProcessorOptions;
+  let issue: Issue;
+  let issueInterface: IIssue;
+
+  beforeEach((): void => {
+    optionsInterface = {
+      ascending: false,
+      closeIssueLabel: '',
+      closeIssueMessage: '',
+      closePrLabel: '',
+      closePrMessage: '',
+      daysBeforeClose: 0,
+      daysBeforeIssueClose: 0,
+      daysBeforeIssueStale: 0,
+      daysBeforePrClose: 0,
+      daysBeforePrStale: 0,
+      daysBeforeStale: 0,
+      debugOnly: false,
+      deleteBranch: false,
+      exemptIssueLabels: '',
+      exemptPrLabels: '',
+      onlyLabels: '',
+      operationsPerRun: 0,
+      removeStaleWhenUpdated: false,
+      repoToken: '',
+      skipStaleIssueMessage: false,
+      skipStalePrMessage: false,
+      staleIssueLabel: '',
+      staleIssueMessage: '',
+      stalePrLabel: '',
+      stalePrMessage: '',
+      startDate: undefined,
+      exemptIssueMilestones: '',
+      exemptPrMilestones: '',
+      exemptMilestones: ''
+    };
+    issueInterface = {
+      created_at: '',
+      locked: false,
+      milestone: undefined,
+      number: 0,
+      pull_request: undefined,
+      state: '',
+      title: '',
+      updated_at: '',
+      labels: []
+    };
+  });
+
+  describe('shouldExemptMilestones()', (): void => {
+    describe('when the given issue is not a pull request', (): void => {
+      beforeEach((): void => {
+        issueInterface.pull_request = undefined;
+      });
+
+      describe('when the given options are not configured to exempt a milestone', (): void => {
+        beforeEach((): void => {
+          optionsInterface.exemptMilestones = '';
+        });
+
+        describe('when the given options are not configured to exempt an issue milestone', (): void => {
+          beforeEach((): void => {
+            optionsInterface.exemptIssueMilestones = '';
+          });
+
+          describe('when the given issue does not have a milestone', (): void => {
+            beforeEach((): void => {
+              issueInterface.milestone = undefined;
+            });
+
+            it('should return false', (): void => {
+              expect.assertions(1);
+              issue = new Issue(optionsInterface, issueInterface);
+              milestones = new Milestones(optionsInterface, issue);
+
+              const result = milestones.shouldExemptMilestones();
+
+              expect(result).toStrictEqual(false);
+            });
+          });
+
+          describe('when the given issue does have a milestone', (): void => {
+            beforeEach((): void => {
+              issueInterface.milestone = {
+                title: 'dummy-title'
+              };
+            });
+
+            it('should return false', (): void => {
+              expect.assertions(1);
+              issue = new Issue(optionsInterface, issueInterface);
+              milestones = new Milestones(optionsInterface, issue);
+
+              const result = milestones.shouldExemptMilestones();
+
+              expect(result).toStrictEqual(false);
+            });
+          });
+        });
+
+        describe('when the given options are configured to exempt an issue milestone', (): void => {
+          beforeEach((): void => {
+            optionsInterface.exemptIssueMilestones =
+              'dummy-exempt-issue-milestone';
+          });
+
+          describe('when the given issue does not have a milestone', (): void => {
+            beforeEach((): void => {
+              issueInterface.milestone = undefined;
+            });
+
+            it('should return false', (): void => {
+              expect.assertions(1);
+              issue = new Issue(optionsInterface, issueInterface);
+              milestones = new Milestones(optionsInterface, issue);
+
+              const result = milestones.shouldExemptMilestones();
+
+              expect(result).toStrictEqual(false);
+            });
+          });
+
+          describe('when the given issue does have a milestone different than the exempt issue milestone', (): void => {
+            beforeEach((): void => {
+              issueInterface.milestone = {
+                title: 'dummy-title'
+              };
+            });
+
+            it('should return false', (): void => {
+              expect.assertions(1);
+              issue = new Issue(optionsInterface, issueInterface);
+              milestones = new Milestones(optionsInterface, issue);
+
+              const result = milestones.shouldExemptMilestones();
+
+              expect(result).toStrictEqual(false);
+            });
+          });
+
+          describe('when the given issue does have a milestone equaling the exempt issue milestone', (): void => {
+            beforeEach((): void => {
+              issueInterface.milestone = {
+                title: 'dummy-exempt-issue-milestone'
+              };
+            });
+
+            it('should return true', (): void => {
+              expect.assertions(1);
+              issue = new Issue(optionsInterface, issueInterface);
+              milestones = new Milestones(optionsInterface, issue);
+
+              const result = milestones.shouldExemptMilestones();
+
+              expect(result).toStrictEqual(true);
+            });
+          });
+        });
+      });
+
+      describe('when the given options are configured to exempt a milestone', (): void => {
+        beforeEach((): void => {
+          optionsInterface.exemptMilestones = 'dummy-exempt-milestone';
+        });
+
+        describe('when the given options are not configured to exempt an issue milestone', (): void => {
+          beforeEach((): void => {
+            optionsInterface.exemptIssueMilestones = '';
+          });
+
+          describe('when the given issue does not have a milestone', (): void => {
+            beforeEach((): void => {
+              issueInterface.milestone = undefined;
+            });
+
+            it('should return false', (): void => {
+              expect.assertions(1);
+              issue = new Issue(optionsInterface, issueInterface);
+              milestones = new Milestones(optionsInterface, issue);
+
+              const result = milestones.shouldExemptMilestones();
+
+              expect(result).toStrictEqual(false);
+            });
+          });
+
+          describe('when the given issue does have a milestone different than the exempt milestone', (): void => {
+            beforeEach((): void => {
+              issueInterface.milestone = {
+                title: 'dummy-title'
+              };
+            });
+
+            it('should return false', (): void => {
+              expect.assertions(1);
+              issue = new Issue(optionsInterface, issueInterface);
+              milestones = new Milestones(optionsInterface, issue);
+
+              const result = milestones.shouldExemptMilestones();
+
+              expect(result).toStrictEqual(false);
+            });
+          });
+
+          describe('when the given issue does have a milestone equaling the exempt milestone', (): void => {
+            beforeEach((): void => {
+              issueInterface.milestone = {
+                title: 'dummy-exempt-milestone'
+              };
+            });
+
+            it('should return true', (): void => {
+              expect.assertions(1);
+              issue = new Issue(optionsInterface, issueInterface);
+              milestones = new Milestones(optionsInterface, issue);
+
+              const result = milestones.shouldExemptMilestones();
+
+              expect(result).toStrictEqual(true);
+            });
+          });
+        });
+
+        describe('when the given options are configured to exempt an issue milestone', (): void => {
+          beforeEach((): void => {
+            optionsInterface.exemptIssueMilestones =
+              'dummy-exempt-issue-milestone';
+          });
+
+          describe('when the given issue does not have a milestone', (): void => {
+            beforeEach((): void => {
+              issueInterface.milestone = undefined;
+            });
+
+            it('should return false', (): void => {
+              expect.assertions(1);
+              issue = new Issue(optionsInterface, issueInterface);
+              milestones = new Milestones(optionsInterface, issue);
+
+              const result = milestones.shouldExemptMilestones();
+
+              expect(result).toStrictEqual(false);
+            });
+          });
+
+          describe('when the given issue does have a milestone different than the exempt issue milestone', (): void => {
+            beforeEach((): void => {
+              issueInterface.milestone = {
+                title: 'dummy-title'
+              };
+            });
+
+            it('should return false', (): void => {
+              expect.assertions(1);
+              issue = new Issue(optionsInterface, issueInterface);
+              milestones = new Milestones(optionsInterface, issue);
+
+              const result = milestones.shouldExemptMilestones();
+
+              expect(result).toStrictEqual(false);
+            });
+          });
+
+          describe('when the given issue does have a milestone equaling the exempt issue milestone', (): void => {
+            beforeEach((): void => {
+              issueInterface.milestone = {
+                title: 'dummy-exempt-issue-milestone'
+              };
+            });
+
+            it('should return true', (): void => {
+              expect.assertions(1);
+              issue = new Issue(optionsInterface, issueInterface);
+              milestones = new Milestones(optionsInterface, issue);
+
+              const result = milestones.shouldExemptMilestones();
+
+              expect(result).toStrictEqual(true);
+            });
+          });
+
+          describe('when the given issue does have a milestone different than the exempt milestone', (): void => {
+            beforeEach((): void => {
+              issueInterface.milestone = {
+                title: 'dummy-title'
+              };
+            });
+
+            it('should return false', (): void => {
+              expect.assertions(1);
+              issue = new Issue(optionsInterface, issueInterface);
+              milestones = new Milestones(optionsInterface, issue);
+
+              const result = milestones.shouldExemptMilestones();
+
+              expect(result).toStrictEqual(false);
+            });
+          });
+
+          describe('when the given issue does have a milestone equaling the exempt milestone', (): void => {
+            beforeEach((): void => {
+              issueInterface.milestone = {
+                title: 'dummy-exempt-milestone'
+              };
+            });
+
+            it('should return false', (): void => {
+              expect.assertions(1);
+              issue = new Issue(optionsInterface, issueInterface);
+              milestones = new Milestones(optionsInterface, issue);
+
+              const result = milestones.shouldExemptMilestones();
+
+              expect(result).toStrictEqual(false);
+            });
+          });
+        });
+      });
+    });
+
+    describe('when the given issue is a pull request', (): void => {
+      beforeEach((): void => {
+        issueInterface.pull_request = {};
+      });
+
+      describe('when the given options are not configured to exempt a milestone', (): void => {
+        beforeEach((): void => {
+          optionsInterface.exemptMilestones = '';
+        });
+
+        describe('when the given options are not configured to exempt a pull request milestone', (): void => {
+          beforeEach((): void => {
+            optionsInterface.exemptPrMilestones = '';
+          });
+
+          describe('when the given issue does not have a milestone', (): void => {
+            beforeEach((): void => {
+              issueInterface.milestone = undefined;
+            });
+
+            it('should return false', (): void => {
+              expect.assertions(1);
+              issue = new Issue(optionsInterface, issueInterface);
+              milestones = new Milestones(optionsInterface, issue);
+
+              const result = milestones.shouldExemptMilestones();
+
+              expect(result).toStrictEqual(false);
+            });
+          });
+
+          describe('when the given issue does have a milestone', (): void => {
+            beforeEach((): void => {
+              issueInterface.milestone = {
+                title: 'dummy-title'
+              };
+            });
+
+            it('should return false', (): void => {
+              expect.assertions(1);
+              issue = new Issue(optionsInterface, issueInterface);
+              milestones = new Milestones(optionsInterface, issue);
+
+              const result = milestones.shouldExemptMilestones();
+
+              expect(result).toStrictEqual(false);
+            });
+          });
+        });
+
+        describe('when the given options are configured to exempt a pull request milestone', (): void => {
+          beforeEach((): void => {
+            optionsInterface.exemptPrMilestones = 'dummy-exempt-pr-milestone';
+          });
+
+          describe('when the given issue does not have a milestone', (): void => {
+            beforeEach((): void => {
+              issueInterface.milestone = undefined;
+            });
+
+            it('should return false', (): void => {
+              expect.assertions(1);
+              issue = new Issue(optionsInterface, issueInterface);
+              milestones = new Milestones(optionsInterface, issue);
+
+              const result = milestones.shouldExemptMilestones();
+
+              expect(result).toStrictEqual(false);
+            });
+          });
+
+          describe('when the given issue does have a milestone different than the exempt pull request milestone', (): void => {
+            beforeEach((): void => {
+              issueInterface.milestone = {
+                title: 'dummy-title'
+              };
+            });
+
+            it('should return false', (): void => {
+              expect.assertions(1);
+              issue = new Issue(optionsInterface, issueInterface);
+              milestones = new Milestones(optionsInterface, issue);
+
+              const result = milestones.shouldExemptMilestones();
+
+              expect(result).toStrictEqual(false);
+            });
+          });
+
+          describe('when the given issue does have a milestone equaling the exempt pull request milestone', (): void => {
+            beforeEach((): void => {
+              issueInterface.milestone = {
+                title: 'dummy-exempt-pr-milestone'
+              };
+            });
+
+            it('should return true', (): void => {
+              expect.assertions(1);
+              issue = new Issue(optionsInterface, issueInterface);
+              milestones = new Milestones(optionsInterface, issue);
+
+              const result = milestones.shouldExemptMilestones();
+
+              expect(result).toStrictEqual(true);
+            });
+          });
+        });
+      });
+
+      describe('when the given options are configured to exempt a milestone', (): void => {
+        beforeEach((): void => {
+          optionsInterface.exemptMilestones = 'dummy-exempt-milestone';
+        });
+
+        describe('when the given options are not configured to exempt a pull request milestone', (): void => {
+          beforeEach((): void => {
+            optionsInterface.exemptPrMilestones = '';
+          });
+
+          describe('when the given issue does not have a milestone', (): void => {
+            beforeEach((): void => {
+              issueInterface.milestone = undefined;
+            });
+
+            it('should return false', (): void => {
+              expect.assertions(1);
+              issue = new Issue(optionsInterface, issueInterface);
+              milestones = new Milestones(optionsInterface, issue);
+
+              const result = milestones.shouldExemptMilestones();
+
+              expect(result).toStrictEqual(false);
+            });
+          });
+
+          describe('when the given issue does have a milestone different than the exempt milestone', (): void => {
+            beforeEach((): void => {
+              issueInterface.milestone = {
+                title: 'dummy-title'
+              };
+            });
+
+            it('should return false', (): void => {
+              expect.assertions(1);
+              issue = new Issue(optionsInterface, issueInterface);
+              milestones = new Milestones(optionsInterface, issue);
+
+              const result = milestones.shouldExemptMilestones();
+
+              expect(result).toStrictEqual(false);
+            });
+          });
+
+          describe('when the given issue does have a milestone equaling the exempt milestone', (): void => {
+            beforeEach((): void => {
+              issueInterface.milestone = {
+                title: 'dummy-exempt-milestone'
+              };
+            });
+
+            it('should return true', (): void => {
+              expect.assertions(1);
+              issue = new Issue(optionsInterface, issueInterface);
+              milestones = new Milestones(optionsInterface, issue);
+
+              const result = milestones.shouldExemptMilestones();
+
+              expect(result).toStrictEqual(true);
+            });
+          });
+        });
+
+        describe('when the given options are configured to exempt a pull request milestone', (): void => {
+          beforeEach((): void => {
+            optionsInterface.exemptPrMilestones = 'dummy-exempt-pr-milestone';
+          });
+
+          describe('when the given issue does not have a milestone', (): void => {
+            beforeEach((): void => {
+              issueInterface.milestone = undefined;
+            });
+
+            it('should return false', (): void => {
+              expect.assertions(1);
+              issue = new Issue(optionsInterface, issueInterface);
+              milestones = new Milestones(optionsInterface, issue);
+
+              const result = milestones.shouldExemptMilestones();
+
+              expect(result).toStrictEqual(false);
+            });
+          });
+
+          describe('when the given issue does have a milestone different than the exempt pull request milestone', (): void => {
+            beforeEach((): void => {
+              issueInterface.milestone = {
+                title: 'dummy-title'
+              };
+            });
+
+            it('should return false', (): void => {
+              expect.assertions(1);
+              issue = new Issue(optionsInterface, issueInterface);
+              milestones = new Milestones(optionsInterface, issue);
+
+              const result = milestones.shouldExemptMilestones();
+
+              expect(result).toStrictEqual(false);
+            });
+          });
+
+          describe('when the given issue does have a milestone equaling the exempt pull request milestone', (): void => {
+            beforeEach((): void => {
+              issueInterface.milestone = {
+                title: 'dummy-exempt-pr-milestone'
+              };
+            });
+
+            it('should return true', (): void => {
+              expect.assertions(1);
+              issue = new Issue(optionsInterface, issueInterface);
+              milestones = new Milestones(optionsInterface, issue);
+
+              const result = milestones.shouldExemptMilestones();
+
+              expect(result).toStrictEqual(true);
+            });
+          });
+
+          describe('when the given issue does have a milestone different than the exempt milestone', (): void => {
+            beforeEach((): void => {
+              issueInterface.milestone = {
+                title: 'dummy-title'
+              };
+            });
+
+            it('should return false', (): void => {
+              expect.assertions(1);
+              issue = new Issue(optionsInterface, issueInterface);
+              milestones = new Milestones(optionsInterface, issue);
+
+              const result = milestones.shouldExemptMilestones();
+
+              expect(result).toStrictEqual(false);
+            });
+          });
+
+          describe('when the given issue does have a milestone equaling the exempt milestone', (): void => {
+            beforeEach((): void => {
+              issueInterface.milestone = {
+                title: 'dummy-exempt-milestone'
+              };
+            });
+
+            it('should return false', (): void => {
+              expect.assertions(1);
+              issue = new Issue(optionsInterface, issueInterface);
+              milestones = new Milestones(optionsInterface, issue);
+
+              const result = milestones.shouldExemptMilestones();
+
+              expect(result).toStrictEqual(false);
+            });
+          });
+        });
+      });
+    });
+  });
+});
diff --git a/src/classes/milestones.ts b/src/classes/milestones.ts
new file mode 100644
index 0000000..99b2cce
--- /dev/null
+++ b/src/classes/milestones.ts
@@ -0,0 +1,59 @@
+import deburr from 'lodash.deburr';
+import {wordsToList} from '../functions/words-to-list';
+import {IssueProcessorOptions} from '../IssueProcessor';
+import {Issue} from './issue';
+
+type CleanMilestone = string;
+
+export class Milestones {
+  private static _cleanMilestone(label: Readonly<string>): CleanMilestone {
+    return deburr(label.toLowerCase());
+  }
+
+  private readonly _options: IssueProcessorOptions;
+  private readonly _issue: Issue;
+
+  constructor(options: Readonly<IssueProcessorOptions>, issue: Issue) {
+    this._options = options;
+    this._issue = issue;
+  }
+
+  shouldExemptMilestones(): boolean {
+    const exemptMilestones: string[] = this._getExemptMilestones();
+
+    return exemptMilestones.some((exemptMilestone: Readonly<string>): boolean =>
+      this._hasMilestone(exemptMilestone)
+    );
+  }
+
+  private _getExemptMilestones(): string[] {
+    return wordsToList(
+      this._issue.isPullRequest
+        ? this._getExemptPullRequestMilestones()
+        : this._getExemptIssueMilestones()
+    );
+  }
+
+  private _getExemptIssueMilestones(): string {
+    return this._options.exemptIssueMilestones !== ''
+      ? this._options.exemptIssueMilestones
+      : this._options.exemptMilestones;
+  }
+
+  private _getExemptPullRequestMilestones(): string {
+    return this._options.exemptPrMilestones !== ''
+      ? this._options.exemptPrMilestones
+      : this._options.exemptMilestones;
+  }
+
+  private _hasMilestone(milestone: Readonly<string>): boolean {
+    if (!this._issue.milestone) {
+      return false;
+    }
+
+    return (
+      Milestones._cleanMilestone(milestone) ===
+      Milestones._cleanMilestone(this._issue.milestone.title)
+    );
+  }
+}
diff --git a/src/functions/is-labeled.spec.ts b/src/functions/is-labeled.spec.ts
index fbabb3d..249fcd0 100644
--- a/src/functions/is-labeled.spec.ts
+++ b/src/functions/is-labeled.spec.ts
@@ -1,4 +1,4 @@
-import {Issue} from '../IssueProcessor';
+import {Issue} from '../classes/issue';
 import {isLabeled} from './is-labeled';
 
 describe('isLabeled()', (): void => {
diff --git a/src/functions/is-labeled.ts b/src/functions/is-labeled.ts
index 7511175..791cd0a 100644
--- a/src/functions/is-labeled.ts
+++ b/src/functions/is-labeled.ts
@@ -1,15 +1,16 @@
 import deburr from 'lodash.deburr';
-import {Issue, Label} from '../IssueProcessor';
+import {Issue} from '../classes/issue';
+import {Label} from '../IssueProcessor';
 import {CleanLabel} from '../types/clean-label';
 
 /**
  * @description
- * Check if the label is listed as a label of the issue
+ * Check if the given label is listed as a label of the given issue
  *
  * @param {Readonly<Issue>} issue A GitHub issue containing some labels
  * @param {Readonly<string>} label The label to check the presence with
  *
- * @return {boolean} Return true when the given label is also in the issue labels
+ * @return {boolean} Return true when the given label is also in the given issue labels
  */
 export function isLabeled(
   issue: Readonly<Issue>,
diff --git a/src/functions/is-pull-request.spec.ts b/src/functions/is-pull-request.spec.ts
index cfa20d6..b0851f1 100644
--- a/src/functions/is-pull-request.spec.ts
+++ b/src/functions/is-pull-request.spec.ts
@@ -1,4 +1,4 @@
-import {Issue} from '../IssueProcessor';
+import {Issue} from '../classes/issue';
 import {isPullRequest} from './is-pull-request';
 
 describe('isPullRequest()', (): void => {
diff --git a/src/functions/is-pull-request.ts b/src/functions/is-pull-request.ts
index 4c3ae99..1ba0017 100644
--- a/src/functions/is-pull-request.ts
+++ b/src/functions/is-pull-request.ts
@@ -1,4 +1,4 @@
-import {Issue} from '../IssueProcessor';
+import {Issue} from '../classes/issue';
 
 export function isPullRequest(issue: Readonly<Issue>): boolean {
   return !!issue.pull_request;
diff --git a/src/functions/labels-to-list.spec.ts b/src/functions/labels-to-list.spec.ts
deleted file mode 100644
index f70c281..0000000
--- a/src/functions/labels-to-list.spec.ts
+++ /dev/null
@@ -1,141 +0,0 @@
-import {labelsToList} from './labels-to-list';
-
-describe('labelsToList()', (): void => {
-  let labels: string;
-
-  describe('when the given labels is empty', (): void => {
-    beforeEach((): void => {
-      labels = '';
-    });
-
-    it('should return an empty list of labels', (): void => {
-      expect.assertions(1);
-
-      const result = labelsToList(labels);
-
-      expect(result).toStrictEqual([]);
-    });
-  });
-
-  describe('when the given labels is a simple label', (): void => {
-    beforeEach((): void => {
-      labels = 'label';
-    });
-
-    it('should return a list of one label', (): void => {
-      expect.assertions(1);
-
-      const result = labelsToList(labels);
-
-      expect(result).toStrictEqual(['label']);
-    });
-  });
-
-  describe('when the given labels is a label with extra spaces before and after', (): void => {
-    beforeEach((): void => {
-      labels = '   label   ';
-    });
-
-    it('should return a list of one label and remove all spaces before and after', (): void => {
-      expect.assertions(1);
-
-      const result = labelsToList(labels);
-
-      expect(result).toStrictEqual(['label']);
-    });
-  });
-
-  describe('when the given labels is a kebab case label', (): void => {
-    beforeEach((): void => {
-      labels = 'kebab-case-label';
-    });
-
-    it('should return a list of one label', (): void => {
-      expect.assertions(1);
-
-      const result = labelsToList(labels);
-
-      expect(result).toStrictEqual(['kebab-case-label']);
-    });
-  });
-
-  describe('when the given labels is two kebab case labels separated with a comma', (): void => {
-    beforeEach((): void => {
-      labels = 'kebab-case-label-1,kebab-case-label-2';
-    });
-
-    it('should return a list of two labels', (): void => {
-      expect.assertions(1);
-
-      const result = labelsToList(labels);
-
-      expect(result).toStrictEqual([
-        'kebab-case-label-1',
-        'kebab-case-label-2'
-      ]);
-    });
-  });
-
-  describe('when the given labels is a multiple word label', (): void => {
-    beforeEach((): void => {
-      labels = 'label like a sentence';
-    });
-
-    it('should return a list of one label', (): void => {
-      expect.assertions(1);
-
-      const result = labelsToList(labels);
-
-      expect(result).toStrictEqual(['label like a sentence']);
-    });
-  });
-
-  describe('when the given labels is two multiple word labels separated with a comma', (): void => {
-    beforeEach((): void => {
-      labels = 'label like a sentence, another label like a sentence';
-    });
-
-    it('should return a list of two labels', (): void => {
-      expect.assertions(1);
-
-      const result = labelsToList(labels);
-
-      expect(result).toStrictEqual([
-        'label like a sentence',
-        'another label like a sentence'
-      ]);
-    });
-  });
-
-  describe('when the given labels is a multiple word label with %20 spaces', (): void => {
-    beforeEach((): void => {
-      labels = 'label%20like%20a%20sentence';
-    });
-
-    it('should return a list of one label', (): void => {
-      expect.assertions(1);
-
-      const result = labelsToList(labels);
-
-      expect(result).toStrictEqual(['label%20like%20a%20sentence']);
-    });
-  });
-
-  describe('when the given labels is two multiple word labels with %20 spaces separated with a comma', (): void => {
-    beforeEach((): void => {
-      labels =
-        'label%20like%20a%20sentence,another%20label%20like%20a%20sentence';
-    });
-
-    it('should return a list of two labels', (): void => {
-      expect.assertions(1);
-
-      const result = labelsToList(labels);
-
-      expect(result).toStrictEqual([
-        'label%20like%20a%20sentence',
-        'another%20label%20like%20a%20sentence'
-      ]);
-    });
-  });
-});
diff --git a/src/functions/labels-to-list.ts b/src/functions/labels-to-list.ts
deleted file mode 100644
index 59ac5de..0000000
--- a/src/functions/labels-to-list.ts
+++ /dev/null
@@ -1,23 +0,0 @@
-/**
- * @description
- * Transform a string of comma separated labels
- * to an array of labels
- *
- * @example
- * labelsToList('label') => ['label']
- * labelsToList('label,label') => ['label', 'label']
- * labelsToList('kebab-label') => ['kebab-label']
- * labelsToList('kebab%20label') => ['kebab%20label']
- * labelsToList('label with words') => ['label with words']
- *
- * @param {Readonly<string>} labels A string of comma separated labels
- *
- * @return {string[]} A list of labels
- */
-export function labelsToList(labels: Readonly<string>): string[] {
-  if (!labels.length) {
-    return [];
-  }
-
-  return labels.split(',').map(l => l.trim());
-}
diff --git a/src/functions/words-to-list.spec.ts b/src/functions/words-to-list.spec.ts
new file mode 100644
index 0000000..06f7576
--- /dev/null
+++ b/src/functions/words-to-list.spec.ts
@@ -0,0 +1,137 @@
+import {wordsToList} from './words-to-list';
+
+describe('wordsToList()', (): void => {
+  let words: string;
+
+  describe('when the given words is empty', (): void => {
+    beforeEach((): void => {
+      words = '';
+    });
+
+    it('should return an empty list of words', (): void => {
+      expect.assertions(1);
+
+      const result = wordsToList(words);
+
+      expect(result).toStrictEqual([]);
+    });
+  });
+
+  describe('when the given words is a simple word', (): void => {
+    beforeEach((): void => {
+      words = 'word';
+    });
+
+    it('should return a list of one word', (): void => {
+      expect.assertions(1);
+
+      const result = wordsToList(words);
+
+      expect(result).toStrictEqual(['word']);
+    });
+  });
+
+  describe('when the given words is a word with extra spaces before and after', (): void => {
+    beforeEach((): void => {
+      words = '   word   ';
+    });
+
+    it('should return a list of one word and remove all spaces before and after', (): void => {
+      expect.assertions(1);
+
+      const result = wordsToList(words);
+
+      expect(result).toStrictEqual(['word']);
+    });
+  });
+
+  describe('when the given words is a kebab case word', (): void => {
+    beforeEach((): void => {
+      words = 'kebab-case-word';
+    });
+
+    it('should return a list of one word', (): void => {
+      expect.assertions(1);
+
+      const result = wordsToList(words);
+
+      expect(result).toStrictEqual(['kebab-case-word']);
+    });
+  });
+
+  describe('when the given words is two kebab case words separated with a comma', (): void => {
+    beforeEach((): void => {
+      words = 'kebab-case-word-1,kebab-case-word-2';
+    });
+
+    it('should return a list of two words', (): void => {
+      expect.assertions(1);
+
+      const result = wordsToList(words);
+
+      expect(result).toStrictEqual(['kebab-case-word-1', 'kebab-case-word-2']);
+    });
+  });
+
+  describe('when the given words is a multiple word word', (): void => {
+    beforeEach((): void => {
+      words = 'word like a sentence';
+    });
+
+    it('should return a list of one word', (): void => {
+      expect.assertions(1);
+
+      const result = wordsToList(words);
+
+      expect(result).toStrictEqual(['word like a sentence']);
+    });
+  });
+
+  describe('when the given words is two multiple word words separated with a comma', (): void => {
+    beforeEach((): void => {
+      words = 'word like a sentence, another word like a sentence';
+    });
+
+    it('should return a list of two words', (): void => {
+      expect.assertions(1);
+
+      const result = wordsToList(words);
+
+      expect(result).toStrictEqual([
+        'word like a sentence',
+        'another word like a sentence'
+      ]);
+    });
+  });
+
+  describe('when the given words is a multiple word word with %20 spaces', (): void => {
+    beforeEach((): void => {
+      words = 'word%20like%20a%20sentence';
+    });
+
+    it('should return a list of one word', (): void => {
+      expect.assertions(1);
+
+      const result = wordsToList(words);
+
+      expect(result).toStrictEqual(['word%20like%20a%20sentence']);
+    });
+  });
+
+  describe('when the given words is two multiple word words with %20 spaces separated with a comma', (): void => {
+    beforeEach((): void => {
+      words = 'word%20like%20a%20sentence,another%20word%20like%20a%20sentence';
+    });
+
+    it('should return a list of two words', (): void => {
+      expect.assertions(1);
+
+      const result = wordsToList(words);
+
+      expect(result).toStrictEqual([
+        'word%20like%20a%20sentence',
+        'another%20word%20like%20a%20sentence'
+      ]);
+    });
+  });
+});
diff --git a/src/functions/words-to-list.ts b/src/functions/words-to-list.ts
new file mode 100644
index 0000000..8eb3701
--- /dev/null
+++ b/src/functions/words-to-list.ts
@@ -0,0 +1,23 @@
+/**
+ * @description
+ * Transform a string of comma separated words
+ * to an array of words
+ *
+ * @example
+ * wordsToList('label') => ['label']
+ * wordsToList('label,label') => ['label', 'label']
+ * wordsToList('kebab-label') => ['kebab-label']
+ * wordsToList('kebab%20label') => ['kebab%20label']
+ * wordsToList('label with words') => ['label with words']
+ *
+ * @param {Readonly<string>} words A string of comma separated words
+ *
+ * @return {string[]} A list of words
+ */
+export function wordsToList(words: Readonly<string>): string[] {
+  if (!words.length) {
+    return [];
+  }
+
+  return words.split(',').map((word: Readonly<string>): string => word.trim());
+}
diff --git a/src/interfaces/issue.ts b/src/interfaces/issue.ts
new file mode 100644
index 0000000..9ee7ce8
--- /dev/null
+++ b/src/interfaces/issue.ts
@@ -0,0 +1,15 @@
+import {Label} from '../IssueProcessor';
+import {IsoDateString} from '../types/iso-date-string';
+import {IMilestone} from './milestone';
+
+export interface IIssue {
+  title: string;
+  number: number;
+  created_at: IsoDateString;
+  updated_at: IsoDateString;
+  labels: Label[];
+  pull_request: Object | null | undefined;
+  state: string;
+  locked: boolean;
+  milestone: IMilestone | undefined;
+}
diff --git a/src/interfaces/milestone.ts b/src/interfaces/milestone.ts
new file mode 100644
index 0000000..d0ff242
--- /dev/null
+++ b/src/interfaces/milestone.ts
@@ -0,0 +1,3 @@
+export interface IMilestone {
+  title: string;
+}
diff --git a/src/main.ts b/src/main.ts
index 2e44657..31a492e 100644
--- a/src/main.ts
+++ b/src/main.ts
@@ -52,16 +52,15 @@ function getAndValidateArgs(): IssueProcessorOptions {
     startDate:
       core.getInput('start-date') !== ''
         ? core.getInput('start-date')
-        : undefined
+        : undefined,
+    exemptMilestones: core.getInput('exempt-milestones'),
+    exemptIssueMilestones: core.getInput('exempt-issue-milestones'),
+    exemptPrMilestones: core.getInput('exempt-pr-milestones')
   };
 
   for (const numberInput of [
     'days-before-stale',
-    'days-before-issue-stale',
-    'days-before-pr-stale',
     'days-before-close',
-    'days-before-issue-close',
-    'days-before-pr-close',
     'operations-per-run'
   ]) {
     if (isNaN(parseInt(core.getInput(numberInput)))) {


Test patch:
diff --git a/__tests__/main.test.ts b/__tests__/main.test.ts
index 55bdb16..aad95e5 100644
--- a/__tests__/main.test.ts
+++ b/__tests__/main.test.ts
@@ -1,12 +1,11 @@
 import * as github from '@actions/github';
-import {
-  Issue,
-  IssueProcessor,
-  IssueProcessorOptions
-} from '../src/IssueProcessor';
+import {Issue} from '../src/classes/issue';
+
+import {IssueProcessor, IssueProcessorOptions} from '../src/IssueProcessor';
 import {IsoDateString} from '../src/types/iso-date-string';
 
 function generateIssue(
+  options: IssueProcessorOptions,
   id: number,
   title: string,
   updatedAt: IsoDateString,
@@ -14,9 +13,10 @@ function generateIssue(
   isPullRequest: boolean = false,
   labels: string[] = [],
   isClosed: boolean = false,
-  isLocked: boolean = false
+  isLocked: boolean = false,
+  milestone = ''
 ): Issue {
-  return {
+  return new Issue(options, {
     number: id,
     labels: labels.map(l => {
       return {name: l};
@@ -26,8 +26,11 @@ function generateIssue(
     updated_at: updatedAt,
     pull_request: isPullRequest ? {} : null,
     state: isClosed ? 'closed' : 'open',
-    locked: isLocked
-  };
+    locked: isLocked,
+    milestone: {
+      title: milestone
+    }
+  });
 }
 
 const DefaultProcessorOptions: IssueProcessorOptions = Object.freeze({
@@ -56,7 +59,10 @@ const DefaultProcessorOptions: IssueProcessorOptions = Object.freeze({
   skipStaleIssueMessage: false,
   skipStalePrMessage: false,
   deleteBranch: false,
-  startDate: ''
+  startDate: '',
+  exemptMilestones: '',
+  exemptIssueMilestones: '',
+  exemptPrMilestones: ''
 });
 
 test('empty issue list results in 1 operation', async () => {
@@ -76,15 +82,13 @@ test('empty issue list results in 1 operation', async () => {
 });
 
 test('processing an issue with no label will make it stale and close it, if it is old enough only if days-before-close is set to 0', async () => {
-  const TestIssueList: Issue[] = [
-    generateIssue(1, 'An issue with no label', '2020-01-01T17:00:00Z')
-  ];
-
   const opts: IssueProcessorOptions = {
     ...DefaultProcessorOptions,
     daysBeforeClose: 0
   };
-
+  const TestIssueList: Issue[] = [
+    generateIssue(opts, 1, 'An issue with no label', '2020-01-01T17:00:00Z')
+  ];
   const processor = new IssueProcessor(
     opts,
     async () => 'abot',
@@ -102,20 +106,21 @@ test('processing an issue with no label will make it stale and close it, if it i
 
 test('processing an issue with no label and a start date as ECMAScript epoch in seconds being before the issue creation date will not make it stale nor close it when it is old enough and days-before-close is set to 0', async () => {
   expect.assertions(2);
+  const january2000 = 946681200000;
+  const opts: IssueProcessorOptions = {
+    ...DefaultProcessorOptions,
+    daysBeforeClose: 0,
+    startDate: january2000.toString()
+  };
   const TestIssueList: Issue[] = [
     generateIssue(
+      opts,
       1,
       'An issue with no label',
       '2020-01-01T17:00:00Z',
       '2020-01-01T17:00:00Z'
     )
   ];
-  const january2000 = 946681200000;
-  const opts: IssueProcessorOptions = {
-    ...DefaultProcessorOptions,
-    daysBeforeClose: 0,
-    startDate: january2000.toString()
-  };
   const processor = new IssueProcessor(
     opts,
     async () => 'abot',
@@ -133,20 +138,21 @@ test('processing an issue with no label and a start date as ECMAScript epoch in
 
 test('processing an issue with no label and a start date as ECMAScript epoch in seconds being after the issue creation date will not make it stale nor close it when it is old enough and days-before-close is set to 0', async () => {
   expect.assertions(2);
+  const january2021 = 1609455600000;
+  const opts: IssueProcessorOptions = {
+    ...DefaultProcessorOptions,
+    daysBeforeClose: 0,
+    startDate: january2021.toString()
+  };
   const TestIssueList: Issue[] = [
     generateIssue(
+      opts,
       1,
       'An issue with no label',
       '2020-01-01T17:00:00Z',
       '2020-01-01T17:00:00Z'
     )
   ];
-  const january2021 = 1609455600000;
-  const opts: IssueProcessorOptions = {
-    ...DefaultProcessorOptions,
-    daysBeforeClose: 0,
-    startDate: january2021.toString()
-  };
   const processor = new IssueProcessor(
     opts,
     async () => 'abot',
@@ -164,20 +170,21 @@ test('processing an issue with no label and a start date as ECMAScript epoch in
 
 test('processing an issue with no label and a start date as ECMAScript epoch in milliseconds being before the issue creation date will not make it stale nor close it when it is old enough and days-before-close is set to 0', async () => {
   expect.assertions(2);
+  const january2000 = 946681200000000;
+  const opts: IssueProcessorOptions = {
+    ...DefaultProcessorOptions,
+    daysBeforeClose: 0,
+    startDate: january2000.toString()
+  };
   const TestIssueList: Issue[] = [
     generateIssue(
+      opts,
       1,
       'An issue with no label',
       '2020-01-01T17:00:00Z',
       '2020-01-01T17:00:00Z'
     )
   ];
-  const january2000 = 946681200000000;
-  const opts: IssueProcessorOptions = {
-    ...DefaultProcessorOptions,
-    daysBeforeClose: 0,
-    startDate: january2000.toString()
-  };
   const processor = new IssueProcessor(
     opts,
     async () => 'abot',
@@ -195,20 +202,21 @@ test('processing an issue with no label and a start date as ECMAScript epoch in
 
 test('processing an issue with no label and a start date as ECMAScript epoch in milliseconds being after the issue creation date will not make it stale nor close it when it is old enough and days-before-close is set to 0', async () => {
   expect.assertions(2);
+  const january2021 = 1609455600000;
+  const opts: IssueProcessorOptions = {
+    ...DefaultProcessorOptions,
+    daysBeforeClose: 0,
+    startDate: january2021.toString()
+  };
   const TestIssueList: Issue[] = [
     generateIssue(
+      opts,
       1,
       'An issue with no label',
       '2020-01-01T17:00:00Z',
       '2020-01-01T17:00:00Z'
     )
   ];
-  const january2021 = 1609455600000;
-  const opts: IssueProcessorOptions = {
-    ...DefaultProcessorOptions,
-    daysBeforeClose: 0,
-    startDate: january2021.toString()
-  };
   const processor = new IssueProcessor(
     opts,
     async () => 'abot',
@@ -226,20 +234,21 @@ test('processing an issue with no label and a start date as ECMAScript epoch in
 
 test('processing an issue with no label and a start date as ISO 8601 being before the issue creation date will make it stale and close it when it is old enough and days-before-close is set to 0', async () => {
   expect.assertions(2);
+  const january2000 = '2000-01-01T00:00:00Z';
+  const opts: IssueProcessorOptions = {
+    ...DefaultProcessorOptions,
+    daysBeforeClose: 0,
+    startDate: january2000.toString()
+  };
   const TestIssueList: Issue[] = [
     generateIssue(
+      opts,
       1,
       'An issue with no label',
       '2020-01-01T17:00:00Z',
       '2020-01-01T17:00:00Z'
     )
   ];
-  const january2000 = '2000-01-01T00:00:00Z';
-  const opts: IssueProcessorOptions = {
-    ...DefaultProcessorOptions,
-    daysBeforeClose: 0,
-    startDate: january2000.toString()
-  };
   const processor = new IssueProcessor(
     opts,
     async () => 'abot',
@@ -257,20 +266,21 @@ test('processing an issue with no label and a start date as ISO 8601 being befor
 
 test('processing an issue with no label and a start date as ISO 8601 being after the issue creation date will not make it stale nor close it when it is old enough and days-before-close is set to 0', async () => {
   expect.assertions(2);
+  const january2021 = '2021-01-01T00:00:00Z';
+  const opts: IssueProcessorOptions = {
+    ...DefaultProcessorOptions,
+    daysBeforeClose: 0,
+    startDate: january2021.toString()
+  };
   const TestIssueList: Issue[] = [
     generateIssue(
+      opts,
       1,
       'An issue with no label',
       '2020-01-01T17:00:00Z',
       '2020-01-01T17:00:00Z'
     )
   ];
-  const january2021 = '2021-01-01T00:00:00Z';
-  const opts: IssueProcessorOptions = {
-    ...DefaultProcessorOptions,
-    daysBeforeClose: 0,
-    startDate: january2021.toString()
-  };
   const processor = new IssueProcessor(
     opts,
     async () => 'abot',
@@ -288,20 +298,21 @@ test('processing an issue with no label and a start date as ISO 8601 being after
 
 test('processing an issue with no label and a start date as RFC 2822 being before the issue creation date will make it stale and close it when it is old enough and days-before-close is set to 0', async () => {
   expect.assertions(2);
+  const january2000 = 'January 1, 2000 00:00:00';
+  const opts: IssueProcessorOptions = {
+    ...DefaultProcessorOptions,
+    daysBeforeClose: 0,
+    startDate: january2000.toString()
+  };
   const TestIssueList: Issue[] = [
     generateIssue(
+      opts,
       1,
       'An issue with no label',
       '2020-01-01T17:00:00Z',
       '2020-01-01T17:00:00Z'
     )
   ];
-  const january2000 = 'January 1, 2000 00:00:00';
-  const opts: IssueProcessorOptions = {
-    ...DefaultProcessorOptions,
-    daysBeforeClose: 0,
-    startDate: january2000.toString()
-  };
   const processor = new IssueProcessor(
     opts,
     async () => 'abot',
@@ -319,20 +330,21 @@ test('processing an issue with no label and a start date as RFC 2822 being befor
 
 test('processing an issue with no label and a start date as RFC 2822 being after the issue creation date will not make it stale nor close it when it is old enough and days-before-close is set to 0', async () => {
   expect.assertions(2);
+  const january2021 = 'January 1, 2021 00:00:00';
+  const opts: IssueProcessorOptions = {
+    ...DefaultProcessorOptions,
+    daysBeforeClose: 0,
+    startDate: january2021.toString()
+  };
   const TestIssueList: Issue[] = [
     generateIssue(
+      opts,
       1,
       'An issue with no label',
       '2020-01-01T17:00:00Z',
       '2020-01-01T17:00:00Z'
     )
   ];
-  const january2021 = 'January 1, 2021 00:00:00';
-  const opts: IssueProcessorOptions = {
-    ...DefaultProcessorOptions,
-    daysBeforeClose: 0,
-    startDate: january2021.toString()
-  };
   const processor = new IssueProcessor(
     opts,
     async () => 'abot',
@@ -349,16 +361,14 @@ test('processing an issue with no label and a start date as RFC 2822 being after
 });
 
 test('processing an issue with no label will make it stale and close it, if it is old enough only if days-before-close is set to > 0 and days-before-issue-close is set to 0', async () => {
-  const TestIssueList: Issue[] = [
-    generateIssue(1, 'An issue with no label', '2020-01-01T17:00:00Z')
-  ];
-
   const opts: IssueProcessorOptions = {
     ...DefaultProcessorOptions,
     daysBeforeClose: 1,
     daysBeforeIssueClose: 0
   };
-
+  const TestIssueList: Issue[] = [
+    generateIssue(opts, 1, 'An issue with no label', '2020-01-01T17:00:00Z')
+  ];
   const processor = new IssueProcessor(
     opts,
     async () => 'abot',
@@ -376,16 +386,14 @@ test('processing an issue with no label will make it stale and close it, if it i
 });
 
 test('processing an issue with no label will make it stale and not close it, if it is old enough only if days-before-close is set to > 0 and days-before-issue-close is set to > 0', async () => {
-  const TestIssueList: Issue[] = [
-    generateIssue(1, 'An issue with no label', '2020-01-01T17:00:00Z')
-  ];
-
   const opts: IssueProcessorOptions = {
     ...DefaultProcessorOptions,
     daysBeforeClose: 1,
     daysBeforeIssueClose: 1
   };
-
+  const TestIssueList: Issue[] = [
+    generateIssue(opts, 1, 'An issue with no label', '2020-01-01T17:00:00Z')
+  ];
   const processor = new IssueProcessor(
     opts,
     async () => 'abot',
@@ -402,15 +410,13 @@ test('processing an issue with no label will make it stale and not close it, if
 });
 
 test('processing an issue with no label will make it stale and not close it if days-before-close is set to > 0', async () => {
-  const TestIssueList: Issue[] = [
-    generateIssue(1, 'An issue with no label', '2020-01-01T17:00:00Z')
-  ];
-
   const opts: IssueProcessorOptions = {
     ...DefaultProcessorOptions,
     daysBeforeClose: 15
   };
-
+  const TestIssueList: Issue[] = [
+    generateIssue(opts, 1, 'An issue with no label', '2020-01-01T17:00:00Z')
+  ];
   const processor = new IssueProcessor(
     opts,
     async () => 'abot',
@@ -427,16 +433,14 @@ test('processing an issue with no label will make it stale and not close it if d
 });
 
 test('processing an issue with no label will make it stale and not close it if days-before-close is set to -1 and days-before-issue-close is set to > 0', async () => {
-  const TestIssueList: Issue[] = [
-    generateIssue(1, 'An issue with no label', '2020-01-01T17:00:00Z')
-  ];
-
   const opts: IssueProcessorOptions = {
     ...DefaultProcessorOptions,
     daysBeforeClose: -1,
     daysBeforeIssueClose: 15
   };
-
+  const TestIssueList: Issue[] = [
+    generateIssue(opts, 1, 'An issue with no label', '2020-01-01T17:00:00Z')
+  ];
   const processor = new IssueProcessor(
     opts,
     async () => 'abot',
@@ -453,16 +457,14 @@ test('processing an issue with no label will make it stale and not close it if d
 });
 
 test('processing an issue with no label will not make it stale if days-before-stale is set to -1', async () => {
-  const TestIssueList: Issue[] = [
-    generateIssue(1, 'An issue with no label', '2020-01-01T17:00:00Z')
-  ];
-
   const opts: IssueProcessorOptions = {
     ...DefaultProcessorOptions,
     staleIssueMessage: '',
     daysBeforeStale: -1
   };
-
+  const TestIssueList: Issue[] = [
+    generateIssue(opts, 1, 'An issue with no label', '2020-01-01T17:00:00Z')
+  ];
   const processor = new IssueProcessor(
     opts,
     async () => 'abot',
@@ -479,17 +481,15 @@ test('processing an issue with no label will not make it stale if days-before-st
 });
 
 test('processing an issue with no label will not make it stale if days-before-stale and days-before-issue-stale are set to -1', async () => {
-  const TestIssueList: Issue[] = [
-    generateIssue(1, 'An issue with no label', '2020-01-01T17:00:00Z')
-  ];
-
   const opts: IssueProcessorOptions = {
     ...DefaultProcessorOptions,
     staleIssueMessage: '',
     daysBeforeStale: -1,
     daysBeforeIssueStale: -1
   };
-
+  const TestIssueList: Issue[] = [
+    generateIssue(opts, 1, 'An issue with no label', '2020-01-01T17:00:00Z')
+  ];
   const processor = new IssueProcessor(
     opts,
     async () => 'abot',
@@ -510,11 +510,14 @@ test('processing an issue with no label will make it stale but not close it', as
   // stale but not close-able, based on default settings
   let issueDate = new Date();
   issueDate.setDate(issueDate.getDate() - 2);
-
   const TestIssueList: Issue[] = [
-    generateIssue(1, 'An issue with no label', issueDate.toDateString())
+    generateIssue(
+      DefaultProcessorOptions,
+      1,
+      'An issue with no label',
+      issueDate.toDateString()
+    )
   ];
-
   const processor = new IssueProcessor(
     DefaultProcessorOptions,
     async () => 'abot',
@@ -531,8 +534,13 @@ test('processing an issue with no label will make it stale but not close it', as
 });
 
 test('processing a stale issue will close it', async () => {
+  const opts: IssueProcessorOptions = {
+    ...DefaultProcessorOptions,
+    daysBeforeClose: 30
+  };
   const TestIssueList: Issue[] = [
     generateIssue(
+      opts,
       1,
       'A stale issue that should be closed',
       '2020-01-01T17:00:00Z',
@@ -541,12 +549,6 @@ test('processing a stale issue will close it', async () => {
       ['Stale']
     )
   ];
-
-  const opts: IssueProcessorOptions = {
-    ...DefaultProcessorOptions,
-    daysBeforeClose: 30
-  };
-
   const processor = new IssueProcessor(
     opts,
     async () => 'abot',
@@ -563,8 +565,13 @@ test('processing a stale issue will close it', async () => {
 });
 
 test('processing a stale issue containing a space in the label will close it', async () => {
+  const opts: IssueProcessorOptions = {
+    ...DefaultProcessorOptions,
+    staleIssueLabel: 'state: stale'
+  };
   const TestIssueList: Issue[] = [
     generateIssue(
+      opts,
       1,
       'A stale issue that should be closed',
       '2020-01-01T17:00:00Z',
@@ -573,12 +580,6 @@ test('processing a stale issue containing a space in the label will close it', a
       ['state: stale']
     )
   ];
-
-  const opts: IssueProcessorOptions = {
-    ...DefaultProcessorOptions,
-    staleIssueLabel: 'state: stale'
-  };
-
   const processor = new IssueProcessor(
     opts,
     async () => 'abot',
@@ -595,8 +596,13 @@ test('processing a stale issue containing a space in the label will close it', a
 });
 
 test('processing a stale issue containing a slash in the label will close it', async () => {
+  const opts: IssueProcessorOptions = {
+    ...DefaultProcessorOptions,
+    staleIssueLabel: 'lifecycle/stale'
+  };
   const TestIssueList: Issue[] = [
     generateIssue(
+      opts,
       1,
       'A stale issue that should be closed',
       '2020-01-01T17:00:00Z',
@@ -605,12 +611,6 @@ test('processing a stale issue containing a slash in the label will close it', a
       ['lifecycle/stale']
     )
   ];
-
-  const opts: IssueProcessorOptions = {
-    ...DefaultProcessorOptions,
-    staleIssueLabel: 'lifecycle/stale'
-  };
-
   const processor = new IssueProcessor(
     opts,
     async () => 'abot',
@@ -627,8 +627,14 @@ test('processing a stale issue containing a slash in the label will close it', a
 });
 
 test('processing a stale issue will close it when days-before-issue-stale override days-before-stale', async () => {
+  const opts: IssueProcessorOptions = {
+    ...DefaultProcessorOptions,
+    daysBeforeClose: 30,
+    daysBeforeIssueStale: 30
+  };
   const TestIssueList: Issue[] = [
     generateIssue(
+      opts,
       1,
       'A stale issue that should be closed',
       '2020-01-01T17:00:00Z',
@@ -637,13 +643,6 @@ test('processing a stale issue will close it when days-before-issue-stale overri
       ['Stale']
     )
   ];
-
-  const opts: IssueProcessorOptions = {
-    ...DefaultProcessorOptions,
-    daysBeforeClose: 30,
-    daysBeforeIssueStale: 30
-  };
-
   const processor = new IssueProcessor(
     opts,
     async () => 'abot',
@@ -660,8 +659,13 @@ test('processing a stale issue will close it when days-before-issue-stale overri
 });
 
 test('processing a stale PR will close it', async () => {
+  const opts: IssueProcessorOptions = {
+    ...DefaultProcessorOptions,
+    daysBeforeClose: 30
+  };
   const TestIssueList: Issue[] = [
     generateIssue(
+      opts,
       1,
       'A stale PR that should be closed',
       '2020-01-01T17:00:00Z',
@@ -670,12 +674,6 @@ test('processing a stale PR will close it', async () => {
       ['Stale']
     )
   ];
-
-  const opts: IssueProcessorOptions = {
-    ...DefaultProcessorOptions,
-    daysBeforeClose: 30
-  };
-
   const processor = new IssueProcessor(
     opts,
     async () => 'abot',
@@ -692,8 +690,14 @@ test('processing a stale PR will close it', async () => {
 });
 
 test('processing a stale PR will close it when days-before-pr-stale override days-before-stale', async () => {
+  const opts: IssueProcessorOptions = {
+    ...DefaultProcessorOptions,
+    daysBeforeClose: 30,
+    daysBeforePrClose: 30
+  };
   const TestIssueList: Issue[] = [
     generateIssue(
+      opts,
       1,
       'A stale PR that should be closed',
       '2020-01-01T17:00:00Z',
@@ -702,13 +706,6 @@ test('processing a stale PR will close it when days-before-pr-stale override day
       ['Stale']
     )
   ];
-
-  const opts: IssueProcessorOptions = {
-    ...DefaultProcessorOptions,
-    daysBeforeClose: 30,
-    daysBeforePrClose: 30
-  };
-
   const processor = new IssueProcessor(
     opts,
     async () => 'abot',
@@ -725,8 +722,14 @@ test('processing a stale PR will close it when days-before-pr-stale override day
 });
 
 test('processing a stale issue will close it even if configured not to mark as stale', async () => {
+  const opts = {
+    ...DefaultProcessorOptions,
+    daysBeforeStale: -1,
+    staleIssueMessage: ''
+  };
   const TestIssueList: Issue[] = [
     generateIssue(
+      opts,
       1,
       'An issue with no label',
       '2020-01-01T17:00:00Z',
@@ -735,13 +738,6 @@ test('processing a stale issue will close it even if configured not to mark as s
       ['Stale']
     )
   ];
-
-  const opts = {
-    ...DefaultProcessorOptions,
-    daysBeforeStale: -1,
-    staleIssueMessage: ''
-  };
-
   const processor = new IssueProcessor(
     opts,
     async () => 'abot',
@@ -758,8 +754,15 @@ test('processing a stale issue will close it even if configured not to mark as s
 });
 
 test('processing a stale issue will close it even if configured not to mark as stale when days-before-issue-stale override days-before-stale', async () => {
+  const opts = {
+    ...DefaultProcessorOptions,
+    daysBeforeStale: 0,
+    daysBeforeIssueStale: -1,
+    staleIssueMessage: ''
+  };
   const TestIssueList: Issue[] = [
     generateIssue(
+      opts,
       1,
       'An issue with no label',
       '2020-01-01T17:00:00Z',
@@ -768,14 +771,6 @@ test('processing a stale issue will close it even if configured not to mark as s
       ['Stale']
     )
   ];
-
-  const opts = {
-    ...DefaultProcessorOptions,
-    daysBeforeStale: 0,
-    daysBeforeIssueStale: -1,
-    staleIssueMessage: ''
-  };
-
   const processor = new IssueProcessor(
     opts,
     async () => 'abot',
@@ -792,8 +787,14 @@ test('processing a stale issue will close it even if configured not to mark as s
 });
 
 test('processing a stale PR will close it even if configured not to mark as stale', async () => {
+  const opts = {
+    ...DefaultProcessorOptions,
+    daysBeforeStale: -1,
+    stalePrMessage: ''
+  };
   const TestIssueList: Issue[] = [
     generateIssue(
+      opts,
       1,
       'An issue with no label',
       '2020-01-01T17:00:00Z',
@@ -802,13 +803,6 @@ test('processing a stale PR will close it even if configured not to mark as stal
       ['Stale']
     )
   ];
-
-  const opts = {
-    ...DefaultProcessorOptions,
-    daysBeforeStale: -1,
-    stalePrMessage: ''
-  };
-
   const processor = new IssueProcessor(
     opts,
     async () => 'abot',
@@ -825,8 +819,15 @@ test('processing a stale PR will close it even if configured not to mark as stal
 });
 
 test('processing a stale PR will close it even if configured not to mark as stale when days-before-pr-stale override days-before-stale', async () => {
+  const opts = {
+    ...DefaultProcessorOptions,
+    daysBeforeStale: 0,
+    daysBeforePrStale: -1,
+    stalePrMessage: ''
+  };
   const TestIssueList: Issue[] = [
     generateIssue(
+      opts,
       1,
       'An issue with no label',
       '2020-01-01T17:00:00Z',
@@ -835,14 +836,6 @@ test('processing a stale PR will close it even if configured not to mark as stal
       ['Stale']
     )
   ];
-
-  const opts = {
-    ...DefaultProcessorOptions,
-    daysBeforeStale: 0,
-    daysBeforePrStale: -1,
-    stalePrMessage: ''
-  };
-
   const processor = new IssueProcessor(
     opts,
     async () => 'abot',
@@ -861,6 +854,7 @@ test('processing a stale PR will close it even if configured not to mark as stal
 test('closed issues will not be marked stale', async () => {
   const TestIssueList: Issue[] = [
     generateIssue(
+      DefaultProcessorOptions,
       1,
       'A closed issue that will not be marked',
       '2020-01-01T17:00:00Z',
@@ -870,7 +864,6 @@ test('closed issues will not be marked stale', async () => {
       true
     )
   ];
-
   const processor = new IssueProcessor(
     DefaultProcessorOptions,
     async () => 'abot',
@@ -888,6 +881,7 @@ test('closed issues will not be marked stale', async () => {
 test('stale closed issues will not be closed', async () => {
   const TestIssueList: Issue[] = [
     generateIssue(
+      DefaultProcessorOptions,
       1,
       'A stale closed issue',
       '2020-01-01T17:00:00Z',
@@ -897,7 +891,6 @@ test('stale closed issues will not be closed', async () => {
       true
     )
   ];
-
   const processor = new IssueProcessor(
     DefaultProcessorOptions,
     async () => 'abot',
@@ -916,6 +909,7 @@ test('stale closed issues will not be closed', async () => {
 test('closed prs will not be marked stale', async () => {
   const TestIssueList: Issue[] = [
     generateIssue(
+      DefaultProcessorOptions,
       1,
       'A closed PR that will not be marked',
       '2020-01-01T17:00:00Z',
@@ -925,7 +919,6 @@ test('closed prs will not be marked stale', async () => {
       true
     )
   ];
-
   const processor = new IssueProcessor(
     DefaultProcessorOptions,
     async () => 'abot',
@@ -944,6 +937,7 @@ test('closed prs will not be marked stale', async () => {
 test('stale closed prs will not be closed', async () => {
   const TestIssueList: Issue[] = [
     generateIssue(
+      DefaultProcessorOptions,
       1,
       'A stale closed PR that will not be closed again',
       '2020-01-01T17:00:00Z',
@@ -953,7 +947,6 @@ test('stale closed prs will not be closed', async () => {
       true
     )
   ];
-
   const processor = new IssueProcessor(
     DefaultProcessorOptions,
     async () => 'abot',
@@ -972,6 +965,7 @@ test('stale closed prs will not be closed', async () => {
 test('locked issues will not be marked stale', async () => {
   const TestIssueList: Issue[] = [
     generateIssue(
+      DefaultProcessorOptions,
       1,
       'A locked issue that will not be stale',
       '2020-01-01T17:00:00Z',
@@ -982,7 +976,6 @@ test('locked issues will not be marked stale', async () => {
       true
     )
   ];
-
   const processor = new IssueProcessor(
     DefaultProcessorOptions,
     async () => 'abot',
@@ -999,6 +992,7 @@ test('locked issues will not be marked stale', async () => {
 test('stale locked issues will not be closed', async () => {
   const TestIssueList: Issue[] = [
     generateIssue(
+      DefaultProcessorOptions,
       1,
       'A stale locked issue that will not be closed',
       '2020-01-01T17:00:00Z',
@@ -1009,7 +1003,6 @@ test('stale locked issues will not be closed', async () => {
       true
     )
   ];
-
   const processor = new IssueProcessor(
     DefaultProcessorOptions,
     async () => 'abot',
@@ -1028,6 +1021,7 @@ test('stale locked issues will not be closed', async () => {
 test('locked prs will not be marked stale', async () => {
   const TestIssueList: Issue[] = [
     generateIssue(
+      DefaultProcessorOptions,
       1,
       'A locked PR that will not be marked stale',
       '2020-01-01T17:00:00Z',
@@ -1038,7 +1032,6 @@ test('locked prs will not be marked stale', async () => {
       true
     )
   ];
-
   const processor = new IssueProcessor(
     DefaultProcessorOptions,
     async () => 'abot',
@@ -1055,6 +1048,7 @@ test('locked prs will not be marked stale', async () => {
 test('stale locked prs will not be closed', async () => {
   const TestIssueList: Issue[] = [
     generateIssue(
+      DefaultProcessorOptions,
       1,
       'A stale locked PR that will not be closed',
       '2020-01-01T17:00:00Z',
@@ -1065,7 +1059,6 @@ test('stale locked prs will not be closed', async () => {
       true
     )
   ];
-
   const processor = new IssueProcessor(
     DefaultProcessorOptions,
     async () => 'abot',
@@ -1083,8 +1076,11 @@ test('stale locked prs will not be closed', async () => {
 
 test('exempt issue labels will not be marked stale', async () => {
   expect.assertions(3);
+  const opts = {...DefaultProcessorOptions};
+  opts.exemptIssueLabels = 'Exempt';
   const TestIssueList: Issue[] = [
     generateIssue(
+      opts,
       1,
       'My first issue',
       '2020-01-01T17:00:00Z',
@@ -1093,10 +1089,6 @@ test('exempt issue labels will not be marked stale', async () => {
       ['Exempt']
     )
   ];
-
-  const opts = {...DefaultProcessorOptions};
-  opts.exemptIssueLabels = 'Exempt';
-
   const processor = new IssueProcessor(
     opts,
     async () => 'abot',
@@ -1114,8 +1106,11 @@ test('exempt issue labels will not be marked stale', async () => {
 });
 
 test('exempt issue labels will not be marked stale (multi issue label with spaces)', async () => {
+  const opts = {...DefaultProcessorOptions};
+  opts.exemptIssueLabels = 'Exempt, Cool, None';
   const TestIssueList: Issue[] = [
     generateIssue(
+      opts,
       1,
       'My first issue',
       '2020-01-01T17:00:00Z',
@@ -1124,10 +1119,6 @@ test('exempt issue labels will not be marked stale (multi issue label with space
       ['Cool']
     )
   ];
-
-  const opts = {...DefaultProcessorOptions};
-  opts.exemptIssueLabels = 'Exempt, Cool, None';
-
   const processor = new IssueProcessor(
     opts,
     async () => 'abot',
@@ -1144,8 +1135,11 @@ test('exempt issue labels will not be marked stale (multi issue label with space
 });
 
 test('exempt issue labels will not be marked stale (multi issue label)', async () => {
+  const opts = {...DefaultProcessorOptions};
+  opts.exemptIssueLabels = 'Exempt,Cool,None';
   const TestIssueList: Issue[] = [
     generateIssue(
+      opts,
       1,
       'My first issue',
       '2020-01-01T17:00:00Z',
@@ -1154,10 +1148,6 @@ test('exempt issue labels will not be marked stale (multi issue label)', async (
       ['Cool']
     )
   ];
-
-  const opts = {...DefaultProcessorOptions};
-  opts.exemptIssueLabels = 'Exempt,Cool,None';
-
   const processor = new IssueProcessor(
     opts,
     async () => 'abot',
@@ -1175,8 +1165,11 @@ test('exempt issue labels will not be marked stale (multi issue label)', async (
 });
 
 test('exempt pr labels will not be marked stale', async () => {
+  const opts = {...DefaultProcessorOptions};
+  opts.exemptIssueLabels = 'Cool';
   const TestIssueList: Issue[] = [
     generateIssue(
+      opts,
       1,
       'My first issue',
       '2020-01-01T17:00:00Z',
@@ -1185,6 +1178,7 @@ test('exempt pr labels will not be marked stale', async () => {
       ['Cool']
     ),
     generateIssue(
+      opts,
       2,
       'My first PR',
       '2020-01-01T17:00:00Z',
@@ -1193,6 +1187,7 @@ test('exempt pr labels will not be marked stale', async () => {
       ['Cool']
     ),
     generateIssue(
+      opts,
       3,
       'Another issue',
       '2020-01-01T17:00:00Z',
@@ -1200,10 +1195,6 @@ test('exempt pr labels will not be marked stale', async () => {
       false
     )
   ];
-
-  const opts = {...DefaultProcessorOptions};
-  opts.exemptIssueLabels = 'Cool';
-
   const processor = new IssueProcessor(
     opts,
     async () => 'abot',
@@ -1220,8 +1211,11 @@ test('exempt pr labels will not be marked stale', async () => {
 
 test('exempt issue labels will not be marked stale and will remove the existing stale label', async () => {
   expect.assertions(3);
+  const opts = {...DefaultProcessorOptions};
+  opts.exemptIssueLabels = 'Exempt';
   const TestIssueList: Issue[] = [
     generateIssue(
+      opts,
       1,
       'My first issue',
       '2020-01-01T17:00:00Z',
@@ -1230,8 +1224,6 @@ test('exempt issue labels will not be marked stale and will remove the existing
       ['Exempt', 'Stale']
     )
   ];
-  const opts = {...DefaultProcessorOptions};
-  opts.exemptIssueLabels = 'Exempt';
   const processor = new IssueProcessor(
     opts,
     async () => 'abot',
@@ -1256,8 +1248,11 @@ test('exempt issue labels will not be marked stale and will remove the existing
 });
 
 test('stale issues should not be closed if days is set to -1', async () => {
+  const opts = {...DefaultProcessorOptions};
+  opts.daysBeforeClose = -1;
   const TestIssueList: Issue[] = [
     generateIssue(
+      opts,
       1,
       'My first issue',
       '2020-01-01T17:00:00Z',
@@ -1266,6 +1261,7 @@ test('stale issues should not be closed if days is set to -1', async () => {
       ['Stale']
     ),
     generateIssue(
+      opts,
       2,
       'My first PR',
       '2020-01-01T17:00:00Z',
@@ -1274,6 +1270,7 @@ test('stale issues should not be closed if days is set to -1', async () => {
       ['Stale']
     ),
     generateIssue(
+      opts,
       3,
       'Another issue',
       '2020-01-01T17:00:00Z',
@@ -1282,10 +1279,6 @@ test('stale issues should not be closed if days is set to -1', async () => {
       ['Stale']
     )
   ];
-
-  const opts = {...DefaultProcessorOptions};
-  opts.daysBeforeClose = -1;
-
   const processor = new IssueProcessor(
     opts,
     async () => 'abot',
@@ -1302,8 +1295,11 @@ test('stale issues should not be closed if days is set to -1', async () => {
 });
 
 test('stale label should be removed if a comment was added to a stale issue', async () => {
+  const opts = {...DefaultProcessorOptions};
+  opts.removeStaleWhenUpdated = true;
   const TestIssueList: Issue[] = [
     generateIssue(
+      opts,
       1,
       'An issue that should un-stale',
       '2020-01-01T17:00:00Z',
@@ -1312,10 +1308,6 @@ test('stale label should be removed if a comment was added to a stale issue', as
       ['Stale']
     )
   ];
-
-  const opts = {...DefaultProcessorOptions};
-  opts.removeStaleWhenUpdated = true;
-
   const processor = new IssueProcessor(
     opts,
     async () => 'abot',
@@ -1340,9 +1332,12 @@ test('stale label should be removed if a comment was added to a stale issue', as
 });
 
 test('stale label should not be removed if a comment was added by the bot (and the issue should be closed)', async () => {
+  const opts = {...DefaultProcessorOptions};
+  opts.removeStaleWhenUpdated = true;
   github.context.actor = 'abot';
   const TestIssueList: Issue[] = [
     generateIssue(
+      opts,
       1,
       'An issue that should stay stale',
       '2020-01-01T17:00:00Z',
@@ -1351,10 +1346,6 @@ test('stale label should not be removed if a comment was added by the bot (and t
       ['Stale']
     )
   ];
-
-  const opts = {...DefaultProcessorOptions};
-  opts.removeStaleWhenUpdated = true;
-
   const processor = new IssueProcessor(
     opts,
     async () => 'abot',
@@ -1379,8 +1370,14 @@ test('stale label should not be removed if a comment was added by the bot (and t
 });
 
 test('stale label containing a space should be removed if a comment was added to a stale issue', async () => {
+  const opts: IssueProcessorOptions = {
+    ...DefaultProcessorOptions,
+    removeStaleWhenUpdated: true,
+    staleIssueLabel: 'stat: stale'
+  };
   const TestIssueList: Issue[] = [
     generateIssue(
+      opts,
       1,
       'An issue that should un-stale',
       '2020-01-01T17:00:00Z',
@@ -1389,13 +1386,6 @@ test('stale label containing a space should be removed if a comment was added to
       ['stat: stale']
     )
   ];
-
-  const opts: IssueProcessorOptions = {
-    ...DefaultProcessorOptions,
-    removeStaleWhenUpdated: true,
-    staleIssueLabel: 'stat: stale'
-  };
-
   const processor = new IssueProcessor(
     opts,
     async () => 'abot',
@@ -1413,10 +1403,14 @@ test('stale label containing a space should be removed if a comment was added to
 });
 
 test('stale issues should not be closed until after the closed number of days', async () => {
+  const opts = {...DefaultProcessorOptions};
+  opts.daysBeforeStale = 5; // stale after 5 days
+  opts.daysBeforeClose = 1; // closes after 6 days
   let lastUpdate = new Date();
   lastUpdate.setDate(lastUpdate.getDate() - 5);
   const TestIssueList: Issue[] = [
     generateIssue(
+      opts,
       1,
       'An issue that should be marked stale but not closed',
       lastUpdate.toString(),
@@ -1424,11 +1418,6 @@ test('stale issues should not be closed until after the closed number of days',
       false
     )
   ];
-
-  const opts = {...DefaultProcessorOptions};
-  opts.daysBeforeStale = 5; // stale after 5 days
-  opts.daysBeforeClose = 1; // closes after 6 days
-
   const processor = new IssueProcessor(
     opts,
     async () => 'abot',
@@ -1446,10 +1435,14 @@ test('stale issues should not be closed until after the closed number of days',
 });
 
 test('stale issues should be closed if the closed nubmer of days (additive) is also passed', async () => {
+  const opts = {...DefaultProcessorOptions};
+  opts.daysBeforeStale = 5; // stale after 5 days
+  opts.daysBeforeClose = 1; // closes after 6 days
   let lastUpdate = new Date();
   lastUpdate.setDate(lastUpdate.getDate() - 7);
   const TestIssueList: Issue[] = [
     generateIssue(
+      opts,
       1,
       'An issue that should be stale and closed',
       lastUpdate.toString(),
@@ -1458,11 +1451,6 @@ test('stale issues should be closed if the closed nubmer of days (additive) is a
       ['Stale']
     )
   ];
-
-  const opts = {...DefaultProcessorOptions};
-  opts.daysBeforeStale = 5; // stale after 5 days
-  opts.daysBeforeClose = 1; // closes after 6 days
-
   const processor = new IssueProcessor(
     opts,
     async () => 'abot',
@@ -1480,10 +1468,14 @@ test('stale issues should be closed if the closed nubmer of days (additive) is a
 });
 
 test('stale issues should not be closed until after the closed number of days (long)', async () => {
+  const opts = {...DefaultProcessorOptions};
+  opts.daysBeforeStale = 5; // stale after 5 days
+  opts.daysBeforeClose = 20; // closes after 25 days
   let lastUpdate = new Date();
   lastUpdate.setDate(lastUpdate.getDate() - 10);
   const TestIssueList: Issue[] = [
     generateIssue(
+      opts,
       1,
       'An issue that should be marked stale but not closed',
       lastUpdate.toString(),
@@ -1491,11 +1483,6 @@ test('stale issues should not be closed until after the closed number of days (l
       false
     )
   ];
-
-  const opts = {...DefaultProcessorOptions};
-  opts.daysBeforeStale = 5; // stale after 5 days
-  opts.daysBeforeClose = 20; // closes after 25 days
-
   const processor = new IssueProcessor(
     opts,
     async () => 'abot',
@@ -1513,10 +1500,15 @@ test('stale issues should not be closed until after the closed number of days (l
 });
 
 test('skips stale message on issues when skip-stale-issue-message is set', async () => {
+  const opts = {...DefaultProcessorOptions};
+  opts.daysBeforeStale = 5; // stale after 5 days
+  opts.daysBeforeClose = 20; // closes after 25 days
+  opts.skipStaleIssueMessage = true;
   let lastUpdate = new Date();
   lastUpdate.setDate(lastUpdate.getDate() - 10);
   const TestIssueList: Issue[] = [
     generateIssue(
+      opts,
       1,
       'An issue that should be marked stale but not closed',
       lastUpdate.toString(),
@@ -1524,12 +1516,6 @@ test('skips stale message on issues when skip-stale-issue-message is set', async
       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 () => 'abot',
@@ -1559,10 +1545,15 @@ test('skips stale message on issues when skip-stale-issue-message is set', async
 });
 
 test('skips stale message on prs when skip-stale-pr-message is set', async () => {
+  const opts = {...DefaultProcessorOptions};
+  opts.daysBeforeStale = 5; // stale after 5 days
+  opts.daysBeforeClose = 20; // closes after 25 days
+  opts.skipStalePrMessage = true;
   let lastUpdate = new Date();
   lastUpdate.setDate(lastUpdate.getDate() - 10);
   const TestIssueList: Issue[] = [
     generateIssue(
+      opts,
       1,
       'An issue that should be marked stale but not closed',
       lastUpdate.toString(),
@@ -1570,12 +1561,6 @@ test('skips stale message on prs when skip-stale-pr-message is set', async () =>
       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 () => 'abot',
@@ -1605,10 +1590,16 @@ test('skips stale message on prs when skip-stale-pr-message is set', async () =>
 });
 
 test('not providing state takes precedence over skipStaleIssueMessage', async () => {
+  const opts = {...DefaultProcessorOptions};
+  opts.daysBeforeStale = 5; // stale after 5 days
+  opts.daysBeforeClose = 20; // closes after 25 days
+  opts.skipStalePrMessage = true;
+  opts.staleIssueMessage = '';
   let lastUpdate = new Date();
   lastUpdate.setDate(lastUpdate.getDate() - 10);
   const TestIssueList: Issue[] = [
     generateIssue(
+      opts,
       1,
       'An issue that should be marked stale but not closed',
       lastUpdate.toString(),
@@ -1616,13 +1607,6 @@ test('not providing state takes precedence over skipStaleIssueMessage', async ()
       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 () => 'abot',
@@ -1640,10 +1624,16 @@ test('not providing state takes precedence over skipStaleIssueMessage', async ()
 });
 
 test('not providing stalePrMessage takes precedence over skipStalePrMessage', async () => {
+  const opts = {...DefaultProcessorOptions};
+  opts.daysBeforeStale = 5; // stale after 5 days
+  opts.daysBeforeClose = 20; // closes after 25 days
+  opts.skipStalePrMessage = true;
+  opts.stalePrMessage = '';
   let lastUpdate = new Date();
   lastUpdate.setDate(lastUpdate.getDate() - 10);
   const TestIssueList: Issue[] = [
     generateIssue(
+      opts,
       1,
       'An issue that should be marked stale but not closed',
       lastUpdate.toString(),
@@ -1651,13 +1641,6 @@ test('not providing stalePrMessage takes precedence over skipStalePrMessage', as
       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 () => 'abot',
@@ -1679,6 +1662,7 @@ test('git branch is deleted when option is enabled', async () => {
   const isPullRequest = true;
   const TestIssueList: Issue[] = [
     generateIssue(
+      opts,
       1,
       'An issue that should have its branch deleted',
       '2020-01-01T17:00:00Z',
@@ -1687,7 +1671,6 @@ test('git branch is deleted when option is enabled', async () => {
       ['Stale']
     )
   ];
-
   const processor = new IssueProcessor(
     opts,
     async () => 'abot',
@@ -1709,6 +1692,7 @@ test('git branch is not deleted when issue is not pull request', async () => {
   const isPullRequest = false;
   const TestIssueList: Issue[] = [
     generateIssue(
+      opts,
       1,
       'An issue that should not have its branch deleted',
       '2020-01-01T17:00:00Z',
@@ -1717,7 +1701,6 @@ test('git branch is not deleted when issue is not pull request', async () => {
       ['Stale']
     )
   ];
-
   const processor = new IssueProcessor(
     opts,
     async () => 'abot',
@@ -1733,3 +1716,549 @@ test('git branch is not deleted when issue is not pull request', async () => {
   expect(processor.staleIssues.length).toEqual(0);
   expect(processor.deletedBranchIssues.length).toEqual(0);
 });
+
+test('an issue without a milestone will be marked as stale', async () => {
+  expect.assertions(3);
+  const TestIssueList: Issue[] = [
+    generateIssue(
+      DefaultProcessorOptions,
+      1,
+      'My first issue',
+      '2020-01-01T17:00:00Z',
+      '2020-01-01T17:00:00Z',
+      false,
+      undefined,
+      undefined,
+      undefined,
+      ''
+    )
+  ];
+  const processor = new IssueProcessor(
+    DefaultProcessorOptions,
+    async () => 'abot',
+    async p => (p == 1 ? TestIssueList : []),
+    async (num: number, dt: string) => [],
+    async (issue: Issue, label: string) => new Date().toDateString()
+  );
+
+  // process our fake issue list
+  await processor.processIssues(1);
+
+  expect(processor.staleIssues.length).toStrictEqual(1);
+  expect(processor.closedIssues.length).toStrictEqual(0);
+  expect(processor.removedLabelIssues.length).toStrictEqual(0);
+});
+
+test('an issue without an exempted milestone will be marked as stale', async () => {
+  expect.assertions(3);
+  const opts = {...DefaultProcessorOptions};
+  opts.exemptMilestones = 'Milestone1';
+  const TestIssueList: Issue[] = [
+    generateIssue(
+      opts,
+      1,
+      'My first issue',
+      '2020-01-01T17:00:00Z',
+      '2020-01-01T17:00:00Z',
+      false,
+      undefined,
+      undefined,
+      undefined,
+      'Milestone'
+    )
+  ];
+  const processor = new IssueProcessor(
+    opts,
+    async () => 'abot',
+    async p => (p == 1 ? TestIssueList : []),
+    async (num: number, dt: string) => [],
+    async (issue: Issue, label: string) => new Date().toDateString()
+  );
+
+  // process our fake issue list
+  await processor.processIssues(1);
+
+  expect(processor.staleIssues.length).toStrictEqual(1);
+  expect(processor.closedIssues.length).toStrictEqual(0);
+  expect(processor.removedLabelIssues.length).toStrictEqual(0);
+});
+
+test('an issue with an exempted milestone will not be marked as stale', async () => {
+  expect.assertions(3);
+  const opts = {...DefaultProcessorOptions};
+  opts.exemptMilestones = 'Milestone1';
+  const TestIssueList: Issue[] = [
+    generateIssue(
+      opts,
+      1,
+      'My first issue',
+      '2020-01-01T17:00:00Z',
+      '2020-01-01T17:00:00Z',
+      false,
+      undefined,
+      undefined,
+      undefined,
+      'Milestone1'
+    )
+  ];
+  const processor = new IssueProcessor(
+    opts,
+    async () => 'abot',
+    async p => (p == 1 ? TestIssueList : []),
+    async (num: number, dt: string) => [],
+    async (issue: Issue, label: string) => new Date().toDateString()
+  );
+
+  // process our fake issue list
+  await processor.processIssues(1);
+
+  expect(processor.staleIssues.length).toStrictEqual(0);
+  expect(processor.closedIssues.length).toStrictEqual(0);
+  expect(processor.removedLabelIssues.length).toStrictEqual(0);
+});
+
+test('an issue with an exempted milestone will not be marked as stale (multi milestones with spaces)', async () => {
+  expect.assertions(3);
+  const opts = {...DefaultProcessorOptions};
+  opts.exemptMilestones = 'Milestone1, Milestone2';
+  const TestIssueList: Issue[] = [
+    generateIssue(
+      opts,
+      1,
+      'My first issue',
+      '2020-01-01T17:00:00Z',
+      '2020-01-01T17:00:00Z',
+      false,
+      undefined,
+      undefined,
+      undefined,
+      'Milestone2'
+    )
+  ];
+  const processor = new IssueProcessor(
+    opts,
+    async () => 'abot',
+    async p => (p == 1 ? TestIssueList : []),
+    async (num: number, dt: string) => [],
+    async (issue: Issue, label: string) => new Date().toDateString()
+  );
+
+  // process our fake issue list
+  await processor.processIssues(1);
+
+  expect(processor.staleIssues.length).toStrictEqual(0);
+  expect(processor.closedIssues.length).toStrictEqual(0);
+  expect(processor.removedLabelIssues.length).toStrictEqual(0);
+});
+
+test('an issue with an exempted milestone will not be marked as stale (multi milestones without spaces)', async () => {
+  expect.assertions(3);
+  const opts = {...DefaultProcessorOptions};
+  opts.exemptMilestones = 'Milestone1,Milestone2';
+  const TestIssueList: Issue[] = [
+    generateIssue(
+      opts,
+      1,
+      'My first issue',
+      '2020-01-01T17:00:00Z',
+      '2020-01-01T17:00:00Z',
+      false,
+      undefined,
+      undefined,
+      undefined,
+      'Milestone2'
+    )
+  ];
+  const processor = new IssueProcessor(
+    opts,
+    async () => 'abot',
+    async p => (p == 1 ? TestIssueList : []),
+    async (num: number, dt: string) => [],
+    async (issue: Issue, label: string) => new Date().toDateString()
+  );
+
+  // process our fake issue list
+  await processor.processIssues(1);
+
+  expect(processor.staleIssues.length).toStrictEqual(0);
+  expect(processor.closedIssues.length).toStrictEqual(0);
+  expect(processor.removedLabelIssues.length).toStrictEqual(0);
+});
+
+test('an issue with an exempted milestone but without an exempted issue milestone will not be marked as stale', async () => {
+  expect.assertions(3);
+  const opts = {...DefaultProcessorOptions};
+  opts.exemptMilestones = 'Milestone1';
+  opts.exemptIssueMilestones = '';
+  const TestIssueList: Issue[] = [
+    generateIssue(
+      opts,
+      1,
+      'My first issue',
+      '2020-01-01T17:00:00Z',
+      '2020-01-01T17:00:00Z',
+      false,
+      undefined,
+      undefined,
+      undefined,
+      'Milestone1'
+    )
+  ];
+  const processor = new IssueProcessor(
+    opts,
+    async () => 'abot',
+    async p => (p == 1 ? TestIssueList : []),
+    async (num: number, dt: string) => [],
+    async (issue: Issue, label: string) => new Date().toDateString()
+  );
+
+  // process our fake issue list
+  await processor.processIssues(1);
+
+  expect(processor.staleIssues.length).toStrictEqual(0);
+  expect(processor.closedIssues.length).toStrictEqual(0);
+  expect(processor.removedLabelIssues.length).toStrictEqual(0);
+});
+
+test('an issue with an exempted milestone but with another exempted issue milestone will be marked as stale', async () => {
+  expect.assertions(3);
+  const opts = {...DefaultProcessorOptions};
+  opts.exemptMilestones = 'Milestone1';
+  opts.exemptIssueMilestones = 'Milestone2';
+  const TestIssueList: Issue[] = [
+    generateIssue(
+      opts,
+      1,
+      'My first issue',
+      '2020-01-01T17:00:00Z',
+      '2020-01-01T17:00:00Z',
+      false,
+      undefined,
+      undefined,
+      undefined,
+      'Milestone1'
+    )
+  ];
+  const processor = new IssueProcessor(
+    opts,
+    async () => 'abot',
+    async p => (p == 1 ? TestIssueList : []),
+    async (num: number, dt: string) => [],
+    async (issue: Issue, label: string) => new Date().toDateString()
+  );
+
+  // process our fake issue list
+  await processor.processIssues(1);
+
+  expect(processor.staleIssues.length).toStrictEqual(1);
+  expect(processor.closedIssues.length).toStrictEqual(0);
+  expect(processor.removedLabelIssues.length).toStrictEqual(0);
+});
+
+test('an issue with an exempted milestone and with an exempted issue milestone will not be marked as stale', async () => {
+  expect.assertions(3);
+  const opts = {...DefaultProcessorOptions};
+  opts.exemptMilestones = 'Milestone1';
+  opts.exemptIssueMilestones = 'Milestone1';
+  const TestIssueList: Issue[] = [
+    generateIssue(
+      opts,
+      1,
+      'My first issue',
+      '2020-01-01T17:00:00Z',
+      '2020-01-01T17:00:00Z',
+      false,
+      undefined,
+      undefined,
+      undefined,
+      'Milestone1'
+    )
+  ];
+  const processor = new IssueProcessor(
+    opts,
+    async () => 'abot',
+    async p => (p == 1 ? TestIssueList : []),
+    async (num: number, dt: string) => [],
+    async (issue: Issue, label: string) => new Date().toDateString()
+  );
+
+  // process our fake issue list
+  await processor.processIssues(1);
+
+  expect(processor.staleIssues.length).toStrictEqual(0);
+  expect(processor.closedIssues.length).toStrictEqual(0);
+  expect(processor.removedLabelIssues.length).toStrictEqual(0);
+});
+
+test('a PR without a milestone will be marked as stale', async () => {
+  expect.assertions(3);
+  const TestIssueList: Issue[] = [
+    generateIssue(
+      DefaultProcessorOptions,
+      1,
+      'My first issue',
+      '2020-01-01T17:00:00Z',
+      '2020-01-01T17:00:00Z',
+      true,
+      undefined,
+      undefined,
+      undefined,
+      ''
+    )
+  ];
+  const processor = new IssueProcessor(
+    DefaultProcessorOptions,
+    async () => 'abot',
+    async p => (p == 1 ? TestIssueList : []),
+    async (num: number, dt: string) => [],
+    async (issue: Issue, label: string) => new Date().toDateString()
+  );
+
+  // process our fake issue list
+  await processor.processIssues(1);
+
+  expect(processor.staleIssues.length).toStrictEqual(1);
+  expect(processor.closedIssues.length).toStrictEqual(0);
+  expect(processor.removedLabelIssues.length).toStrictEqual(0);
+});
+
+test('a PR without an exempted milestone will be marked as stale', async () => {
+  expect.assertions(3);
+  const opts = {...DefaultProcessorOptions};
+  opts.exemptMilestones = 'Milestone1';
+  const TestIssueList: Issue[] = [
+    generateIssue(
+      opts,
+      1,
+      'My first issue',
+      '2020-01-01T17:00:00Z',
+      '2020-01-01T17:00:00Z',
+      true,
+      undefined,
+      undefined,
+      undefined,
+      'Milestone'
+    )
+  ];
+  const processor = new IssueProcessor(
+    opts,
+    async () => 'abot',
+    async p => (p == 1 ? TestIssueList : []),
+    async (num: number, dt: string) => [],
+    async (issue: Issue, label: string) => new Date().toDateString()
+  );
+
+  // process our fake issue list
+  await processor.processIssues(1);
+
+  expect(processor.staleIssues.length).toStrictEqual(1);
+  expect(processor.closedIssues.length).toStrictEqual(0);
+  expect(processor.removedLabelIssues.length).toStrictEqual(0);
+});
+
+test('a PR with an exempted milestone will not be marked as stale', async () => {
+  expect.assertions(3);
+  const opts = {...DefaultProcessorOptions};
+  opts.exemptMilestones = 'Milestone1';
+  const TestIssueList: Issue[] = [
+    generateIssue(
+      opts,
+      1,
+      'My first issue',
+      '2020-01-01T17:00:00Z',
+      '2020-01-01T17:00:00Z',
+      true,
+      undefined,
+      undefined,
+      undefined,
+      'Milestone1'
+    )
+  ];
+  const processor = new IssueProcessor(
+    opts,
+    async () => 'abot',
+    async p => (p == 1 ? TestIssueList : []),
+    async (num: number, dt: string) => [],
+    async (issue: Issue, label: string) => new Date().toDateString()
+  );
+
+  // process our fake issue list
+  await processor.processIssues(1);
+
+  expect(processor.staleIssues.length).toStrictEqual(0);
+  expect(processor.closedIssues.length).toStrictEqual(0);
+  expect(processor.removedLabelIssues.length).toStrictEqual(0);
+});
+
+test('a PR with an exempted milestone will not be marked as stale (multi milestones with spaces)', async () => {
+  expect.assertions(3);
+  const opts = {...DefaultProcessorOptions};
+  opts.exemptMilestones = 'Milestone1, Milestone2';
+  const TestIssueList: Issue[] = [
+    generateIssue(
+      opts,
+      1,
+      'My first issue',
+      '2020-01-01T17:00:00Z',
+      '2020-01-01T17:00:00Z',
+      true,
+      undefined,
+      undefined,
+      undefined,
+      'Milestone2'
+    )
+  ];
+  const processor = new IssueProcessor(
+    opts,
+    async () => 'abot',
+    async p => (p == 1 ? TestIssueList : []),
+    async (num: number, dt: string) => [],
+    async (issue: Issue, label: string) => new Date().toDateString()
+  );
+
+  // process our fake issue list
+  await processor.processIssues(1);
+
+  expect(processor.staleIssues.length).toStrictEqual(0);
+  expect(processor.closedIssues.length).toStrictEqual(0);
+  expect(processor.removedLabelIssues.length).toStrictEqual(0);
+});
+
+test('a PR with an exempted milestone will not be marked as stale (multi milestones without spaces)', async () => {
+  expect.assertions(3);
+  const opts = {...DefaultProcessorOptions};
+  opts.exemptMilestones = 'Milestone1,Milestone2';
+  const TestIssueList: Issue[] = [
+    generateIssue(
+      opts,
+      1,
+      'My first issue',
+      '2020-01-01T17:00:00Z',
+      '2020-01-01T17:00:00Z',
+      true,
+      undefined,
+      undefined,
+      undefined,
+      'Milestone2'
+    )
+  ];
+  const processor = new IssueProcessor(
+    opts,
+    async () => 'abot',
+    async p => (p == 1 ? TestIssueList : []),
+    async (num: number, dt: string) => [],
+    async (issue: Issue, label: string) => new Date().toDateString()
+  );
+
+  // process our fake issue list
+  await processor.processIssues(1);
+
+  expect(processor.staleIssues.length).toStrictEqual(0);
+  expect(processor.closedIssues.length).toStrictEqual(0);
+  expect(processor.removedLabelIssues.length).toStrictEqual(0);
+});
+
+test('a PR with an exempted milestone but without an exempted issue milestone will not be marked as stale', async () => {
+  expect.assertions(3);
+  const opts = {...DefaultProcessorOptions};
+  opts.exemptMilestones = 'Milestone1';
+  opts.exemptPrMilestones = '';
+  const TestIssueList: Issue[] = [
+    generateIssue(
+      opts,
+      1,
+      'My first issue',
+      '2020-01-01T17:00:00Z',
+      '2020-01-01T17:00:00Z',
+      true,
+      undefined,
+      undefined,
+      undefined,
+      'Milestone1'
+    )
+  ];
+  const processor = new IssueProcessor(
+    opts,
+    async () => 'abot',
+    async p => (p == 1 ? TestIssueList : []),
+    async (num: number, dt: string) => [],
+    async (issue: Issue, label: string) => new Date().toDateString()
+  );
+
+  // process our fake issue list
+  await processor.processIssues(1);
+
+  expect(processor.staleIssues.length).toStrictEqual(0);
+  expect(processor.closedIssues.length).toStrictEqual(0);
+  expect(processor.removedLabelIssues.length).toStrictEqual(0);
+});
+
+test('a PR with an exempted milestone but with another exempted issue milestone will be marked as stale', async () => {
+  expect.assertions(3);
+  const opts = {...DefaultProcessorOptions};
+  opts.exemptMilestones = 'Milestone1';
+  opts.exemptPrMilestones = 'Milestone2';
+  const TestIssueList: Issue[] = [
+    generateIssue(
+      opts,
+      1,
+      'My first issue',
+      '2020-01-01T17:00:00Z',
+      '2020-01-01T17:00:00Z',
+      true,
+      undefined,
+      undefined,
+      undefined,
+      'Milestone1'
+    )
+  ];
+  const processor = new IssueProcessor(
+    opts,
+    async () => 'abot',
+    async p => (p == 1 ? TestIssueList : []),
+    async (num: number, dt: string) => [],
+    async (issue: Issue, label: string) => new Date().toDateString()
+  );
+
+  // process our fake issue list
+  await processor.processIssues(1);
+
+  expect(processor.staleIssues.length).toStrictEqual(1);
+  expect(processor.closedIssues.length).toStrictEqual(0);
+  expect(processor.removedLabelIssues.length).toStrictEqual(0);
+});
+
+test('a PR with an exempted milestone and with an exempted issue milestone will not be marked as stale', async () => {
+  expect.assertions(3);
+  const opts = {...DefaultProcessorOptions};
+  opts.exemptMilestones = 'Milestone1';
+  opts.exemptPrMilestones = 'Milestone1';
+  const TestIssueList: Issue[] = [
+    generateIssue(
+      opts,
+      1,
+      'My first issue',
+      '2020-01-01T17:00:00Z',
+      '2020-01-01T17:00:00Z',
+      true,
+      undefined,
+      undefined,
+      undefined,
+      'Milestone1'
+    )
+  ];
+  const processor = new IssueProcessor(
+    opts,
+    async () => 'abot',
+    async p => (p == 1 ? TestIssueList : []),
+    async (num: number, dt: string) => [],
+    async (issue: Issue, label: string) => new Date().toDateString()
+  );
+
+  // process our fake issue list
+  await processor.processIssues(1);
+
+  expect(processor.staleIssues.length).toStrictEqual(0);
+  expect(processor.closedIssues.length).toStrictEqual(0);
+  expect(processor.removedLabelIssues.length).toStrictEqual(0);
+});