mirror of
https://github.com/actions/stale.git
synced 2025-12-24 09:28:18 +00:00
Compare commits
46 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9d6f46564a | ||
|
|
d21d307fd8 | ||
|
|
7164109781 | ||
|
|
079c368275 | ||
|
|
86561461b9 | ||
|
|
342b612d3c | ||
|
|
d09524cd2c | ||
|
|
f71123a6f7 | ||
|
|
1b9f13b607 | ||
|
|
f8321eb62b | ||
|
|
6a04021499 | ||
|
|
7e83de7bef | ||
|
|
a9eccc216e | ||
|
|
0fa41d1841 | ||
|
|
f698371c0d | ||
|
|
7f340a46f3 | ||
|
|
3f95874437 | ||
|
|
e6b77bc964 | ||
|
|
552e4c60f0 | ||
|
|
b12dccced8 | ||
|
|
7de5f1946d | ||
|
|
546a4e9fd6 | ||
|
|
b5b956deb3 | ||
|
|
6674e9130f | ||
|
|
73dbf86108 | ||
|
|
a0b4b61711 | ||
|
|
0f78ebc7dc | ||
|
|
b70b09d43a | ||
|
|
aca4d264ff | ||
|
|
44c3cb508b | ||
|
|
39de730f99 | ||
|
|
ddc7648635 | ||
|
|
107018c400 | ||
|
|
87c2b794b9 | ||
|
|
b7c7b1c5c3 | ||
|
|
08900ff5d1 | ||
|
|
6ea8297c81 | ||
|
|
23d5d28543 | ||
|
|
2c5799741a | ||
|
|
324009e5d0 | ||
|
|
9b82e8c1ef | ||
|
|
af40726159 | ||
|
|
658139a1f1 | ||
|
|
707ba4d103 | ||
|
|
62b357a603 | ||
|
|
f75b3c75ce |
107
.eslintrc.json
107
.eslintrc.json
@@ -1,52 +1,57 @@
|
||||
{
|
||||
"plugins": ["jest", "@typescript-eslint"],
|
||||
"extends": ["plugin:github/recommended"],
|
||||
"parser": "@typescript-eslint/parser",
|
||||
"parserOptions": {
|
||||
"ecmaVersion": 9,
|
||||
"sourceType": "module",
|
||||
"project": "./tsconfig.json"
|
||||
},
|
||||
"rules": {
|
||||
"eslint-comments/no-use": "off",
|
||||
"import/no-namespace": "off",
|
||||
"no-unused-vars": "off",
|
||||
"@typescript-eslint/no-unused-vars": "error",
|
||||
"@typescript-eslint/explicit-member-accessibility": ["error", {"accessibility": "no-public"}],
|
||||
"@typescript-eslint/no-require-imports": "error",
|
||||
"@typescript-eslint/array-type": "error",
|
||||
"@typescript-eslint/await-thenable": "error",
|
||||
"@typescript-eslint/ban-ts-comment": "error",
|
||||
"camelcase": "off",
|
||||
"@typescript-eslint/consistent-type-assertions": "error",
|
||||
"@typescript-eslint/func-call-spacing": ["error", "never"],
|
||||
"@typescript-eslint/no-array-constructor": "error",
|
||||
"@typescript-eslint/no-empty-interface": "error",
|
||||
"@typescript-eslint/no-explicit-any": "off",
|
||||
"@typescript-eslint/no-extraneous-class": "error",
|
||||
"@typescript-eslint/no-for-in-array": "error",
|
||||
"@typescript-eslint/no-inferrable-types": "error",
|
||||
"@typescript-eslint/no-misused-new": "error",
|
||||
"@typescript-eslint/no-namespace": "error",
|
||||
"@typescript-eslint/no-non-null-assertion": "warn",
|
||||
"@typescript-eslint/no-unnecessary-qualifier": "error",
|
||||
"@typescript-eslint/no-unnecessary-type-assertion": "error",
|
||||
"@typescript-eslint/no-useless-constructor": "error",
|
||||
"@typescript-eslint/no-var-requires": "error",
|
||||
"@typescript-eslint/prefer-for-of": "warn",
|
||||
"@typescript-eslint/prefer-function-type": "warn",
|
||||
"@typescript-eslint/prefer-includes": "error",
|
||||
"@typescript-eslint/prefer-string-starts-ends-with": "error",
|
||||
"@typescript-eslint/promise-function-async": "error",
|
||||
"@typescript-eslint/require-array-sort-compare": "error",
|
||||
"@typescript-eslint/restrict-plus-operands": "error",
|
||||
"semi": "off",
|
||||
"@typescript-eslint/type-annotation-spacing": "error",
|
||||
"@typescript-eslint/unbound-method": "off"
|
||||
},
|
||||
"env": {
|
||||
"node": true,
|
||||
"es6": true,
|
||||
"jest/globals": true
|
||||
}
|
||||
}
|
||||
"plugins": ["jest", "@typescript-eslint"],
|
||||
"extends": ["plugin:github/recommended"],
|
||||
"parser": "@typescript-eslint/parser",
|
||||
"parserOptions": {
|
||||
"ecmaVersion": 9,
|
||||
"sourceType": "module",
|
||||
"project": "./tsconfig.json"
|
||||
},
|
||||
"rules": {
|
||||
"eslint-comments/no-use": "off",
|
||||
"import/no-namespace": "off",
|
||||
"no-unused-vars": "off",
|
||||
"@typescript-eslint/no-unused-vars": "error",
|
||||
"@typescript-eslint/explicit-member-accessibility": [
|
||||
"error",
|
||||
{"accessibility": "no-public"}
|
||||
],
|
||||
"@typescript-eslint/no-require-imports": "error",
|
||||
"@typescript-eslint/array-type": "error",
|
||||
"@typescript-eslint/await-thenable": "error",
|
||||
"@typescript-eslint/ban-ts-comment": "error",
|
||||
"camelcase": "off",
|
||||
"@typescript-eslint/consistent-type-assertions": "error",
|
||||
"@typescript-eslint/func-call-spacing": ["error", "never"],
|
||||
"@typescript-eslint/no-array-constructor": "error",
|
||||
"@typescript-eslint/no-empty-interface": "error",
|
||||
"@typescript-eslint/no-explicit-any": "off",
|
||||
"@typescript-eslint/no-extraneous-class": "error",
|
||||
"@typescript-eslint/no-for-in-array": "error",
|
||||
"@typescript-eslint/no-inferrable-types": "error",
|
||||
"@typescript-eslint/no-misused-new": "error",
|
||||
"@typescript-eslint/no-namespace": "error",
|
||||
"@typescript-eslint/no-non-null-assertion": "warn",
|
||||
"@typescript-eslint/no-unnecessary-qualifier": "error",
|
||||
"@typescript-eslint/no-unnecessary-type-assertion": "error",
|
||||
"@typescript-eslint/no-useless-constructor": "error",
|
||||
"@typescript-eslint/no-var-requires": "error",
|
||||
"@typescript-eslint/prefer-for-of": "warn",
|
||||
"@typescript-eslint/prefer-function-type": "warn",
|
||||
"@typescript-eslint/prefer-includes": "error",
|
||||
"@typescript-eslint/prefer-string-starts-ends-with": "error",
|
||||
"@typescript-eslint/promise-function-async": "error",
|
||||
"@typescript-eslint/require-array-sort-compare": "error",
|
||||
"@typescript-eslint/restrict-plus-operands": "error",
|
||||
"semi": "off",
|
||||
"@typescript-eslint/type-annotation-spacing": "error",
|
||||
"@typescript-eslint/unbound-method": "off",
|
||||
"no-shadow": "off",
|
||||
"@typescript-eslint/no-shadow": "error"
|
||||
},
|
||||
"env": {
|
||||
"node": true,
|
||||
"es6": true,
|
||||
"jest/globals": true
|
||||
}
|
||||
}
|
||||
|
||||
31
.github/workflows/codeql.yml
vendored
31
.github/workflows/codeql.yml
vendored
@@ -1,4 +1,4 @@
|
||||
name: "Code scanning"
|
||||
name: 'Code scanning'
|
||||
|
||||
on:
|
||||
push:
|
||||
@@ -8,29 +8,16 @@ on:
|
||||
|
||||
jobs:
|
||||
CodeQL-Build:
|
||||
|
||||
# CodeQL runs on ubuntu-latest and windows-latest
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v2
|
||||
with:
|
||||
# We must fetch at least the immediate parents so that if this is
|
||||
# a pull request then we can checkout the head.
|
||||
fetch-depth: 2
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v2
|
||||
|
||||
# If this run was triggered by a pull request event, then checkout
|
||||
# the head of the pull request instead of the merge commit.
|
||||
- run: git checkout HEAD^2
|
||||
if: ${{ github.event_name == 'pull_request' }}
|
||||
|
||||
# Initializes the CodeQL tools for scanning.
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@v1
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@v1
|
||||
|
||||
- name: Autobuild
|
||||
uses: github/codeql-action/autobuild@v1
|
||||
- name: Autobuild
|
||||
uses: github/codeql-action/autobuild@v1
|
||||
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@v1
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@v1
|
||||
|
||||
16
.github/workflows/stale.yml
vendored
16
.github/workflows/stale.yml
vendored
@@ -1,16 +1,16 @@
|
||||
name: "Stale issue handler"
|
||||
name: 'Stale issue handler'
|
||||
on:
|
||||
workflow_dispatch:
|
||||
schedule:
|
||||
- cron: "0 0 * * *"
|
||||
- cron: '0 0 * * *'
|
||||
|
||||
jobs:
|
||||
stale:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/stale@v3
|
||||
with:
|
||||
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
stale-issue-message: 'This issue is stale because it has been open 30 days with no activity. Remove stale label or comment or this will be closed in 5 days'
|
||||
days-before-stale: 30
|
||||
days-before-close: 5
|
||||
- uses: actions/stale@main
|
||||
with:
|
||||
stale-issue-message: 'This issue is stale because it has been open 30 days with no activity. Remove stale label or comment or this will be closed in 5 days'
|
||||
days-before-stale: 30
|
||||
days-before-close: 5
|
||||
exempt-issue-labels: 'blocked,must,should,keep'
|
||||
|
||||
23
.github/workflows/test.yml
vendored
23
.github/workflows/test.yml
vendored
@@ -1,4 +1,4 @@
|
||||
name: "Build"
|
||||
name: 'Build'
|
||||
on: # rebuild any PRs and main branch changes
|
||||
pull_request:
|
||||
push:
|
||||
@@ -10,17 +10,16 @@ jobs:
|
||||
build: # make sure build/ci work properly
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- run: |
|
||||
npm install
|
||||
npm run all
|
||||
- uses: actions/checkout@v2
|
||||
- run: |
|
||||
npm install
|
||||
npm run all
|
||||
test: # make sure the action works on a clean machine without building
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: ./
|
||||
with:
|
||||
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
stale-issue-message: 'This issue is stale'
|
||||
stale-pr-message: 'This PR is stale'
|
||||
debug-only: true
|
||||
- uses: actions/checkout@v2
|
||||
- uses: ./
|
||||
with:
|
||||
stale-issue-message: 'This issue is stale'
|
||||
stale-pr-message: 'This PR is stale'
|
||||
debug-only: true
|
||||
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -2,3 +2,4 @@
|
||||
node_modules/
|
||||
lib/
|
||||
__tests__/runner/*
|
||||
.idea
|
||||
|
||||
@@ -11,4 +11,4 @@ allowed:
|
||||
- unlicense
|
||||
|
||||
reviewed:
|
||||
npm:
|
||||
npm:
|
||||
|
||||
7
.prettierignore
Normal file
7
.prettierignore
Normal file
@@ -0,0 +1,7 @@
|
||||
.idea
|
||||
.licenses
|
||||
.vscode
|
||||
dist
|
||||
lib
|
||||
node_modules
|
||||
package-lock.json
|
||||
@@ -1,11 +1,10 @@
|
||||
{
|
||||
"printWidth": 80,
|
||||
"tabWidth": 2,
|
||||
"useTabs": false,
|
||||
"semi": true,
|
||||
"singleQuote": true,
|
||||
"trailingComma": "none",
|
||||
"bracketSpacing": false,
|
||||
"arrowParens": "avoid",
|
||||
"parser": "typescript"
|
||||
}
|
||||
"printWidth": 80,
|
||||
"tabWidth": 2,
|
||||
"useTabs": false,
|
||||
"semi": true,
|
||||
"singleQuote": true,
|
||||
"trailingComma": "none",
|
||||
"bracketSpacing": false,
|
||||
"arrowParens": "avoid"
|
||||
}
|
||||
|
||||
1
CODEOWNERS
Normal file
1
CODEOWNERS
Normal file
@@ -0,0 +1 @@
|
||||
* @actions/actions-runtime
|
||||
185
README.md
185
README.md
@@ -4,83 +4,202 @@ Warns and then closes issues and PRs that have had no activity for a specified a
|
||||
|
||||
### Building and testing
|
||||
|
||||
Install the dependencies
|
||||
Install the dependencies
|
||||
|
||||
```bash
|
||||
$ npm install
|
||||
```
|
||||
|
||||
Build the typescript and package it for distribution
|
||||
|
||||
```bash
|
||||
$ npm run build && npm run pack
|
||||
```
|
||||
|
||||
Run the tests :heavy_check_mark:
|
||||
Run the tests :heavy_check_mark:
|
||||
|
||||
```bash
|
||||
$ npm test
|
||||
```
|
||||
|
||||
### Arguments
|
||||
|
||||
| Input | Description | Usage |
|
||||
| --------------------------- | -------------------------------------------------------------------------------------------- | -------- |
|
||||
| `repo-token` | PAT(Personal Access Token) for authorizing repository. _Defaults to **${{ github.token }}**_ | Optional |
|
||||
| `days-before-stale` | Idle number of days before marking an issue/pr as stale. _Defaults to **60**_ | Optional |
|
||||
| `days-before-issue-stale` | Idle number of days before marking an issue as stale (override `days-before-stale`). | Optional |
|
||||
| `days-before-pr-stale` | Idle number of days before marking an pr as stale (override `days-before-stale`). | Optional |
|
||||
| `days-before-close` | Idle number of days before closing an stale issue/pr. _Defaults to **7**_ | Optional |
|
||||
| `days-before-issue-close` | Idle number of days before closing an stale issue (override `days-before-close`). | Optional |
|
||||
| `days-before-pr-close` | Idle number of days before closing an stale pr (override `days-before-close`). | Optional |
|
||||
| `stale-issue-message` | Message to post on the stale issue. | Optional |
|
||||
| `stale-pr-message` | Message to post on the stale pr. | Optional |
|
||||
| `close-issue-message` | Message to post on the stale issue while closing it. | Optional |
|
||||
| `close-pr-message` | Message to post on the stale pr while closing it. | Optional |
|
||||
| `stale-issue-label` | Label to apply on the stale issue. _Defaults to **stale**_ | Optional |
|
||||
| `close-issue-label` | Label to apply on closing issue. | Optional |
|
||||
| `stale-pr-label` | Label to apply on the stale pr. | Optional |
|
||||
| `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 |
|
||||
| `debug-only` | Dry-run on action. _Defaults to **false**_ | Optional |
|
||||
| `ascending` | Order to get issues/pr. _Defaults to **false**_ | Optional |
|
||||
| `skip-stale-issue-message` | Skip adding stale message on stale issue. _Defaults to **false**_ | Optional |
|
||||
| `skip-stale-pr-message` | Skip adding stale message on stale pr. _Defaults to **false**_ | Optional |
|
||||
| `start-date` | The date used to skip the stale action on issue/pr created before it (ISO 8601 or RFC 2822). | Optional |
|
||||
|
||||
### Usage
|
||||
|
||||
See [action.yml](./action.yml) For comprehensive list of options.
|
||||
|
||||
|
||||
Basic:
|
||||
|
||||
```yaml
|
||||
name: "Close stale issues"
|
||||
name: 'Close stale issues and PRs'
|
||||
on:
|
||||
schedule:
|
||||
- cron: "0 0 * * *"
|
||||
- cron: '30 1 * * *'
|
||||
|
||||
jobs:
|
||||
stale:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/stale@v3
|
||||
with:
|
||||
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
stale-issue-message: 'Message to comment on stale issues. If none provided, will not mark issues stale'
|
||||
stale-pr-message: 'Message to comment on stale PRs. If none provided, will not mark PRs stale'
|
||||
- uses: actions/stale@v3
|
||||
with:
|
||||
stale-issue-message: 'Message to comment on stale issues. If none provided, will not mark issues stale'
|
||||
stale-pr-message: 'Message to comment on stale PRs. If none provided, will not mark PRs stale'
|
||||
```
|
||||
|
||||
|
||||
Configure stale timeouts:
|
||||
|
||||
```yaml
|
||||
name: "Close stale issues"
|
||||
name: 'Close stale issues and PRs'
|
||||
on:
|
||||
schedule:
|
||||
- cron: "0 0 * * *"
|
||||
- cron: '30 1 * * *'
|
||||
|
||||
jobs:
|
||||
stale:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/stale@v3
|
||||
with:
|
||||
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
stale-issue-message: 'This issue is stale because it has been open 30 days with no activity. Remove stale label or comment or this will be closed in 5 days'
|
||||
days-before-stale: 30
|
||||
days-before-close: 5
|
||||
- uses: actions/stale@v3
|
||||
with:
|
||||
stale-issue-message: 'This issue is stale because it has been open 30 days with no activity. Remove stale label or comment or this will be closed in 5 days.'
|
||||
days-before-stale: 30
|
||||
days-before-close: 5
|
||||
```
|
||||
|
||||
Configure labels:
|
||||
|
||||
Configure different stale timeouts but never close a pr:
|
||||
|
||||
```yaml
|
||||
name: "Close stale issues"
|
||||
name: 'Close stale issues and PR'
|
||||
on:
|
||||
schedule:
|
||||
- cron: "0 0 * * *"
|
||||
- cron: '30 1 * * *'
|
||||
|
||||
jobs:
|
||||
stale:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/stale@v3
|
||||
with:
|
||||
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
stale-issue-message: 'Stale issue message'
|
||||
stale-pr-message: 'Stale pull request message'
|
||||
stale-issue-label: 'no-issue-activity'
|
||||
exempt-issue-labels: 'awaiting-approval,work-in-progress'
|
||||
stale-pr-label: 'no-pr-activity'
|
||||
exempt-pr-labels: 'awaiting-approval,work-in-progress'
|
||||
only-labels: 'awaiting-feedback,awaiting-answers'
|
||||
- uses: actions/stale@v3
|
||||
with:
|
||||
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
stale-issue-message: 'This issue is stale because it has been open 30 days with no activity. Remove stale label or comment or this will be closed in 5 days.'
|
||||
stale-pr-message: 'This pr is stale because it has been open 45 days with no activity. Remove stale label or comment or this will be closed in 10 days.'
|
||||
close-issue-message: 'This issue was closed because it has been stalled for 5 days with no activity.'
|
||||
days-before-stale: 30
|
||||
days-before-close: 5
|
||||
days-before-pr-close: -1
|
||||
```
|
||||
|
||||
Configure different stale timeouts:
|
||||
|
||||
```yaml
|
||||
name: 'Close stale issues and PRs'
|
||||
on:
|
||||
schedule:
|
||||
- cron: '30 1 * * *'
|
||||
|
||||
jobs:
|
||||
stale:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/stale@v3
|
||||
with:
|
||||
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
stale-issue-message: 'This issue is stale because it has been open 30 days with no activity. Remove stale label or comment or this will be closed in 5 days.'
|
||||
stale-pr-message: 'This pr is stale because it has been open 45 days with no activity. Remove stale label or comment or this will be closed in 10 days.'
|
||||
close-issue-message: 'This issue was closed because it has been stalled for 5 days with no activity.'
|
||||
close-pr-message: 'This pr was closed because it has been stalled for 10 days with no activity.'
|
||||
days-before-issue-stale: 30
|
||||
days-before-pr-stale: 45
|
||||
days-before-issue-close: 5
|
||||
days-before-pr-close: 10
|
||||
```
|
||||
|
||||
Configure labels:
|
||||
|
||||
```yaml
|
||||
name: 'Close stale issues and PRs'
|
||||
on:
|
||||
schedule:
|
||||
- cron: '30 1 * * *'
|
||||
|
||||
jobs:
|
||||
stale:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/stale@v3
|
||||
with:
|
||||
stale-issue-message: 'Stale issue message'
|
||||
stale-pr-message: 'Stale pull request message'
|
||||
stale-issue-label: 'no-issue-activity'
|
||||
exempt-issue-labels: 'awaiting-approval,work-in-progress'
|
||||
stale-pr-label: 'no-pr-activity'
|
||||
exempt-pr-labels: 'awaiting-approval,work-in-progress'
|
||||
only-labels: 'awaiting-feedback,awaiting-answers'
|
||||
```
|
||||
|
||||
Configure the stale action to only stale issue/pr created after the 18th april 2020:
|
||||
|
||||
```yaml
|
||||
name: 'Close stale issues and PRs'
|
||||
on:
|
||||
schedule:
|
||||
- cron: '30 1 * * *'
|
||||
|
||||
jobs:
|
||||
stale:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/stale@v3
|
||||
with:
|
||||
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
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
80
action.yml
80
action.yml
@@ -4,58 +4,110 @@ author: 'GitHub'
|
||||
inputs:
|
||||
repo-token:
|
||||
description: 'Token for the repository. Can be passed in using `{{ secrets.GITHUB_TOKEN }}`.'
|
||||
required: true
|
||||
required: false
|
||||
default: ${{ github.token }}
|
||||
stale-issue-message:
|
||||
description: 'The message to post on the issue when tagging it. If none provided, will not mark issues stale.'
|
||||
required: false
|
||||
stale-pr-message:
|
||||
description: 'The message to post on the pr when tagging it. If none provided, will not mark pull requests stale.'
|
||||
required: false
|
||||
close-issue-message:
|
||||
description: 'The message to post on the issue when closing it. If none provided, will not comment when closing an issue.'
|
||||
required: false
|
||||
close-pr-message:
|
||||
description: 'The message to post on the pr when closing it. If none provided, will not comment when closing a pull requests.'
|
||||
required: false
|
||||
days-before-stale:
|
||||
description: 'The number of days old an issue can be before marking it stale. Set to -1 to never mark issues or pull requests as stale automatically.'
|
||||
default: 60
|
||||
description: 'The number of days old an issue or a pull request can be before marking it stale. Set to -1 to never mark issues or pull requests as stale automatically.'
|
||||
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 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 only the pull requests.'
|
||||
required: false
|
||||
days-before-close:
|
||||
description: 'The number of days to wait to close an issue or pull request after it being marked stale. Set to -1 to never close stale issues.'
|
||||
default: 7
|
||||
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 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 only the pull requests.'
|
||||
required: false
|
||||
stale-issue-label:
|
||||
description: 'The label to apply when an issue is stale.'
|
||||
required: false
|
||||
default: 'Stale'
|
||||
close-issue-label:
|
||||
description: 'The label to apply when an issue is closed.'
|
||||
required: false
|
||||
exempt-issue-labels:
|
||||
description: 'The labels to apply when an issue is exempt from being marked stale. Separate multiple labels with commas (eg. "label1,label2")'
|
||||
description: 'The labels that mean an issue is exempt from being marked stale. Separate multiple labels with commas (eg. "label1,label2")'
|
||||
default: ''
|
||||
required: false
|
||||
stale-pr-label:
|
||||
description: 'The label to apply when a pull request is stale.'
|
||||
default: 'Stale'
|
||||
required: false
|
||||
close-pr-label:
|
||||
description: 'The label to apply when a pull request is closed.'
|
||||
required: false
|
||||
exempt-pr-labels:
|
||||
description: 'The labels to apply when a pull request is exempt from being marked stale. Separate multiple labels with commas (eg. "label1,label2")'
|
||||
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: ''
|
||||
required: false
|
||||
operations-per-run:
|
||||
description: 'The maximum number of operations per run, used to control rate limiting.'
|
||||
default: 30
|
||||
description: 'The maximum number of operations per run, used to control rate limiting (GitHub API CRUD related).'
|
||||
default: '30'
|
||||
required: false
|
||||
remove-stale-when-updated:
|
||||
description: 'Remove stale labels from issues when they are updated or commented on.'
|
||||
default: true
|
||||
default: 'true'
|
||||
required: false
|
||||
debug-only:
|
||||
description: 'Run the processor in debug mode without actually performing any operations on live issues.'
|
||||
default: false
|
||||
default: 'false'
|
||||
required: false
|
||||
ascending:
|
||||
description: 'The order to get issues or pull requests. Defaults to false, which is descending'
|
||||
default: false
|
||||
default: 'false'
|
||||
required: false
|
||||
skip-stale-pr-message:
|
||||
description: 'Skip adding stale message when marking a pull request as stale.'
|
||||
default: false
|
||||
default: 'false'
|
||||
required: false
|
||||
skip-stale-issue-message:
|
||||
description: 'Skip adding stale message when marking an issue as stale.'
|
||||
default: false
|
||||
default: 'false'
|
||||
required: false
|
||||
delete-branch:
|
||||
description: 'Delete the git branch after closing a stale pull request.'
|
||||
default: 'false'
|
||||
required: false
|
||||
start-date:
|
||||
description: 'The date used to skip the stale action on issue/pr created before it (ISO 8601 or RFC 2822).'
|
||||
default: ''
|
||||
required: false
|
||||
runs:
|
||||
using: 'node12'
|
||||
main: 'dist/index.js'
|
||||
|
||||
1320
dist/index.js
vendored
1320
dist/index.js
vendored
File diff suppressed because it is too large
Load Diff
@@ -2,10 +2,10 @@ module.exports = {
|
||||
clearMocks: true,
|
||||
moduleFileExtensions: ['js', 'ts'],
|
||||
testEnvironment: 'node',
|
||||
testMatch: ['**/*.test.ts'],
|
||||
testMatch: ['**/*.test.ts', '**/*.spec.ts'],
|
||||
testRunner: 'jest-circus/runner',
|
||||
transform: {
|
||||
'^.+\\.ts$': 'ts-jest'
|
||||
},
|
||||
verbose: true
|
||||
}
|
||||
};
|
||||
|
||||
5997
package-lock.json
generated
5997
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
40
package.json
40
package.json
@@ -6,9 +6,12 @@
|
||||
"main": "lib/main.js",
|
||||
"scripts": {
|
||||
"build": "tsc",
|
||||
"format": "prettier --write **/*.ts",
|
||||
"format-check": "prettier --check **/*.ts",
|
||||
"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"
|
||||
@@ -27,23 +30,26 @@
|
||||
"dependencies": {
|
||||
"@actions/core": "^1.2.6",
|
||||
"@actions/github": "^4.0.0",
|
||||
"@octokit/rest": "^18.0.4",
|
||||
"semver": "^7.3.2"
|
||||
"@octokit/rest": "^18.0.12",
|
||||
"lodash.deburr": "^4.1.0",
|
||||
"semver": "^7.3.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/semver": "^7.3.1",
|
||||
"@types/jest": "^26.0.10",
|
||||
"@types/node": "^14.10.0",
|
||||
"@typescript-eslint/parser": "^3.10.1",
|
||||
"@vercel/ncc": "^0.24.0",
|
||||
"eslint": "^7.7.0",
|
||||
"@types/jest": "^26.0.20",
|
||||
"@types/lodash.deburr": "^4.1.6",
|
||||
"@types/node": "^14.14.21",
|
||||
"@types/semver": "^7.3.4",
|
||||
"@typescript-eslint/eslint-plugin": "^4.14.0",
|
||||
"@typescript-eslint/parser": "^4.14.0",
|
||||
"@vercel/ncc": "^0.27.0",
|
||||
"eslint": "^7.18.0",
|
||||
"eslint-plugin-github": "^4.0.1",
|
||||
"eslint-plugin-jest": "^23.20.0",
|
||||
"jest": "^24.9.0",
|
||||
"jest-circus": "^26.4.2",
|
||||
"js-yaml": "^3.14.0",
|
||||
"prettier": "^2.1.1",
|
||||
"ts-jest": "^24.2.0",
|
||||
"typescript": "^4.0.2"
|
||||
"eslint-plugin-jest": "^24.1.3",
|
||||
"jest": "^26.6.3",
|
||||
"jest-circus": "^26.6.3",
|
||||
"js-yaml": "^4.0.0",
|
||||
"prettier": "^2.2.1",
|
||||
"ts-jest": "^26.4.4",
|
||||
"typescript": "^4.1.3"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,15 +1,27 @@
|
||||
import * as core from '@actions/core';
|
||||
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 {isLabeled} from './functions/is-labeled';
|
||||
import {isPullRequest} from './functions/is-pull-request';
|
||||
import {shouldMarkWhenStale} from './functions/should-mark-when-stale';
|
||||
import {IsoOrRfcDateString} from './types/iso-or-rfc-date-string';
|
||||
import {wordsToList} from './functions/words-to-list';
|
||||
import {IIssue} from './interfaces/issue';
|
||||
|
||||
export interface Issue {
|
||||
title: string;
|
||||
export interface PullRequest {
|
||||
number: number;
|
||||
updated_at: string;
|
||||
labels: Label[];
|
||||
pull_request: any;
|
||||
state: string;
|
||||
locked: boolean;
|
||||
head: {
|
||||
ref: string;
|
||||
};
|
||||
}
|
||||
|
||||
export interface User {
|
||||
@@ -38,7 +50,11 @@ export interface IssueProcessorOptions {
|
||||
closeIssueMessage: string;
|
||||
closePrMessage: string;
|
||||
daysBeforeStale: number;
|
||||
daysBeforeIssueStale: number; // Could be NaN
|
||||
daysBeforePrStale: number; // Could be NaN
|
||||
daysBeforeClose: number;
|
||||
daysBeforeIssueClose: number; // Could be NaN
|
||||
daysBeforePrClose: number; // Could be NaN
|
||||
staleIssueLabel: string;
|
||||
closeIssueLabel: string;
|
||||
exemptIssueLabels: string;
|
||||
@@ -52,22 +68,37 @@ export interface IssueProcessorOptions {
|
||||
ascending: boolean;
|
||||
skipStaleIssueMessage: boolean;
|
||||
skipStalePrMessage: boolean;
|
||||
deleteBranch: boolean;
|
||||
startDate: IsoOrRfcDateString | undefined; // Should be ISO 8601 or RFC 2822
|
||||
exemptMilestones: string;
|
||||
exemptIssueMilestones: string;
|
||||
exemptPrMilestones: string;
|
||||
}
|
||||
|
||||
/***
|
||||
* Handle processing of issues for staleness/closure.
|
||||
*/
|
||||
export class IssueProcessor {
|
||||
readonly client: any; // need to make this the correct type
|
||||
readonly options: IssueProcessorOptions;
|
||||
private operationsLeft = 0;
|
||||
private static _updatedSince(timestamp: string, num_days: number): boolean {
|
||||
const daysInMillis = 1000 * 60 * 60 * 24 * num_days;
|
||||
const millisSinceLastUpdated =
|
||||
new Date().getTime() - new Date(timestamp).getTime();
|
||||
|
||||
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[] = [];
|
||||
|
||||
constructor(
|
||||
options: IssueProcessorOptions,
|
||||
getActor?: () => Promise<string>,
|
||||
getIssues?: (page: number) => Promise<Issue[]>,
|
||||
listIssueComments?: (
|
||||
issueNumber: number,
|
||||
@@ -79,23 +110,27 @@ export class IssueProcessor {
|
||||
) => Promise<string | undefined>
|
||||
) {
|
||||
this.options = options;
|
||||
this.operationsLeft = options.operationsPerRun;
|
||||
this._operationsLeft = options.operationsPerRun;
|
||||
this.client = getOctokit(options.repoToken);
|
||||
|
||||
if (getActor) {
|
||||
this._getActor = getActor;
|
||||
}
|
||||
|
||||
if (getIssues) {
|
||||
this.getIssues = getIssues;
|
||||
this._getIssues = getIssues;
|
||||
}
|
||||
|
||||
if (listIssueComments) {
|
||||
this.listIssueComments = listIssueComments;
|
||||
this._listIssueComments = listIssueComments;
|
||||
}
|
||||
|
||||
if (getLabelCreationDate) {
|
||||
this.getLabelCreationDate = getLabelCreationDate;
|
||||
this._getLabelCreationDate = getLabelCreationDate;
|
||||
}
|
||||
|
||||
if (this.options.debugOnly) {
|
||||
core.warning(
|
||||
this._logger.warning(
|
||||
'Executing in debug mode. Debug output will be written but no issues will be processed.'
|
||||
);
|
||||
}
|
||||
@@ -103,100 +138,175 @@ 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;
|
||||
const issues: Issue[] = await this._getIssues(page);
|
||||
this._operationsLeft -= 1;
|
||||
|
||||
const actor: string = await this._getActor();
|
||||
|
||||
if (issues.length <= 0) {
|
||||
core.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 isPr = !!issue.pull_request;
|
||||
const issueLogger: IssueLogger = new IssueLogger(issue);
|
||||
|
||||
core.info(
|
||||
`Found issue: issue #${issue.number} - ${issue.title} 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: 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 = IssueProcessor.parseCommaSeparatedString(
|
||||
isPr ? this.options.exemptPrLabels : this.options.exemptIssueLabels
|
||||
);
|
||||
const skipMessage = isPr
|
||||
const skipMessage = issue.isPullRequest
|
||||
? this.options.skipStalePrMessage
|
||||
: this.options.skipStaleIssueMessage;
|
||||
const issueType: string = isPr ? 'pr' : 'issue';
|
||||
const shouldMarkWhenStale = this.options.daysBeforeStale > -1;
|
||||
const issueType: IssueType = getIssueType(issue.isPullRequest);
|
||||
const daysBeforeStale: number = issue.isPullRequest
|
||||
? this._getDaysBeforePrStale()
|
||||
: this._getDaysBeforeIssueStale();
|
||||
|
||||
if (!staleMessage && shouldMarkWhenStale) {
|
||||
core.info(`Skipping ${issueType} due to empty stale message`);
|
||||
if (issue.isPullRequest) {
|
||||
issueLogger.info(`Days before pull request stale: ${daysBeforeStale}`);
|
||||
} else {
|
||||
issueLogger.info(`Days before issue stale: ${daysBeforeStale}`);
|
||||
}
|
||||
|
||||
const shouldMarkAsStale: boolean = shouldMarkWhenStale(daysBeforeStale);
|
||||
|
||||
if (!staleMessage && shouldMarkAsStale) {
|
||||
issueLogger.info(`Skipping ${issueType} due to empty stale message`);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (issue.state === 'closed') {
|
||||
core.info(`Skipping ${issueType} because it is closed`);
|
||||
issueLogger.info(`Skipping ${issueType} because it is closed`);
|
||||
continue; // don't process closed issues
|
||||
}
|
||||
|
||||
if (issue.locked) {
|
||||
core.info(`Skipping ${issueType} because it is locked`);
|
||||
issueLogger.info(`Skipping ${issueType} because it is locked`);
|
||||
continue; // don't process locked issues
|
||||
}
|
||||
|
||||
if (this.options.startDate) {
|
||||
const startDate: Date = new Date(this.options.startDate);
|
||||
const createdAt: Date = new Date(issue.created_at);
|
||||
|
||||
issueLogger.info(
|
||||
`A start date was specified for the ${getHumanizedDate(startDate)} (${
|
||||
this.options.startDate
|
||||
})`
|
||||
);
|
||||
|
||||
// Expecting that GitHub will always set a creation date on the issues and PRs
|
||||
// But you never know!
|
||||
if (!isValidDate(createdAt)) {
|
||||
throw new Error(
|
||||
`Invalid issue field: "created_at". Expected a valid date`
|
||||
);
|
||||
}
|
||||
|
||||
issueLogger.info(
|
||||
`Issue created the ${getHumanizedDate(createdAt)} (${
|
||||
issue.created_at
|
||||
})`
|
||||
);
|
||||
|
||||
if (!isDateMoreRecentThan(createdAt, startDate)) {
|
||||
issueLogger.info(
|
||||
`Skipping ${issueType} because it was created before the specified start date`
|
||||
);
|
||||
|
||||
continue; // don't process issues which were created before the start date
|
||||
}
|
||||
}
|
||||
|
||||
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: string) =>
|
||||
IssueProcessor.isLabeled(issue, exemptLabel)
|
||||
exemptLabels.some((exemptLabel: Readonly<string>): boolean =>
|
||||
isLabeled(issue, exemptLabel)
|
||||
)
|
||||
) {
|
||||
core.info(`Skipping ${issueType} because it has an exempt label`);
|
||||
if (issue.isStale) {
|
||||
issueLogger.info(`An exempt label was added after the stale label.`);
|
||||
await this._removeStaleLabel(issue, staleLabel);
|
||||
}
|
||||
|
||||
issueLogger.info(
|
||||
`Skipping ${issueType} because it has an exempt label`
|
||||
);
|
||||
continue; // don't process exempt issues
|
||||
}
|
||||
|
||||
// does this issue have a stale label?
|
||||
let isStale = IssueProcessor.isLabeled(issue, staleLabel);
|
||||
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(
|
||||
const shouldBeStale = !IssueProcessor._updatedSince(
|
||||
issue.updated_at,
|
||||
this.options.daysBeforeStale
|
||||
daysBeforeStale
|
||||
);
|
||||
|
||||
// determine if this issue needs to be marked stale first
|
||||
if (!isStale && shouldBeStale && shouldMarkWhenStale) {
|
||||
core.info(
|
||||
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
|
||||
await this._markStale(issue, staleMessage, staleLabel, skipMessage);
|
||||
issue.isStale = true; // this issue is now considered stale
|
||||
} else if (!issue.isStale) {
|
||||
issueLogger.info(
|
||||
`Not marking as stale: shouldBeStale=${shouldBeStale}, shouldMarkAsStale=${shouldMarkAsStale}`
|
||||
);
|
||||
}
|
||||
|
||||
// process the issue if it was marked stale
|
||||
if (isStale) {
|
||||
core.info(`Found a stale ${issueType}`);
|
||||
await this.processStaleIssue(
|
||||
if (issue.isStale) {
|
||||
issueLogger.info(`Found a stale ${issueType}`);
|
||||
await this._processStaleIssue(
|
||||
issue,
|
||||
issueType,
|
||||
staleLabel,
|
||||
actor,
|
||||
closeMessage,
|
||||
closeLabel
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (this.operationsLeft <= 0) {
|
||||
core.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;
|
||||
}
|
||||
|
||||
@@ -205,62 +315,88 @@ export class IssueProcessor {
|
||||
}
|
||||
|
||||
// handle all of the stale issue logic when we find a stale issue
|
||||
private async processStaleIssue(
|
||||
private async _processStaleIssue(
|
||||
issue: Issue,
|
||||
issueType: string,
|
||||
issueType: IssueType,
|
||||
staleLabel: string,
|
||||
actor: string,
|
||||
closeMessage?: string,
|
||||
closeLabel?: string
|
||||
) {
|
||||
const issueLogger: IssueLogger = new IssueLogger(issue);
|
||||
const markedStaleOn: string =
|
||||
(await this.getLabelCreationDate(issue, staleLabel)) || issue.updated_at;
|
||||
core.info(`Issue #${issue.number} marked stale on: ${markedStaleOn}`);
|
||||
|
||||
const issueHasComments: boolean = await this.hasCommentsSince(
|
||||
issue,
|
||||
markedStaleOn
|
||||
(await this._getLabelCreationDate(issue, staleLabel)) || issue.updated_at;
|
||||
issueLogger.info(
|
||||
`Issue #${issue.number} marked stale on: ${markedStaleOn}`
|
||||
);
|
||||
core.info(
|
||||
|
||||
const issueHasComments: boolean = await this._hasCommentsSince(
|
||||
issue,
|
||||
markedStaleOn,
|
||||
actor
|
||||
);
|
||||
issueLogger.info(
|
||||
`Issue #${issue.number} has been commented on: ${issueHasComments}`
|
||||
);
|
||||
|
||||
const issueHasUpdate: boolean = IssueProcessor.updatedSince(
|
||||
const isPr: boolean = isPullRequest(issue);
|
||||
const daysBeforeClose: number = isPr
|
||||
? this._getDaysBeforePrClose()
|
||||
: this._getDaysBeforeIssueClose();
|
||||
|
||||
if (isPr) {
|
||||
issueLogger.info(`Days before pull request close: ${daysBeforeClose}`);
|
||||
} else {
|
||||
issueLogger.info(`Days before issue close: ${daysBeforeClose}`);
|
||||
}
|
||||
|
||||
const issueHasUpdate: boolean = IssueProcessor._updatedSince(
|
||||
issue.updated_at,
|
||||
this.options.daysBeforeClose
|
||||
daysBeforeClose
|
||||
);
|
||||
issueLogger.info(
|
||||
`Issue #${issue.number} has been updated: ${issueHasUpdate}`
|
||||
);
|
||||
core.info(`Issue #${issue.number} has been updated: ${issueHasUpdate}`);
|
||||
|
||||
// should we un-stale this issue?
|
||||
if (this.options.removeStaleWhenUpdated && issueHasComments) {
|
||||
core.info(
|
||||
`Issue #${issue.number} is no longer stale. Removing stale label.`
|
||||
);
|
||||
await this.removeLabel(issue, staleLabel);
|
||||
await this._removeStaleLabel(issue, staleLabel);
|
||||
}
|
||||
|
||||
// now start closing logic
|
||||
if (this.options.daysBeforeClose < 0) {
|
||||
if (daysBeforeClose < 0) {
|
||||
return; // nothing to do because we aren't closing stale issues
|
||||
}
|
||||
|
||||
if (!issueHasComments && !issueHasUpdate) {
|
||||
core.info(
|
||||
issueLogger.info(
|
||||
`Closing ${issueType} because it was last updated on ${issue.updated_at}`
|
||||
);
|
||||
await this.closeIssue(issue, closeMessage, closeLabel);
|
||||
await this._closeIssue(issue, closeMessage, closeLabel);
|
||||
|
||||
if (this.options.deleteBranch && issue.pull_request) {
|
||||
issueLogger.info(
|
||||
`Deleting branch for #${issue.number} as delete-branch option was specified`
|
||||
);
|
||||
await this._deleteBranch(issue);
|
||||
this.deletedBranchIssues.push(issue);
|
||||
}
|
||||
} else {
|
||||
core.info(
|
||||
`Stale ${issueType} is not old enough to close yet (hasComments? ${issueHasComments}, hasUpdate? ${issueHasUpdate}`
|
||||
issueLogger.info(
|
||||
`Stale ${issueType} is not old enough to close yet (hasComments? ${issueHasComments}, hasUpdate? ${issueHasUpdate})`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// checks to see if a given issue is still stale (has had activity on it)
|
||||
private async hasCommentsSince(
|
||||
private async _hasCommentsSince(
|
||||
issue: Issue,
|
||||
sinceDate: string
|
||||
sinceDate: string,
|
||||
actor: string
|
||||
): Promise<boolean> {
|
||||
core.info(
|
||||
const issueLogger: IssueLogger = new IssueLogger(issue);
|
||||
|
||||
issueLogger.info(
|
||||
`Checking for comments on issue #${issue.number} since ${sinceDate}`
|
||||
);
|
||||
|
||||
@@ -269,15 +405,14 @@ export class IssueProcessor {
|
||||
}
|
||||
|
||||
// find any comments since the date
|
||||
const comments = await this.listIssueComments(issue.number, sinceDate);
|
||||
const comments = await this._listIssueComments(issue.number, sinceDate);
|
||||
|
||||
const filteredComments = comments.filter(
|
||||
comment =>
|
||||
comment.user.type === 'User' && comment.user.login !== context.actor
|
||||
comment => comment.user.type === 'User' && comment.user.login !== actor
|
||||
);
|
||||
|
||||
core.info(
|
||||
`Comments not made by ${context.actor} or another bot: ${filteredComments.length}`
|
||||
issueLogger.info(
|
||||
`Comments not made by actor or another bot: ${filteredComments.length}`
|
||||
);
|
||||
|
||||
// if there are any user comments returned
|
||||
@@ -285,7 +420,7 @@ export class IssueProcessor {
|
||||
}
|
||||
|
||||
// grab comments for an issue since a given date
|
||||
private async listIssueComments(
|
||||
private async _listIssueComments(
|
||||
issueNumber: number,
|
||||
sinceDate: string
|
||||
): Promise<Comment[]> {
|
||||
@@ -299,13 +434,25 @@ export class IssueProcessor {
|
||||
});
|
||||
return comments.data;
|
||||
} catch (error) {
|
||||
core.error(`List issue comments error: ${error.message}`);
|
||||
this._logger.error(`List issue comments error: ${error.message}`);
|
||||
return Promise.resolve([]);
|
||||
}
|
||||
}
|
||||
|
||||
// grab issues from github in baches of 100
|
||||
private async getIssues(page: number): Promise<Issue[]> {
|
||||
// get the actor from the GitHub token or context
|
||||
private async _getActor(): Promise<string> {
|
||||
let actor;
|
||||
try {
|
||||
actor = await this.client.users.getAuthenticated();
|
||||
} catch (error) {
|
||||
return context.actor;
|
||||
}
|
||||
|
||||
return actor.data.login;
|
||||
}
|
||||
|
||||
// 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;
|
||||
type OctoKitIssueList = GetResponseTypeFromEndpointMethod<typeof endpoint>;
|
||||
@@ -322,25 +469,30 @@ export class IssueProcessor {
|
||||
page
|
||||
}
|
||||
);
|
||||
return issueResult.data;
|
||||
|
||||
return issueResult.data.map(
|
||||
(issue: Readonly<IIssue>): Issue => new Issue(this.options, issue)
|
||||
);
|
||||
} catch (error) {
|
||||
core.error(`Get issues for repo error: ${error.message}`);
|
||||
this._logger.error(`Get issues for repo error: ${error.message}`);
|
||||
return Promise.resolve([]);
|
||||
}
|
||||
}
|
||||
|
||||
// Mark an issue as stale with a comment and a label
|
||||
private async markStale(
|
||||
private async _markStale(
|
||||
issue: Issue,
|
||||
staleMessage: string,
|
||||
staleLabel: string,
|
||||
skipMessage: boolean
|
||||
): Promise<void> {
|
||||
core.info(`Marking issue #${issue.number} - ${issue.title} as stale`);
|
||||
const issueLogger: IssueLogger = new 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
|
||||
@@ -360,7 +512,7 @@ export class IssueProcessor {
|
||||
body: staleMessage
|
||||
});
|
||||
} catch (error) {
|
||||
core.error(`Error creating a comment: ${error.message}`);
|
||||
issueLogger.error(`Error creating a comment: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -372,23 +524,23 @@ export class IssueProcessor {
|
||||
labels: [staleLabel]
|
||||
});
|
||||
} catch (error) {
|
||||
core.error(`Error adding a label: ${error.message}`);
|
||||
issueLogger.error(`Error adding a label: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
// Close an issue based on staleness
|
||||
private async closeIssue(
|
||||
private async _closeIssue(
|
||||
issue: Issue,
|
||||
closeMessage?: string,
|
||||
closeLabel?: string
|
||||
): Promise<void> {
|
||||
core.info(
|
||||
`Closing issue #${issue.number} - ${issue.title} for being stale`
|
||||
);
|
||||
const issueLogger: IssueLogger = new 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;
|
||||
@@ -403,7 +555,7 @@ export class IssueProcessor {
|
||||
body: closeMessage
|
||||
});
|
||||
} catch (error) {
|
||||
core.error(`Error creating a comment: ${error.message}`);
|
||||
issueLogger.error(`Error creating a comment: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -416,7 +568,7 @@ export class IssueProcessor {
|
||||
labels: [closeLabel]
|
||||
});
|
||||
} catch (error) {
|
||||
core.error(`Error adding a label: ${error.message}`);
|
||||
issueLogger.error(`Error adding a label: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -428,20 +580,83 @@ export class IssueProcessor {
|
||||
state: 'closed'
|
||||
});
|
||||
} catch (error) {
|
||||
core.error(`Error updating an issue: ${error.message}`);
|
||||
issueLogger.error(`Error updating an issue: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
private async _getPullRequest(
|
||||
issue: Issue
|
||||
): Promise<PullRequest | undefined> {
|
||||
const issueLogger: IssueLogger = new IssueLogger(issue);
|
||||
this._operationsLeft -= 1;
|
||||
|
||||
try {
|
||||
const pullRequest = await this.client.pulls.get({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
pull_number: issue.number
|
||||
});
|
||||
|
||||
return pullRequest.data;
|
||||
} catch (error) {
|
||||
issueLogger.error(
|
||||
`Error getting pull request ${issue.number}: ${error.message}`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Delete the branch on closed pull request
|
||||
private async _deleteBranch(issue: Issue): Promise<void> {
|
||||
const issueLogger: IssueLogger = new IssueLogger(issue);
|
||||
|
||||
issueLogger.info(
|
||||
`Delete branch from closed issue #${issue.number} - ${issue.title}`
|
||||
);
|
||||
|
||||
if (this.options.debugOnly) {
|
||||
return;
|
||||
}
|
||||
|
||||
const pullRequest = await this._getPullRequest(issue);
|
||||
|
||||
if (!pullRequest) {
|
||||
issueLogger.info(
|
||||
`Not deleting branch as pull request not found for issue ${issue.number}`
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const branch = pullRequest.head.ref;
|
||||
issueLogger.info(
|
||||
`Deleting branch ${branch} from closed issue #${issue.number}`
|
||||
);
|
||||
|
||||
this._operationsLeft -= 1;
|
||||
|
||||
try {
|
||||
await this.client.git.deleteRef({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
ref: `heads/${branch}`
|
||||
});
|
||||
} catch (error) {
|
||||
issueLogger.error(
|
||||
`Error deleting branch ${branch} from issue #${issue.number}: ${error.message}`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Remove a label from an issue
|
||||
private async removeLabel(issue: Issue, label: string): Promise<void> {
|
||||
core.info(
|
||||
`Removing label ${label} from issue #${issue.number} - ${issue.title}`
|
||||
);
|
||||
private async _removeLabel(issue: Issue, label: string): Promise<void> {
|
||||
const issueLogger: IssueLogger = new 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;
|
||||
}
|
||||
@@ -451,22 +666,24 @@ export class IssueProcessor {
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: issue.number,
|
||||
name: encodeURIComponent(label) // A label can have a "?" in the name
|
||||
name: label
|
||||
});
|
||||
} catch (error) {
|
||||
core.error(`Error removing a label: ${error.message}`);
|
||||
issueLogger.error(`Error removing a label: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
// returns the creation date of a given label on an issue (or nothing if no label existed)
|
||||
///see https://developer.github.com/v3/activity/events/
|
||||
private async getLabelCreationDate(
|
||||
private async _getLabelCreationDate(
|
||||
issue: Issue,
|
||||
label: string
|
||||
): Promise<string | undefined> {
|
||||
core.info(`Checking for label ${label} on issue #${issue.number}`);
|
||||
const issueLogger: IssueLogger = new IssueLogger(issue);
|
||||
|
||||
this.operationsLeft -= 1;
|
||||
issueLogger.info(`Checking for label on issue #${issue.number}`);
|
||||
|
||||
this._operationsLeft -= 1;
|
||||
|
||||
const options = this.client.issues.listEvents.endpoint.merge({
|
||||
owner: context.repo.owner,
|
||||
@@ -490,24 +707,40 @@ export class IssueProcessor {
|
||||
return staleLabeledEvent.created_at;
|
||||
}
|
||||
|
||||
private static isLabeled(issue: Issue, label: string): boolean {
|
||||
const labelComparer: (l: Label) => boolean = l =>
|
||||
label.localeCompare(l.name, undefined, {sensitivity: 'accent'}) === 0;
|
||||
return issue.labels.filter(labelComparer).length > 0;
|
||||
private _getDaysBeforeIssueStale(): number {
|
||||
return isNaN(this.options.daysBeforeIssueStale)
|
||||
? this.options.daysBeforeStale
|
||||
: this.options.daysBeforeIssueStale;
|
||||
}
|
||||
|
||||
private static updatedSince(timestamp: string, num_days: number): boolean {
|
||||
const daysInMillis = 1000 * 60 * 60 * 24 * num_days;
|
||||
const millisSinceLastUpdated =
|
||||
new Date().getTime() - new Date(timestamp).getTime();
|
||||
|
||||
return millisSinceLastUpdated <= daysInMillis;
|
||||
private _getDaysBeforePrStale(): number {
|
||||
return isNaN(this.options.daysBeforePrStale)
|
||||
? this.options.daysBeforeStale
|
||||
: this.options.daysBeforePrStale;
|
||||
}
|
||||
|
||||
private static parseCommaSeparatedString(s: string): string[] {
|
||||
// String.prototype.split defaults to [''] when called on an empty string
|
||||
// In this case, we'd prefer to just return an empty array indicating no labels
|
||||
if (!s.length) return [];
|
||||
return s.split(',').map(l => l.trim());
|
||||
private _getDaysBeforeIssueClose(): number {
|
||||
return isNaN(this.options.daysBeforeIssueClose)
|
||||
? this.options.daysBeforeClose
|
||||
: this.options.daysBeforeIssueClose;
|
||||
}
|
||||
|
||||
private _getDaysBeforePrClose(): number {
|
||||
return isNaN(this.options.daysBeforePrClose)
|
||||
? this.options.daysBeforeClose
|
||||
: this.options.daysBeforePrClose;
|
||||
}
|
||||
|
||||
private async _removeStaleLabel(
|
||||
issue: Issue,
|
||||
staleLabel: Readonly<string>
|
||||
): Promise<void> {
|
||||
const issueLogger: IssueLogger = new IssueLogger(issue);
|
||||
|
||||
issueLogger.info(
|
||||
`Issue #${issue.number} is no longer stale. Removing stale label.`
|
||||
);
|
||||
|
||||
return this._removeLabel(issue, staleLabel);
|
||||
}
|
||||
}
|
||||
|
||||
208
src/classes/issue.spec.ts
Normal file
208
src/classes/issue.spec.ts
Normal file
@@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
48
src/classes/issue.ts
Normal file
48
src/classes/issue.ts
Normal file
@@ -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;
|
||||
}
|
||||
}
|
||||
78
src/classes/loggers/issue-logger.spec.ts
Normal file
78
src/classes/loggers/issue-logger.spec.ts
Normal file
@@ -0,0 +1,78 @@
|
||||
import {Issue} from '../issue';
|
||||
import {IssueLogger} from './issue-logger';
|
||||
import * as core from '@actions/core';
|
||||
|
||||
describe('IssueLogger', (): void => {
|
||||
let issue: Issue;
|
||||
let issueLogger: IssueLogger;
|
||||
|
||||
beforeEach((): void => {
|
||||
issue = {
|
||||
number: 8
|
||||
} as Issue;
|
||||
issueLogger = new IssueLogger(issue);
|
||||
});
|
||||
|
||||
describe('warning()', (): void => {
|
||||
let message: string;
|
||||
|
||||
let coreWarningSpy: jest.SpyInstance;
|
||||
|
||||
beforeEach((): void => {
|
||||
message = 'dummy-message';
|
||||
|
||||
coreWarningSpy = jest.spyOn(core, 'warning').mockImplementation();
|
||||
});
|
||||
|
||||
it('should log a warning with the given message and with the issue number as prefix', (): void => {
|
||||
expect.assertions(2);
|
||||
|
||||
issueLogger.warning(message);
|
||||
|
||||
expect(coreWarningSpy).toHaveBeenCalledTimes(1);
|
||||
expect(coreWarningSpy).toHaveBeenCalledWith('[#8] dummy-message');
|
||||
});
|
||||
});
|
||||
|
||||
describe('info()', (): void => {
|
||||
let message: string;
|
||||
|
||||
let coreInfoSpy: jest.SpyInstance;
|
||||
|
||||
beforeEach((): void => {
|
||||
message = 'dummy-message';
|
||||
|
||||
coreInfoSpy = jest.spyOn(core, 'info').mockImplementation();
|
||||
});
|
||||
|
||||
it('should log an information with the given message and with the issue number as prefix', (): void => {
|
||||
expect.assertions(2);
|
||||
|
||||
issueLogger.info(message);
|
||||
|
||||
expect(coreInfoSpy).toHaveBeenCalledTimes(1);
|
||||
expect(coreInfoSpy).toHaveBeenCalledWith('[#8] dummy-message');
|
||||
});
|
||||
});
|
||||
|
||||
describe('error()', (): void => {
|
||||
let message: string;
|
||||
|
||||
let coreErrorSpy: jest.SpyInstance;
|
||||
|
||||
beforeEach((): void => {
|
||||
message = 'dummy-message';
|
||||
|
||||
coreErrorSpy = jest.spyOn(core, 'error').mockImplementation();
|
||||
});
|
||||
|
||||
it('should log an error with the given message and with the issue number as prefix', (): void => {
|
||||
expect.assertions(2);
|
||||
|
||||
issueLogger.error(message);
|
||||
|
||||
expect(coreErrorSpy).toHaveBeenCalledTimes(1);
|
||||
expect(coreErrorSpy).toHaveBeenCalledWith('[#8] dummy-message');
|
||||
});
|
||||
});
|
||||
});
|
||||
31
src/classes/loggers/issue-logger.ts
Normal file
31
src/classes/loggers/issue-logger.ts
Normal file
@@ -0,0 +1,31 @@
|
||||
import * as core from '@actions/core';
|
||||
import {Issue} from '../issue';
|
||||
import {Logger} from './logger';
|
||||
|
||||
export class IssueLogger implements Logger {
|
||||
private readonly _issue: Issue;
|
||||
|
||||
constructor(issue: Issue) {
|
||||
this._issue = issue;
|
||||
}
|
||||
|
||||
warning(message: Readonly<string>): void {
|
||||
core.warning(this._prefixWithIssueNumber(message));
|
||||
}
|
||||
|
||||
info(message: Readonly<string>): void {
|
||||
core.info(this._prefixWithIssueNumber(message));
|
||||
}
|
||||
|
||||
error(message: Readonly<string>): void {
|
||||
core.error(this._prefixWithIssueNumber(message));
|
||||
}
|
||||
|
||||
private _prefixWithIssueNumber(message: Readonly<string>): string {
|
||||
return `[#${this._getIssueNumber()}] ${message}`;
|
||||
}
|
||||
|
||||
private _getIssueNumber(): number {
|
||||
return this._issue.number;
|
||||
}
|
||||
}
|
||||
73
src/classes/loggers/logger.spec.ts
Normal file
73
src/classes/loggers/logger.spec.ts
Normal file
@@ -0,0 +1,73 @@
|
||||
import {Logger} from './logger';
|
||||
import * as core from '@actions/core';
|
||||
|
||||
describe('Logger', (): void => {
|
||||
let logger: Logger;
|
||||
|
||||
beforeEach((): void => {
|
||||
logger = new Logger();
|
||||
});
|
||||
|
||||
describe('warning()', (): void => {
|
||||
let message: string;
|
||||
|
||||
let coreWarningSpy: jest.SpyInstance;
|
||||
|
||||
beforeEach((): void => {
|
||||
message = 'dummy-message';
|
||||
|
||||
coreWarningSpy = jest.spyOn(core, 'warning').mockImplementation();
|
||||
});
|
||||
|
||||
it('should log a warning with the given message', (): void => {
|
||||
expect.assertions(2);
|
||||
|
||||
logger.warning(message);
|
||||
|
||||
expect(coreWarningSpy).toHaveBeenCalledTimes(1);
|
||||
expect(coreWarningSpy).toHaveBeenCalledWith('dummy-message');
|
||||
});
|
||||
});
|
||||
|
||||
describe('info()', (): void => {
|
||||
let message: string;
|
||||
|
||||
let coreInfoSpy: jest.SpyInstance;
|
||||
|
||||
beforeEach((): void => {
|
||||
message = 'dummy-message';
|
||||
|
||||
coreInfoSpy = jest.spyOn(core, 'info').mockImplementation();
|
||||
});
|
||||
|
||||
it('should log an information with the given message', (): void => {
|
||||
expect.assertions(2);
|
||||
|
||||
logger.info(message);
|
||||
|
||||
expect(coreInfoSpy).toHaveBeenCalledTimes(1);
|
||||
expect(coreInfoSpy).toHaveBeenCalledWith('dummy-message');
|
||||
});
|
||||
});
|
||||
|
||||
describe('error()', (): void => {
|
||||
let message: string;
|
||||
|
||||
let coreErrorSpy: jest.SpyInstance;
|
||||
|
||||
beforeEach((): void => {
|
||||
message = 'dummy-message';
|
||||
|
||||
coreErrorSpy = jest.spyOn(core, 'error').mockImplementation();
|
||||
});
|
||||
|
||||
it('should log an error with the given message', (): void => {
|
||||
expect.assertions(2);
|
||||
|
||||
logger.error(message);
|
||||
|
||||
expect(coreErrorSpy).toHaveBeenCalledTimes(1);
|
||||
expect(coreErrorSpy).toHaveBeenCalledWith('dummy-message');
|
||||
});
|
||||
});
|
||||
});
|
||||
15
src/classes/loggers/logger.ts
Normal file
15
src/classes/loggers/logger.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import * as core from '@actions/core';
|
||||
|
||||
export class Logger {
|
||||
warning(message: Readonly<string>): void {
|
||||
core.warning(message);
|
||||
}
|
||||
|
||||
info(message: Readonly<string>): void {
|
||||
core.info(message);
|
||||
}
|
||||
|
||||
error(message: Readonly<string>): void {
|
||||
core.error(message);
|
||||
}
|
||||
}
|
||||
596
src/classes/milestones.spec.ts
Normal file
596
src/classes/milestones.spec.ts
Normal file
@@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
59
src/classes/milestones.ts
Normal file
59
src/classes/milestones.ts
Normal file
@@ -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)
|
||||
);
|
||||
}
|
||||
}
|
||||
4
src/enums/issue-type.ts
Normal file
4
src/enums/issue-type.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
export enum IssueType {
|
||||
Issue = 'issue',
|
||||
PullRequest = 'pr'
|
||||
}
|
||||
33
src/functions/dates/get-humanized-date.spec.ts
Normal file
33
src/functions/dates/get-humanized-date.spec.ts
Normal file
@@ -0,0 +1,33 @@
|
||||
import {getHumanizedDate} from './get-humanized-date';
|
||||
|
||||
describe('getHumanizedDate()', (): void => {
|
||||
let date: Date;
|
||||
|
||||
describe('when the given date is the 1st of april 2020', (): void => {
|
||||
beforeEach((): void => {
|
||||
date = new Date(2020, 3, 1);
|
||||
});
|
||||
|
||||
it('should return the date formatted as DD-MM-YYYY', (): void => {
|
||||
expect.assertions(1);
|
||||
|
||||
const result = getHumanizedDate(date);
|
||||
|
||||
expect(result).toStrictEqual('01-04-2020');
|
||||
});
|
||||
});
|
||||
|
||||
describe('when the given date is the 18st of december 2020', (): void => {
|
||||
beforeEach((): void => {
|
||||
date = new Date(2020, 11, 18);
|
||||
});
|
||||
|
||||
it('should return the date formatted as DD-MM-YYYY', (): void => {
|
||||
expect.assertions(1);
|
||||
|
||||
const result = getHumanizedDate(date);
|
||||
|
||||
expect(result).toStrictEqual('18-12-2020');
|
||||
});
|
||||
});
|
||||
});
|
||||
17
src/functions/dates/get-humanized-date.ts
Normal file
17
src/functions/dates/get-humanized-date.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
import {HumanizedDate} from '../../types/humanized-date';
|
||||
|
||||
export function getHumanizedDate(date: Readonly<Date>): HumanizedDate {
|
||||
const year: number = date.getFullYear();
|
||||
let month = `${date.getMonth() + 1}`;
|
||||
let day = `${date.getDate()}`;
|
||||
|
||||
if (month.length < 2) {
|
||||
month = `0${month}`;
|
||||
}
|
||||
|
||||
if (day.length < 2) {
|
||||
day = `0${day}`;
|
||||
}
|
||||
|
||||
return [day, month, year].join('-');
|
||||
}
|
||||
51
src/functions/dates/is-date-more-recent-than.spec.ts
Normal file
51
src/functions/dates/is-date-more-recent-than.spec.ts
Normal file
@@ -0,0 +1,51 @@
|
||||
import {isDateMoreRecentThan} from './is-date-more-recent-than';
|
||||
|
||||
describe('isDateMoreRecentThan()', (): void => {
|
||||
let date: Date;
|
||||
let comparedDate: Date;
|
||||
|
||||
describe('when the given date is older than the compared date', (): void => {
|
||||
beforeEach((): void => {
|
||||
date = new Date(2020, 0, 20);
|
||||
comparedDate = new Date(2021, 0, 20);
|
||||
});
|
||||
|
||||
it('should return false', (): void => {
|
||||
expect.assertions(1);
|
||||
|
||||
const result = isDateMoreRecentThan(date, comparedDate);
|
||||
|
||||
expect(result).toStrictEqual(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when the given date is equal to the compared date', (): void => {
|
||||
beforeEach((): void => {
|
||||
date = new Date(2020, 0, 20);
|
||||
comparedDate = new Date(2020, 0, 20);
|
||||
});
|
||||
|
||||
it('should return false', (): void => {
|
||||
expect.assertions(1);
|
||||
|
||||
const result = isDateMoreRecentThan(date, comparedDate);
|
||||
|
||||
expect(result).toStrictEqual(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when the given date is more recent than the compared date', (): void => {
|
||||
beforeEach((): void => {
|
||||
date = new Date(2021, 0, 20);
|
||||
comparedDate = new Date(2020, 0, 20);
|
||||
});
|
||||
|
||||
it('should return true', (): void => {
|
||||
expect.assertions(1);
|
||||
|
||||
const result = isDateMoreRecentThan(date, comparedDate);
|
||||
|
||||
expect(result).toStrictEqual(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
6
src/functions/dates/is-date-more-recent-than.ts
Normal file
6
src/functions/dates/is-date-more-recent-than.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
export function isDateMoreRecentThan(
|
||||
date: Readonly<Date>,
|
||||
comparedDate: Readonly<Date>
|
||||
): boolean {
|
||||
return date > comparedDate;
|
||||
}
|
||||
61
src/functions/dates/is-valid-date.spec.ts
Normal file
61
src/functions/dates/is-valid-date.spec.ts
Normal file
@@ -0,0 +1,61 @@
|
||||
import {isValidDate} from './is-valid-date';
|
||||
|
||||
describe('isValidDate()', (): void => {
|
||||
let date: Date;
|
||||
|
||||
describe('when the given date is an invalid date', (): void => {
|
||||
beforeEach((): void => {
|
||||
date = new Date('16-04-1994');
|
||||
});
|
||||
|
||||
it('should return false', (): void => {
|
||||
expect.assertions(1);
|
||||
|
||||
const result = isValidDate(date);
|
||||
|
||||
expect(result).toStrictEqual(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when the given date is a new date', (): void => {
|
||||
beforeEach((): void => {
|
||||
date = new Date();
|
||||
});
|
||||
|
||||
it('should return true', (): void => {
|
||||
expect.assertions(1);
|
||||
|
||||
const result = isValidDate(date);
|
||||
|
||||
expect(result).toStrictEqual(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when the given date is an ISO and valid date', (): void => {
|
||||
beforeEach((): void => {
|
||||
date = new Date('2011-04-22T13:33:48Z');
|
||||
});
|
||||
|
||||
it('should return true', (): void => {
|
||||
expect.assertions(1);
|
||||
|
||||
const result = isValidDate(date);
|
||||
|
||||
expect(result).toStrictEqual(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when the given date is an ISO with ms and valid date', (): void => {
|
||||
beforeEach((): void => {
|
||||
date = new Date('2011-10-05T14:48:00.000Z');
|
||||
});
|
||||
|
||||
it('should return true', (): void => {
|
||||
expect.assertions(1);
|
||||
|
||||
const result = isValidDate(date);
|
||||
|
||||
expect(result).toStrictEqual(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
18
src/functions/dates/is-valid-date.ts
Normal file
18
src/functions/dates/is-valid-date.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
/**
|
||||
* @description
|
||||
* Check if a date is valid
|
||||
*
|
||||
* @see
|
||||
* https://stackoverflow.com/a/1353711/4440414
|
||||
*
|
||||
* @param {Readonly<Date>} date The date to check
|
||||
*
|
||||
* @returns {boolean} true when the given date is valid
|
||||
*/
|
||||
export function isValidDate(date: Readonly<Date>): boolean {
|
||||
if (Object.prototype.toString.call(date) === '[object Date]') {
|
||||
return !isNaN(date.getTime());
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
33
src/functions/get-issue-type.spec.ts
Normal file
33
src/functions/get-issue-type.spec.ts
Normal file
@@ -0,0 +1,33 @@
|
||||
import {getIssueType} from './get-issue-type';
|
||||
|
||||
describe('getIssueType()', (): void => {
|
||||
let isPullRequest: boolean;
|
||||
|
||||
describe('when the issue is a not pull request', (): void => {
|
||||
beforeEach((): void => {
|
||||
isPullRequest = false;
|
||||
});
|
||||
|
||||
it('should return that the issue is really an issue', (): void => {
|
||||
expect.assertions(1);
|
||||
|
||||
const result = getIssueType(isPullRequest);
|
||||
|
||||
expect(result).toStrictEqual('issue');
|
||||
});
|
||||
});
|
||||
|
||||
describe('when the issue is a pull request', (): void => {
|
||||
beforeEach((): void => {
|
||||
isPullRequest = true;
|
||||
});
|
||||
|
||||
it('should return that the issue is a pull request', (): void => {
|
||||
expect.assertions(1);
|
||||
|
||||
const result = getIssueType(isPullRequest);
|
||||
|
||||
expect(result).toStrictEqual('pr');
|
||||
});
|
||||
});
|
||||
});
|
||||
5
src/functions/get-issue-type.ts
Normal file
5
src/functions/get-issue-type.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
import {IssueType} from '../enums/issue-type';
|
||||
|
||||
export function getIssueType(isPullRequest: Readonly<boolean>): IssueType {
|
||||
return isPullRequest ? IssueType.PullRequest : IssueType.Issue;
|
||||
}
|
||||
187
src/functions/is-labeled.spec.ts
Normal file
187
src/functions/is-labeled.spec.ts
Normal file
@@ -0,0 +1,187 @@
|
||||
import {Issue} from '../classes/issue';
|
||||
import {isLabeled} from './is-labeled';
|
||||
|
||||
describe('isLabeled()', (): void => {
|
||||
let issue: Issue;
|
||||
let label: string;
|
||||
|
||||
describe('when the given issue contains no label', (): void => {
|
||||
beforeEach((): void => {
|
||||
issue = ({
|
||||
labels: []
|
||||
} as unknown) as Issue;
|
||||
});
|
||||
|
||||
describe('when the given label is a simple label', (): void => {
|
||||
beforeEach((): void => {
|
||||
label = 'label';
|
||||
});
|
||||
|
||||
it('should return false', (): void => {
|
||||
expect.assertions(1);
|
||||
|
||||
const result = isLabeled(issue, label);
|
||||
|
||||
expect(result).toStrictEqual(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('when the given issue contains a simple label', (): void => {
|
||||
beforeEach((): void => {
|
||||
issue = {
|
||||
labels: [
|
||||
{
|
||||
name: 'label'
|
||||
}
|
||||
]
|
||||
} as Issue;
|
||||
});
|
||||
|
||||
describe('when the given label is a simple label', (): void => {
|
||||
beforeEach((): void => {
|
||||
label = 'label';
|
||||
});
|
||||
|
||||
it('should return true', (): void => {
|
||||
expect.assertions(1);
|
||||
|
||||
const result = isLabeled(issue, label);
|
||||
|
||||
expect(result).toStrictEqual(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('when the given issue contains a kebab case label', (): void => {
|
||||
beforeEach((): void => {
|
||||
issue = {
|
||||
labels: [
|
||||
{
|
||||
name: 'kebab-case-label'
|
||||
}
|
||||
]
|
||||
} as Issue;
|
||||
});
|
||||
|
||||
describe('when the given label is a kebab case label', (): void => {
|
||||
beforeEach((): void => {
|
||||
label = 'kebab-case-label';
|
||||
});
|
||||
|
||||
it('should return true', (): void => {
|
||||
expect.assertions(1);
|
||||
|
||||
const result = isLabeled(issue, label);
|
||||
|
||||
expect(result).toStrictEqual(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('when the given issue contains a multiple word label', (): void => {
|
||||
beforeEach((): void => {
|
||||
issue = {
|
||||
labels: [
|
||||
{
|
||||
name: 'label like a sentence'
|
||||
}
|
||||
]
|
||||
} as Issue;
|
||||
});
|
||||
|
||||
describe('when the given label is a multiple word label', (): void => {
|
||||
beforeEach((): void => {
|
||||
label = 'label like a sentence';
|
||||
});
|
||||
|
||||
it('should return true', (): void => {
|
||||
expect.assertions(1);
|
||||
|
||||
const result = isLabeled(issue, label);
|
||||
|
||||
expect(result).toStrictEqual(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('when the given issue contains a multiple word label with %20 spaces', (): void => {
|
||||
beforeEach((): void => {
|
||||
issue = {
|
||||
labels: [
|
||||
{
|
||||
name: 'label%20like%20a%20sentence'
|
||||
}
|
||||
]
|
||||
} as Issue;
|
||||
});
|
||||
|
||||
describe('when the given label is a multiple word label with %20 spaces', (): void => {
|
||||
beforeEach((): void => {
|
||||
label = 'label%20like%20a%20sentence';
|
||||
});
|
||||
|
||||
it('should return true', (): void => {
|
||||
expect.assertions(1);
|
||||
|
||||
const result = isLabeled(issue, label);
|
||||
|
||||
expect(result).toStrictEqual(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('when the given issue contains a label wih diacritical marks', (): void => {
|
||||
beforeEach((): void => {
|
||||
issue = {
|
||||
labels: [
|
||||
{
|
||||
name: 'déjà vu'
|
||||
}
|
||||
]
|
||||
} as Issue;
|
||||
});
|
||||
|
||||
describe('when the given issue contains a label', (): void => {
|
||||
beforeEach((): void => {
|
||||
label = 'deja vu';
|
||||
});
|
||||
|
||||
it('should return true', (): void => {
|
||||
expect.assertions(1);
|
||||
|
||||
const result = isLabeled(issue, label);
|
||||
|
||||
expect(result).toStrictEqual(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when the given issue contains an uppercase label', (): void => {
|
||||
beforeEach((): void => {
|
||||
label = 'DEJA VU';
|
||||
});
|
||||
|
||||
it('should return true', (): void => {
|
||||
expect.assertions(1);
|
||||
|
||||
const result = isLabeled(issue, label);
|
||||
|
||||
expect(result).toStrictEqual(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when the given issue contains a label wih diacritical marks', (): void => {
|
||||
beforeEach((): void => {
|
||||
label = 'déjà vu';
|
||||
});
|
||||
|
||||
it('should return true', (): void => {
|
||||
expect.assertions(1);
|
||||
|
||||
const result = isLabeled(issue, label);
|
||||
|
||||
expect(result).toStrictEqual(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
26
src/functions/is-labeled.ts
Normal file
26
src/functions/is-labeled.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
import deburr from 'lodash.deburr';
|
||||
import {Issue} from '../classes/issue';
|
||||
import {Label} from '../IssueProcessor';
|
||||
import {CleanLabel} from '../types/clean-label';
|
||||
|
||||
/**
|
||||
* @description
|
||||
* 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 given issue labels
|
||||
*/
|
||||
export function isLabeled(
|
||||
issue: Readonly<Issue>,
|
||||
label: Readonly<string>
|
||||
): boolean {
|
||||
return !!issue.labels.find((issueLabel: Readonly<Label>): boolean => {
|
||||
return cleanLabel(label) === cleanLabel(issueLabel.name);
|
||||
});
|
||||
}
|
||||
|
||||
function cleanLabel(label: Readonly<string>): CleanLabel {
|
||||
return deburr(label.toLowerCase());
|
||||
}
|
||||
57
src/functions/is-pull-request.spec.ts
Normal file
57
src/functions/is-pull-request.spec.ts
Normal file
@@ -0,0 +1,57 @@
|
||||
import {Issue} from '../classes/issue';
|
||||
import {isPullRequest} from './is-pull-request';
|
||||
|
||||
describe('isPullRequest()', (): void => {
|
||||
let issue: Issue;
|
||||
|
||||
describe('when the given issue has an undefined pull request', (): void => {
|
||||
beforeEach((): void => {
|
||||
issue = {
|
||||
pull_request: undefined
|
||||
} as Issue;
|
||||
});
|
||||
|
||||
it('should return false', (): void => {
|
||||
expect.assertions(1);
|
||||
|
||||
const result = isPullRequest(issue);
|
||||
|
||||
expect(result).toStrictEqual(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when the given issue has a null pull request', (): void => {
|
||||
beforeEach((): void => {
|
||||
issue = {
|
||||
pull_request: null
|
||||
} as Issue;
|
||||
});
|
||||
|
||||
it('should return false', (): void => {
|
||||
expect.assertions(1);
|
||||
|
||||
const result = isPullRequest(issue);
|
||||
|
||||
expect(result).toStrictEqual(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe.each([{}, true])(
|
||||
'when the given issue has pull request',
|
||||
(value): void => {
|
||||
beforeEach((): void => {
|
||||
issue = {
|
||||
pull_request: value
|
||||
} as Issue;
|
||||
});
|
||||
|
||||
it('should return true', (): void => {
|
||||
expect.assertions(1);
|
||||
|
||||
const result = isPullRequest(issue);
|
||||
|
||||
expect(result).toStrictEqual(true);
|
||||
});
|
||||
}
|
||||
);
|
||||
});
|
||||
5
src/functions/is-pull-request.ts
Normal file
5
src/functions/is-pull-request.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
import {Issue} from '../classes/issue';
|
||||
|
||||
export function isPullRequest(issue: Readonly<Issue>): boolean {
|
||||
return !!issue.pull_request;
|
||||
}
|
||||
47
src/functions/should-mark-when-stale.spec.ts
Normal file
47
src/functions/should-mark-when-stale.spec.ts
Normal file
@@ -0,0 +1,47 @@
|
||||
import {shouldMarkWhenStale} from './should-mark-when-stale';
|
||||
|
||||
describe('shouldMarkWhenStale()', (): void => {
|
||||
let daysBeforeStale: number;
|
||||
|
||||
describe('when the given number of days indicate that it should be stalled', (): void => {
|
||||
beforeEach((): void => {
|
||||
daysBeforeStale = -1;
|
||||
});
|
||||
|
||||
it('should return false', (): void => {
|
||||
expect.assertions(1);
|
||||
|
||||
const result = shouldMarkWhenStale(daysBeforeStale);
|
||||
|
||||
expect(result).toStrictEqual(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when the given number of days indicate that it should be stalled today', (): void => {
|
||||
beforeEach((): void => {
|
||||
daysBeforeStale = 0;
|
||||
});
|
||||
|
||||
it('should return true', (): void => {
|
||||
expect.assertions(1);
|
||||
|
||||
const result = shouldMarkWhenStale(daysBeforeStale);
|
||||
|
||||
expect(result).toStrictEqual(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when the given number of days indicate that it should be stalled tomorrow', (): void => {
|
||||
beforeEach((): void => {
|
||||
daysBeforeStale = 1;
|
||||
});
|
||||
|
||||
it('should return true', (): void => {
|
||||
expect.assertions(1);
|
||||
|
||||
const result = shouldMarkWhenStale(daysBeforeStale);
|
||||
|
||||
expect(result).toStrictEqual(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
5
src/functions/should-mark-when-stale.ts
Normal file
5
src/functions/should-mark-when-stale.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
export function shouldMarkWhenStale(
|
||||
daysBeforeStale: Readonly<number>
|
||||
): boolean {
|
||||
return daysBeforeStale >= 0;
|
||||
}
|
||||
137
src/functions/words-to-list.spec.ts
Normal file
137
src/functions/words-to-list.spec.ts
Normal file
@@ -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'
|
||||
]);
|
||||
});
|
||||
});
|
||||
});
|
||||
23
src/functions/words-to-list.ts
Normal file
23
src/functions/words-to-list.ts
Normal file
@@ -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());
|
||||
}
|
||||
15
src/interfaces/issue.ts
Normal file
15
src/interfaces/issue.ts
Normal file
@@ -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;
|
||||
}
|
||||
3
src/interfaces/milestone.ts
Normal file
3
src/interfaces/milestone.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export interface IMilestone {
|
||||
title: string;
|
||||
}
|
||||
30
src/main.ts
30
src/main.ts
@@ -1,4 +1,5 @@
|
||||
import * as core from '@actions/core';
|
||||
import {isValidDate} from './functions/dates/is-valid-date';
|
||||
import {IssueProcessor, IssueProcessorOptions} from './IssueProcessor';
|
||||
|
||||
async function run(): Promise<void> {
|
||||
@@ -14,8 +15,8 @@ async function run(): Promise<void> {
|
||||
}
|
||||
|
||||
function getAndValidateArgs(): IssueProcessorOptions {
|
||||
const args = {
|
||||
repoToken: core.getInput('repo-token', {required: true}),
|
||||
const args: IssueProcessorOptions = {
|
||||
repoToken: core.getInput('repo-token'),
|
||||
staleIssueMessage: core.getInput('stale-issue-message'),
|
||||
stalePrMessage: core.getInput('stale-pr-message'),
|
||||
closeIssueMessage: core.getInput('close-issue-message'),
|
||||
@@ -23,9 +24,13 @@ function getAndValidateArgs(): IssueProcessorOptions {
|
||||
daysBeforeStale: parseInt(
|
||||
core.getInput('days-before-stale', {required: true})
|
||||
),
|
||||
daysBeforeIssueStale: parseInt(core.getInput('days-before-issue-stale')),
|
||||
daysBeforePrStale: parseInt(core.getInput('days-before-pr-stale')),
|
||||
daysBeforeClose: parseInt(
|
||||
core.getInput('days-before-close', {required: true})
|
||||
),
|
||||
daysBeforeIssueClose: parseInt(core.getInput('days-before-issue-close')),
|
||||
daysBeforePrClose: parseInt(core.getInput('days-before-pr-close')),
|
||||
staleIssueLabel: core.getInput('stale-issue-label', {required: true}),
|
||||
closeIssueLabel: core.getInput('close-issue-label'),
|
||||
exemptIssueLabels: core.getInput('exempt-issue-labels'),
|
||||
@@ -42,7 +47,15 @@ function getAndValidateArgs(): IssueProcessorOptions {
|
||||
debugOnly: core.getInput('debug-only') === 'true',
|
||||
ascending: core.getInput('ascending') === 'true',
|
||||
skipStalePrMessage: core.getInput('skip-stale-pr-message') === 'true',
|
||||
skipStaleIssueMessage: core.getInput('skip-stale-issue-message') === 'true'
|
||||
skipStaleIssueMessage: core.getInput('skip-stale-issue-message') === 'true',
|
||||
deleteBranch: core.getInput('delete-branch') === 'true',
|
||||
startDate:
|
||||
core.getInput('start-date') !== ''
|
||||
? core.getInput('start-date')
|
||||
: undefined,
|
||||
exemptMilestones: core.getInput('exempt-milestones'),
|
||||
exemptIssueMilestones: core.getInput('exempt-issue-milestones'),
|
||||
exemptPrMilestones: core.getInput('exempt-pr-milestones')
|
||||
};
|
||||
|
||||
for (const numberInput of [
|
||||
@@ -55,6 +68,17 @@ function getAndValidateArgs(): IssueProcessorOptions {
|
||||
}
|
||||
}
|
||||
|
||||
for (const optionalDateInput of ['start-date']) {
|
||||
// Ignore empty dates because it is considered as the right type for a default value (so a valid one)
|
||||
if (core.getInput(optionalDateInput) !== '') {
|
||||
if (!isValidDate(new Date(core.getInput(optionalDateInput)))) {
|
||||
throw new Error(
|
||||
`input ${optionalDateInput} did not parse to a valid date`
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return args;
|
||||
}
|
||||
|
||||
|
||||
1
src/types/clean-label.ts
Normal file
1
src/types/clean-label.ts
Normal file
@@ -0,0 +1 @@
|
||||
export type CleanLabel = string;
|
||||
1
src/types/humanized-date.ts
Normal file
1
src/types/humanized-date.ts
Normal file
@@ -0,0 +1 @@
|
||||
export type HumanizedDate = string;
|
||||
1
src/types/iso-date-string.ts
Normal file
1
src/types/iso-date-string.ts
Normal file
@@ -0,0 +1 @@
|
||||
export type IsoDateString = string;
|
||||
1
src/types/iso-or-rfc-date-string.ts
Normal file
1
src/types/iso-or-rfc-date-string.ts
Normal file
@@ -0,0 +1 @@
|
||||
export type IsoOrRfcDateString = string;
|
||||
@@ -1,13 +1,13 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "es6", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019' or 'ESNEXT'. */
|
||||
"module": "commonjs", /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */
|
||||
"outDir": "./lib", /* Redirect output structure to the directory. */
|
||||
"rootDir": "./src", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */
|
||||
"strict": true, /* Enable all strict type-checking options. */
|
||||
"noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */
|
||||
"esModuleInterop": true, /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */
|
||||
"target": "es6" /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019' or 'ESNEXT'. */,
|
||||
"module": "commonjs" /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */,
|
||||
"outDir": "./lib" /* Redirect output structure to the directory. */,
|
||||
"rootDir": "./src" /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */,
|
||||
"strict": true /* Enable all strict type-checking options. */,
|
||||
"noImplicitAny": true /* Raise error on expressions and declarations with an implied 'any' type. */,
|
||||
"esModuleInterop": true /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */
|
||||
//"sourceMap": true
|
||||
},
|
||||
"exclude": ["node_modules", "**/*.test.ts"]
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user