├── .env.example ├── .github ├── CODEOWNERS ├── FUNDING.yml └── tests_checker.yml ├── .gitignore ├── .travis.yml ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── jest.config.js ├── package-lock.json ├── package.json ├── src ├── fileFilters.ts └── index.ts ├── test └── fileFilters.test.ts ├── tsconfig.json └── tslint.json /.env.example: -------------------------------------------------------------------------------- 1 | # The ID of your GitHub App 2 | APP_ID= 3 | WEBHOOK_SECRET=development 4 | 5 | # Use `trace` to get verbose logging or `info` to show less 6 | LOG_LEVEL=debug 7 | 8 | # Go to https://smee.io/new set this to the URL that you are redirected to. 9 | WEBHOOK_PROXY_URL= 10 | -------------------------------------------------------------------------------- /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | * @maks-rafalko 2 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: [infection] 4 | open_collective: infection 5 | -------------------------------------------------------------------------------- /.github/tests_checker.yml: -------------------------------------------------------------------------------- 1 | testDir: 'test' -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | npm-debug.log 3 | *.pem 4 | .env 5 | coverage 6 | lib 7 | .idea -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | language: node_js 3 | node_js: 4 | - "8.3" 5 | notifications: 6 | disabled: true 7 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to making participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, gender identity and expression, level of experience, 9 | education, socio-economic status, nationality, personal appearance, race, 10 | religion, or sexual identity and orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | * Using welcoming and inclusive language 18 | * Being respectful of differing viewpoints and experiences 19 | * Gracefully accepting constructive criticism 20 | * Focusing on what is best for the community 21 | * Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | * The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances 27 | * Trolling, insulting/derogatory comments, and personal or political attacks 28 | * Public or private harassment 29 | * Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission 31 | * Other conduct which could reasonably be considered inappropriate in a 32 | professional setting 33 | 34 | ## Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or 41 | reject comments, commits, code, wiki edits, issues, and other contributions 42 | that are not aligned to this Code of Conduct, or to ban temporarily or 43 | permanently any contributor for other behaviors that they deem inappropriate, 44 | threatening, offensive, or harmful. 45 | 46 | ## Scope 47 | 48 | This Code of Conduct applies both within project spaces and in public spaces 49 | when an individual is representing the project or its community. Examples of 50 | representing a project or community include using an official project e-mail 51 | address, posting via an official social media account, or acting as an appointed 52 | representative at an online or offline event. Representation of a project may be 53 | further defined and clarified by project maintainers. 54 | 55 | ## Enforcement 56 | 57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 58 | reported by contacting the project team at maks.rafalko@gmail.com. All 59 | complaints will be reviewed and investigated and will result in a response that 60 | is deemed necessary and appropriate to the circumstances. The project team is 61 | obligated to maintain confidentiality with regard to the reporter of an incident. 62 | Further details of specific enforcement policies may be posted separately. 63 | 64 | Project maintainers who do not follow or enforce the Code of Conduct in good 65 | faith may face temporary or permanent repercussions as determined by other 66 | members of the project's leadership. 67 | 68 | ## Attribution 69 | 70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 71 | available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html 72 | 73 | [homepage]: https://www.contributor-covenant.org 74 | 75 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | ## Contributing 2 | 3 | [fork]: /fork 4 | [pr]: /compare 5 | [style]: https://standardjs.com/ 6 | [code-of-conduct]: CODE_OF_CONDUCT.md 7 | 8 | Hi there! We're thrilled that you'd like to contribute to this project. Your help is essential for keeping it great. 9 | 10 | Please note that this project is released with a [Contributor Code of Conduct][code-of-conduct]. By participating in this project you agree to abide by its terms. 11 | 12 | ## Issues and PRs 13 | 14 | If you have suggestions for how this project could be improved, or want to report a bug, open an issue! We'd love all and any contributions. If you have questions, too, we'd love to hear them. 15 | 16 | We'd also love PRs. If you're thinking of a large PR, we advise opening up an issue first to talk about it, though! Look at the links below if you're not sure how to open a PR. 17 | 18 | ## Submitting a pull request 19 | 20 | 1. [Fork][fork] and clone the repository. 21 | 1. Configure and install the dependencies: `npm install`. 22 | 1. Make sure the tests pass on your machine: `npm test`. 23 | 1. Create a new branch: `git checkout -b my-branch-name`. 24 | 1. Make your change, add tests, and make sure the tests still pass. 25 | 1. Push to your fork and [submit a pull request][pr]. 26 | 1. Pat your self on the back and wait for your pull request to be reviewed and merged. 27 | 28 | Here are a few things you can do that will increase the likelihood of your pull request being accepted: 29 | 30 | - Follow the [style guide][style] which is using standard. Any linting errors should be shown when running `npm test`. 31 | - Write and update tests. 32 | - Keep your changes as focused as possible. If there are multiple changes you would like to make that are not dependent upon each other, consider submitting them as separate pull requests. 33 | - Write a [good commit message](http://tbaggery.com/2008/04/19/a-note-about-git-commit-messages.html). 34 | 35 | Work in Progress pull requests are also welcome to get feedback early on, or if there is something blocked you. 36 | 37 | ## Resources 38 | 39 | - [How to Contribute to Open Source](https://opensource.guide/how-to-contribute/) 40 | - [Using Pull Requests](https://help.github.com/articles/about-pull-requests/) 41 | - [GitHub Help](https://help.github.com) 42 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | ISC License 2 | 3 | Copyright (c) 2018, Maks Rafalko (https://github.com/infection/tests-checker) 4 | 5 | Permission to use, copy, modify, and/or distribute this software for any 6 | purpose with or without fee is hereby granted, provided that the above 7 | copyright notice and this permission notice appear in all copies. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 10 | WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 11 | MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR 12 | ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 13 | WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 14 | ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF 15 | OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 16 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | > **Warning** 2 | > 3 | > This GitHub App has been deprecated in favor of GitHub Action with the same functionality: https://github.com/infection/tests-checker-action 4 | 5 | [![Build Status](https://travis-ci.org/infection/tests-checker.svg?branch=master)](https://travis-ci.org/infection/tests-checker) 6 | 7 | # Tests Checker 8 | 9 | To install this bot to your Open Sourced project: 10 | 11 | * open https://github.com/apps/tests-checker 12 | * click "Install" 13 | * choose the repository you want to install `tests-checker` to 14 | 15 | tests-checker 16 | 17 | 18 | ## Settings 19 | 20 | You can configure bot by adding `.github/tests_checker.yml` file to the repository and override any of the settings listed below. 21 | 22 | Default values are: 23 | 24 | ```yaml 25 | comment: 'Could you please add tests to make sure this change works as expected?', 26 | fileExtensions: ['.php', '.ts', '.js', '.c', '.cs', '.cpp', '.rb', '.java'] 27 | testDir: 'tests' 28 | testPattern: '' 29 | ``` 30 | 31 | where 32 | 33 | * `comment` - a text that bot will post when it won't find tests in the PR 34 | * `fileExtensions` - extensions of the files that should be treated as a `source code`. Bot will do nothing if you just updating `README.md` because usually no tests are required to cover such change. 35 | * `testDir` - folder, where tests are located. Make sure to set it correctly, otherwise bot will not be able to understand whether the test file was added or not. 36 | * `testPattern` - a shell glob pattern that should match test files. For example, you can set it to `testPattern: *_test.go` and Bot will be able to understand, that added test has this pattern instead of located in `testDir`. `testDir` and `testPattern` are alternatives, however can be used together. 37 | 38 | Both `testDir` and `testPattern` may be specified in a custom configuration, both settings will be used to locate test files. 39 | If you want to change any of the settings, just add `.github/tests_checker.yml`: 40 | 41 | ```yaml 42 | testDir: app-tests 43 | ``` 44 | 45 | If you don't want to change anything, you can skip creating this file. 46 | 47 | ## Setup 48 | 49 | This is needed if you want to deploy this bot to your server or want to contribute to it. 50 | Please note, that `tests-checker` is ready to use. 51 | You just need to install Github Application as mentioned in above. 52 | 53 | ```sh 54 | # Install dependencies 55 | npm install 56 | 57 | # Run typescript 58 | npm run build 59 | 60 | # Run the bot for production or 61 | npm start 62 | 63 | # for development 64 | npm run dev 65 | ``` 66 | 67 | ## Deploy 68 | 69 | Install `now`: 70 | 71 | `npm install -g now` 72 | 73 | Deploy: 74 | 75 | ```bash 76 | now -e APP_ID=17064 \ 77 | -e WEBHOOK_SECRET=XXX \ 78 | -e PRIVATE_KEY_BASE64="$(cat ./key.pem | base64)" 79 | ``` 80 | 81 | See `WEBHOOK_SECRET` on GitHub app: https://github.com/settings/apps/tests-checker. 82 | 83 | Set a permanent alias for the new deployed application URL: 84 | 85 | ```bash 86 | now alias set https://tests-checker-XYZ.now.sh https://tests-checker.now.sh 87 | ``` 88 | 89 | ### Debugging `now.sh` 90 | 91 | * `now ls tests-checker` 92 | * `now inspect tests-checker.now.sh` 93 | * `now rm tests-checker-qkkyxnelyo.now.sh` to free some instances available for OSS plan 94 | 95 | ## Contributing 96 | 97 | If you have suggestions for how `tests-checker` could be improved, or want to report a bug, open an issue! 98 | We'd love all and any contributions. 99 | 100 | For more, check out the [Contributing Guide](CONTRIBUTING.md). 101 | 102 | > A GitHub App built with [Probot](https://github.com/probot/probot) that require writing tests in Pull Requests. 103 | 104 | # Developing 105 | 106 | 1. Read https://probot.github.io/docs/development/#manually-configuring-a-github-app and configure `smee` 107 | 2. add generated `smee` URL to `.env` - `WEBHOOK_PROXY_URL` variable, and to github app: https://github.com/settings/apps/tests-checker-dev 108 | 3. Open terminal 1 and run `smee -u https://smee.io/xxx` 109 | 4. open terminal 2 and run `npm start` 110 | 5. now bot is ready to work from the local machine! 111 | 112 | ## License 113 | 114 | [ISC](LICENSE) © 2018 Maks Rafalko (https://github.com/infection/tests-checker) 115 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | roots: ['/src/', '/test/'], 3 | transform: { 4 | '^.+\\.tsx?$': 'ts-jest' 5 | }, 6 | testRegex: '(/__tests__/.*|\\.(test|spec))\\.[tj]sx?$', 7 | moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'json', 'node'] 8 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "tests-checker", 3 | "version": "1.0.0", 4 | "description": "Requires writing the tests in Pull Requests.", 5 | "author": "Maks Rafalko (https://github.com/infection/tests-checker)", 6 | "license": "ISC", 7 | "repository": "https://github.com/infection/tests-checker.git", 8 | "homepage": "https://github.com/infection/tests-checker", 9 | "bugs": "https://github.com/infection/tests-checker/issues", 10 | "keywords": [ 11 | "probot", 12 | "github", 13 | "probot-app" 14 | ], 15 | "scripts": { 16 | "now-start": "PRIVATE_KEY=$(echo $PRIVATE_KEY_BASE64 | base64 -d) npm start", 17 | "build": "tsc -p tsconfig.json", 18 | "dev": "nodemon --exec \"npm start\"", 19 | "start": "probot run ./lib/index.js", 20 | "lint": "tslint -c tslint.json '{src,test}/**/*.ts'", 21 | "lint-fix": "tslint -c tslint.json --fix '{src,test}/**/*.ts'", 22 | "test": "jest", 23 | "test:watch": "jest --watch --notify --notifyMode=change --coverage" 24 | }, 25 | "dependencies": { 26 | "minimatch": "^3.0.4", 27 | "probot": "^7.0.0" 28 | }, 29 | "devDependencies": { 30 | "@types/jest": "^23.1.5", 31 | "@types/minimatch": "^3.0.3", 32 | "@types/node": "^10.5.2", 33 | "eslint-plugin-typescript": "^0.12.0", 34 | "jest": "^23.4.0", 35 | "nodemon": "^1.17.2", 36 | "smee-client": "^1.0.2", 37 | "ts-jest": "^23.0.0", 38 | "tslint": "^5.11.0", 39 | "typescript": "^2.9.2", 40 | "typescript-eslint-parser": "^18.0.0" 41 | }, 42 | "engines": { 43 | "node": ">= 8.3.0" 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/fileFilters.ts: -------------------------------------------------------------------------------- 1 | import Octokit from '@octokit/rest'; 2 | import minimatch = require('minimatch'); 3 | 4 | export function getTouchedSourceFilesRequireTests( 5 | files: Octokit.GetFilesResponseItem[], 6 | fileExtensions: string[]): Octokit.GetFilesResponseItem[] { 7 | return files.filter((file) => { 8 | return fileExtensions.find((fileExtension: string) => { 9 | return file.filename.substr(-fileExtension.length) === fileExtension; 10 | }); 11 | }); 12 | } 13 | 14 | export function getTouchedTestFiles( 15 | files: Octokit.GetFilesResponseItem[], 16 | testDir: string, 17 | testPattern: string): Octokit.GetFilesResponseItem[] { 18 | 19 | let filtered: Octokit.GetFilesResponseItem[] = []; 20 | 21 | if (testDir) { 22 | filtered = files.filter((file) => { 23 | return file.filename.indexOf(testDir + '/') === 0; 24 | }); 25 | } 26 | if (testPattern) { 27 | filtered = filtered.concat(files.filter((file) => { 28 | return minimatch(file.filename, testPattern); 29 | })); 30 | } 31 | return filtered; 32 | } 33 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import {Application} from 'probot'; 2 | import {getTouchedSourceFilesRequireTests, getTouchedTestFiles} from './fileFilters'; 3 | 4 | export = (app: Application) => { 5 | app.on('pull_request.opened', async (context) => { 6 | const config = await context.config('tests_checker.yml', { 7 | comment: 'Could you please add tests to make sure this change works as expected?', 8 | fileExtensions: ['.php', '.ts', '.js', '.c', '.cs', '.cpp', '.rb', '.java'], 9 | testDir: 'tests', 10 | testPattern: '', 11 | }); 12 | 13 | const issue = context.issue(); 14 | 15 | context.log('PR=' + 'https://github.com/' + issue.owner + '/' + issue.repo + '/pull/' + issue.number); 16 | 17 | const allFiles = await context.github.paginate( 18 | context.github.pullRequests.getFiles(issue), 19 | (res) => res.data, 20 | ); 21 | 22 | const sourceFilesRequireTests = getTouchedSourceFilesRequireTests(allFiles, config.fileExtensions); 23 | 24 | if (sourceFilesRequireTests.length === 0) { 25 | context.log('PR does not have files that require tests. Skipping...'); 26 | return; 27 | } 28 | 29 | const testFiles = getTouchedTestFiles(allFiles, config.testDir, config.testPattern); 30 | 31 | context.log('testFiles=', testFiles); 32 | 33 | if (testFiles.length === 0) { 34 | context.log('Adding a comment about tests.'); 35 | context.github.pullRequests.createReview({ 36 | ...issue, 37 | body: config.comment, 38 | event: 'REQUEST_CHANGES', 39 | }); 40 | } 41 | }); 42 | }; 43 | -------------------------------------------------------------------------------- /test/fileFilters.test.ts: -------------------------------------------------------------------------------- 1 | import Octokit from '@octokit/rest'; 2 | import {getTouchedSourceFilesRequireTests, getTouchedTestFiles} from '../src/fileFilters'; 3 | 4 | test('getTouchedSourceFilesRequireTests should filter out files by file extensions', () => { 5 | const files = [ 6 | {filename: 'README.md'} as Octokit.GetFilesResponseItem, 7 | {filename: 'foo.php'} as Octokit.GetFilesResponseItem, 8 | {filename: 'bar.ts'} as Octokit.GetFilesResponseItem, 9 | ]; 10 | const extensions = ['.php']; 11 | 12 | const filteredFiles = getTouchedSourceFilesRequireTests(files, extensions); 13 | 14 | expect(filteredFiles.length).toBe(1); 15 | expect(filteredFiles[0].filename).toBe('foo.php'); 16 | }); 17 | 18 | test('getTouchedTestFiles should return test files from the list of different files', () => { 19 | const files = [ 20 | {filename: 'README.md'} as Octokit.GetFilesResponseItem, 21 | {filename: 'foo.php'} as Octokit.GetFilesResponseItem, 22 | {filename: 'lib/tests/bar1.ts'} as Octokit.GetFilesResponseItem, 23 | {filename: 'lib/tests/foo1.ts'} as Octokit.GetFilesResponseItem, 24 | {filename: 'lib/testsxxx/bar2.ts'} as Octokit.GetFilesResponseItem, 25 | {filename: 'a/test/bar3.ts'} as Octokit.GetFilesResponseItem, 26 | {filename: 'a/tests/bar4.ts'} as Octokit.GetFilesResponseItem, 27 | {filename: 'test_test.ts'} as Octokit.GetFilesResponseItem, 28 | ]; 29 | 30 | const filteredFilesTestDir = getTouchedTestFiles(files, 'lib/tests', ''); 31 | expect(filteredFilesTestDir.length).toBe(2); 32 | expect(filteredFilesTestDir[0].filename).toBe('lib/tests/bar1.ts'); 33 | expect(filteredFilesTestDir[1].filename).toBe('lib/tests/foo1.ts'); 34 | 35 | const filteredFilesTestPatternSimple = getTouchedTestFiles(files, '', '*_test.ts'); 36 | expect(filteredFilesTestPatternSimple.length).toBe(1); 37 | expect(filteredFilesTestPatternSimple[0].filename).toBe('test_test.ts'); 38 | 39 | const filteredFilesTestPatternSubdir = getTouchedTestFiles(files, '', '*/test/*.ts'); 40 | expect(filteredFilesTestPatternSubdir.length).toBe(1); 41 | expect(filteredFilesTestPatternSubdir[0].filename).toBe('a/test/bar3.ts'); 42 | 43 | const filteredFilesTestPatternAllInTests = getTouchedTestFiles(files, '', '**/tests/*.ts'); 44 | expect(filteredFilesTestPatternAllInTests.length).toBe(3); 45 | expect(filteredFilesTestPatternAllInTests[0].filename).toBe('lib/tests/bar1.ts'); 46 | expect(filteredFilesTestPatternAllInTests[1].filename).toBe('lib/tests/foo1.ts'); 47 | expect(filteredFilesTestPatternAllInTests[2].filename).toBe('a/tests/bar4.ts'); 48 | 49 | const filteredFilesTestBothDirAndPattern = getTouchedTestFiles(files, 'lib/testsxxx', '**/tests/*.ts'); 50 | expect(filteredFilesTestBothDirAndPattern.length).toBe(4); 51 | expect(filteredFilesTestBothDirAndPattern[0].filename).toBe('lib/testsxxx/bar2.ts'); 52 | expect(filteredFilesTestBothDirAndPattern[1].filename).toBe('lib/tests/bar1.ts'); 53 | expect(filteredFilesTestBothDirAndPattern[2].filename).toBe('lib/tests/foo1.ts'); 54 | expect(filteredFilesTestBothDirAndPattern[3].filename).toBe('a/tests/bar4.ts'); 55 | }); 56 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "allowJs": false, 4 | "lib": ["es2015", "es2017"], 5 | "module": "commonjs", 6 | "moduleResolution": "node", 7 | "target": "es5", 8 | "noImplicitReturns": true, 9 | "noFallthroughCasesInSwitch": true, 10 | "noUnusedLocals": true, 11 | "pretty": true, 12 | "strict": true, 13 | "sourceMap": true, 14 | "outDir": "./lib", 15 | "skipLibCheck": true, 16 | "noImplicitAny": true, 17 | "esModuleInterop": true, 18 | "declaration": true 19 | }, 20 | "include": [ 21 | "src/**/*" 22 | ], 23 | "compileOnSave": true 24 | } 25 | -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "defaultSeverity": "error", 3 | "extends": [ 4 | "tslint:recommended" 5 | ], 6 | "jsRules": {}, 7 | "rules": { 8 | "indent": [ 9 | true, 10 | "spaces", 11 | 4 12 | ], 13 | "quotemark": [ 14 | true, 15 | "single" 16 | ], 17 | "ordered-imports": true, 18 | "object-literal-key-quotes": [ 19 | true, 20 | "as-needed" 21 | ] 22 | }, 23 | "rulesDirectory": [] 24 | } --------------------------------------------------------------------------------