├── .env.example ├── .githint.json ├── .github ├── ISSUE_TEMPLATE.md ├── PULL_REQUEST_TEMPLATE.md └── RELEASE_TEMPLATE.md ├── .gitignore ├── .sequelizerc ├── .travis.yml ├── CHANGELOG.md ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── app.yml ├── database ├── config │ └── config.js ├── migrations │ ├── 20190209220258-create-installation.js │ └── 20190209231258-create-repository.js ├── models │ ├── index.js │ ├── installation.js │ └── repository.js └── seeders │ └── 20190209220258-create-installation.js ├── index.js ├── package-lock.json ├── package.json ├── test ├── fixtures │ ├── check_run.created.json │ └── check_suite.requested.json └── index.test.js └── utils.js /.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 | -------------------------------------------------------------------------------- /.githint.json: -------------------------------------------------------------------------------- 1 | { 2 | "options": { 3 | "detectPull": true 4 | }, 5 | "checks": { 6 | ".gitignore": { 7 | "script": "!!(tree.tree.find(t => t.path === '.gitignore'))", 8 | "message": [ 9 | "The repository must contain a .gitignore file" 10 | ] 11 | }, 12 | "CHANGELOG": { 13 | "script": "!!(tree.tree.find(t => t.path === 'CHANGELOG' || t.path === 'CHANGELOG.md'))", 14 | "message": [ 15 | "The repository must contain a CHANGELOG(.md) file" 16 | ] 17 | }, 18 | "CODE_OF_CONDUCT": { 19 | "script": "!!(tree.tree.find(t => t.path === 'CODE_OF_CONDUCT.md'))", 20 | "message": [ 21 | "The repository must contain a CODE_OF_CONDUCT.md file" 22 | ] 23 | }, 24 | "CONTRIBUTING": { 25 | "script": "!!(tree.tree.find(t => t.path === 'CONTRIBUTING.md'))", 26 | "message": [ 27 | "The repository must contain a CONTRIBUTING.md file" 28 | ] 29 | }, 30 | "LICENSE": { 31 | "script": "!!(tree.tree.find(t => t.path === 'LICENSE'))", 32 | "message": [ 33 | "The repository must contain a LICENSE file" 34 | ] 35 | }, 36 | "README": { 37 | "script": "!!(tree.tree.find(t => t.path === 'README.md'))", 38 | "message": [ 39 | "Help people interested in this repository understand your project by adding a README." 40 | ] 41 | }, 42 | "Branch Name": { 43 | "script": "/^((ft-)|(ch-)|(bg-))[a-z0-9\\-]+$/.test(branch.name)", 44 | "message": [ 45 | "Branches created should be named using the following format:", 46 | "{story type}-{2-3 word summary}", 47 | "", 48 | "{story type} - Indicates the context of the branch and should be one of:", 49 | " * bg = Bug", 50 | " * ch = Chore", 51 | " * ft = Feature", 52 | "", 53 | "{2-3 word summary} - Short 2-3 words summary about what the branch contains", 54 | " This can contain digits, lowercase alphabets, dash.", 55 | "", 56 | "Example: ft-resources-rest-endpoints" 57 | ] 58 | }, 59 | "Commit Message": { 60 | "script": [ 61 | "const message = commit.commit.message.trim()", 62 | "const messageLines = message.split('\\n')", 63 | "const len = messageLines.length", 64 | "if (len >= 5) {", 65 | " if (messageLines[0].trim() !== '' && messageLines[1].trim() === '' && messageLines[2].trim() !== ''", 66 | " && messageLines[len - 1].trim() !== '' && messageLines[len - 2].trim() === '' && messageLines[len - 3].trim() !== '') {", 67 | " return true;", 68 | " }", 69 | "}", 70 | "return false;" 71 | ], 72 | "message": "A commit message consists of a header, a body and a footer, separated by blank lines." 73 | }, 74 | "Commit Message Lines": { 75 | "script": [ 76 | "const message = commit.commit.message.trim()", 77 | "const messageLines = message.split('\\n')", 78 | "return !(messageLines.find(line => line.length > 100));" 79 | ], 80 | "message": [ 81 | "Any line of the commit message cannot be longer than 100 characters!", 82 | "This allows the message to be easier to read on github as well as in various git tools." 83 | ] 84 | }, 85 | "Commit Message Header": { 86 | "script": [ 87 | "const message = commit.commit.message.trim()", 88 | "const messageLines = message.split('\\n')", 89 | "if (messageLines.length >= 5) {", 90 | " const header = messageLines[0]", 91 | " return /^(bug|chore|docs|feat|fix|refactor|style|test)(\\([\\w\\s-.]+\\))?:\\s*[a-z].+[^\\.]$/.test(header)", 92 | "}", 93 | "return false;" 94 | ], 95 | "message": [ 96 | "The commit message header is a single line that contains succinct description", 97 | "of the change containing a type, an optional scope and a subject.", 98 | "The commit message header should be in the following format:", 99 | "`{type}({scope}): {subject}`", 100 | "", 101 | "`type` - This describes the kind of change that this commit is providing.", 102 | " * feat (feature)", 103 | " * fix (bug fix)", 104 | " * chore (maintain)", 105 | " * docs (documentation)", 106 | " * style (formatting, missing semi colons, …)", 107 | " * refactor", 108 | " * test (when adding missing tests)", 109 | "", 110 | "`scope` - can be anything specifying place of the commit change", 111 | "", 112 | "`subject` - This is a very short description of the change.", 113 | " * use imperative, present tense: “change” not “changed” nor “changes”", 114 | " * don't capitalize first letter", 115 | " * no dot (.) at the end" 116 | ] 117 | }, 118 | "Commit Message Footer": { 119 | "skip": true, 120 | "script": [ 121 | "const message = commit.commit.message.trim()", 122 | "const messageLines = message.split('\\n')", 123 | "if (messageLines.length >= 5) {", 124 | " const footer = messageLines[messageLines.length - 1]", 125 | " return /^\\[([Ss]tarts|[Ff]inishes|[Ff]ixes|[Dd]elivers)?\\s*#[0-9]+]$/.test(footer)", 126 | "}", 127 | "return false;" 128 | ], 129 | "message": [ 130 | "Started, finished, fixed or delivered stories should be listed in the footer", 131 | "prefixed with 'Finishes', 'Fixes' , or 'Delivers' keyword like this:", 132 | "", 133 | "[Finishes #1234567]" 134 | ] 135 | }, 136 | "PR Title": { 137 | "script": "/^[\\w\\s-().]+$/.test(pull.title)", 138 | "message": [ 139 | "The PR title should be in the following format:", 140 | "`{story description}`", 141 | "", 142 | "Example of a valid PR title:", 143 | "", 144 | "Build out REST Endpoint for Resources (CRUD)" 145 | ] 146 | }, 147 | "PR Description": { 148 | "script": [ 149 | "const body = pull.body", 150 | "return body.indexOf('#### What does this PR do?') > -1", 151 | " && body.indexOf('#### Description of task to be completed?') > -1", 152 | " && body.indexOf('#### How should this be manually tested?') > -1", 153 | " && body.indexOf('#### Any background context you want to provide?') > -1", 154 | " && body.indexOf('#### What are the relevant pivotal tracker stories?') > -1", 155 | " && body.indexOf('#### Screenshots (if appropriate)') > -1", 156 | " && body.indexOf('#### Questions') > -1" 157 | ], 158 | "message": [ 159 | "The description of the PR should contain the following headings", 160 | "and corresponding content in Markdown format.", 161 | "", 162 | "`#### What does this PR do?`", 163 | "`#### Description of task to be completed?`", 164 | "`#### How should this be manually tested?`", 165 | "`#### Any background context you want to provide?`", 166 | "`#### What are the relevant pivotal tracker stories?`", 167 | "`#### Screenshots (if appropriate)`", 168 | "`#### Questions:`" 169 | ] 170 | }, 171 | "PR 'ready' Label": { 172 | "script": "pull.labels.length === 1 && !!(pull.labels.find(l => l.name === 'ready'))", 173 | "message": "PR must have label 'ready'" 174 | } 175 | } 176 | } 177 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | ## Identified Gap 4 | 5 | 6 | ## Possible Solution 7 | 8 | 9 | ## Examples 10 | 11 | 12 | ## Additional Context 13 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | #### What does this PR do? 2 | #### Description of task to be completed? 3 | #### How should this be manually tested? 4 | #### Any background context you want to provide? 5 | #### What are the relevant pivotal tracker stories? 6 | #### Screenshots (if appropriate) 7 | #### Questions: 8 | -------------------------------------------------------------------------------- /.github/RELEASE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | `title: , Version ` 2 | 3 | ## New Guidelines 4 | 5 | >You can exclude this section if there are no new guidelines 6 | 7 | - **Section Title (eg. Developing)**: Guideline name (and short description), `#` 8 | - etc 9 | 10 | ## Updates 11 | 12 | - **Section Title (eg. Developing)**: Update made, `#` 13 | - etc. 14 | 15 | ## Fixes 16 | 17 | - **Section Title (eg. Developing)**: Update made, `#` 18 | - etc. 19 | 20 | ## Credits 21 | 22 | >List contributors who were involved in the changes for the release, including reviewers. 23 | 24 | - Name (@gh-handle) 25 | - etc 26 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | build 2 | node_modules 3 | .env 4 | .DS_Store 5 | .vscode 6 | private.md 7 | todo.txt 8 | -------------------------------------------------------------------------------- /.sequelizerc: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | 3 | let src = 'database'; 4 | 5 | module.exports = { 6 | "config": path.resolve(`./${src}/config`, 'config.js'), 7 | "models-path": path.resolve(`./${src}/models`), 8 | "seeders-path": path.resolve(`./${src}/seeders`), 9 | "migrations-path": path.resolve(`./${src}/migrations`) 10 | }; 11 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | language: node_js 3 | node_js: 4 | - "8.3" 5 | notifications: 6 | disabled: true 7 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | All notable changes to this project will be documented in this file. 3 | 4 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), 5 | and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). 6 | 7 | ## [Unreleased] 8 | ### Added 9 | - `/api/repos` endpoint 10 | - `/api/installs` endpoint 11 | 12 | ## [1.1.0] - 2019-07-14 13 | ### Added 14 | - `options` can now have a field called `skip` which can be used to skip (or not skip) all checks 15 | ```json 16 | { 17 | "options": { 18 | "detectPull": true, 19 | "skip": true 20 | } 21 | } 22 | ``` 23 | This will skip all checks. 24 | - The `skip` for each check overrides this _global_ `skip`. 25 | 26 | ### Changed 27 | - The `detectPull` and the newly added `skip` fields in `options` (as well as the `skip` for each check) don't have 28 | to hold plain `Boolean` values; they can hold `String` values. If they hold `String` values such values are expected 29 | to be JavaScript code snippets to be executed in the same manner checks are executed. The code snippet for `detectPull` 30 | does not have access to the `pull` object. 31 | ```json 32 | { 33 | "options": { 34 | "detectPull": "commit.author.login.toLowerCase() !== 'greenkeeper[bot]'", 35 | "skip": "commit.author.login.toLowerCase() === 'greenkeeper[bot]'" 36 | } 37 | } 38 | ``` 39 | This will skip all checks (and not bother detecting a pull request object) if the author of the commit is the 40 | _greenkeeper_ bot. 41 | 42 | ## [1.0.0] - 2019-06-24 43 | ### Added 44 | - Bot is feature-complete and stable. 45 | -------------------------------------------------------------------------------- /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 chieze.franklin@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`, note: these tests also apply the linter, so there's no need to lint separately. 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) 2019, Franklin Chieze (https://prlint.herokuapp.com) 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 | [GitHint](https://github.com/apps/githint-bot) ensures that your pull requests follow specified conventions. 2 | 3 | [![Made in Nigeria](https://img.shields.io/badge/made%20in-nigeria-008751.svg?style=flat-square)](https://github.com/acekyd/made-in-nigeria) 4 | 5 | ![](https://camo.githubusercontent.com/7dae22e63277199f47c25ad0911d03d83d6d937e/68747470733a2f2f67697468696e742e6865726f6b756170702e636f6d2f696d616765732f73637265656e73686f74732f70617373696e672d74657374732e706e67) 6 | 7 | There are conventions that may not be easily checked with tools like ESLint or Hound CI. These could range from arbitrary checks like `A pull request must be raised by a user whose first name is not more than 6 characters long` to more practical checks like `A pull request must have at least 2 review comments`. GitHint thrives on checking these kinds of conventions. 8 | 9 | GitHint fetches metadata about pull requests, commits, branches, trees, and passes the metadata to user-defined scripts for evaluation. Such scripts are expected to return `true` or `false` to determine if a pull request is ready to be merged. 10 | 11 | To start using GitHint, first [install the GitHint GitHub app](https://github.com/apps/githint-bot/installations/new) on your repository and add a [.githint.json file](https://githint.herokuapp.com/config) to the root directory of the repository. That's it! 12 | 13 | For more info visit the [documentation](https://githint.herokuapp.com/). 14 | -------------------------------------------------------------------------------- /app.yml: -------------------------------------------------------------------------------- 1 | # This is a GitHub App Manifest. These settings will be used by default when 2 | # initially configuring your GitHub App. 3 | # 4 | # NOTE: changing this file will not update your GitHub App settings. 5 | # You must visit github.com/settings/apps/your-app-name to edit them. 6 | # 7 | # Read more about configuring your GitHub App: 8 | # https://probot.github.io/docs/development/#configuring-a-github-app 9 | # 10 | # Read more about GitHub App Manifests: 11 | # https://developer.github.com/apps/building-github-apps/creating-github-apps-from-a-manifest/ 12 | 13 | # The list of events the GitHub App subscribes to. 14 | # Uncomment the event names below to enable them. 15 | default_events: 16 | - check_run 17 | - check_suite 18 | - installation 19 | - installation_repositories 20 | # - commit_comment 21 | # - create 22 | # - delete 23 | # - deployment 24 | # - deployment_status 25 | # - fork 26 | # - gollum 27 | # - issue_comment 28 | # - issues 29 | # - label 30 | # - milestone 31 | # - member 32 | # - membership 33 | # - org_block 34 | # - organization 35 | # - page_build 36 | # - project 37 | # - project_card 38 | # - project_column 39 | # - public 40 | # - pull_request 41 | # - pull_request_review 42 | # - pull_request_review_comment 43 | # - push 44 | # - release 45 | # - repository 46 | # - repository_import 47 | # - status 48 | # - team 49 | # - team_add 50 | # - watch 51 | 52 | # The set of permissions needed by the GitHub App. The format of the object uses 53 | # the permission name for the key (for example, issues) and the access type for 54 | # the value (for example, write). 55 | # Valid values are `read`, `write`, and `none` 56 | default_permissions: 57 | # Repository creation, deletion, settings, teams, and collaborators. 58 | # https://developer.github.com/v3/apps/permissions/#permission-on-administration 59 | # administration: read 60 | 61 | # Checks on code. 62 | # https://developer.github.com/v3/apps/permissions/#permission-on-checks 63 | checks: write 64 | 65 | # Repository contents, commits, branches, downloads, releases, and merges. 66 | # https://developer.github.com/v3/apps/permissions/#permission-on-contents 67 | # contents: read 68 | 69 | # Deployments and deployment statuses. 70 | # https://developer.github.com/v3/apps/permissions/#permission-on-deployments 71 | # deployments: read 72 | 73 | # Issues and related comments, assignees, labels, and milestones. 74 | # https://developer.github.com/v3/apps/permissions/#permission-on-issues 75 | # issues: read 76 | 77 | # Search repositories, list collaborators, and access repository metadata. 78 | # https://developer.github.com/v3/apps/permissions/#metadata-permissions 79 | metadata: read 80 | 81 | # Retrieve Pages statuses, configuration, and builds, as well as create new builds. 82 | # https://developer.github.com/v3/apps/permissions/#permission-on-pages 83 | # pages: read 84 | 85 | # Pull requests and related comments, assignees, labels, milestones, and merges. 86 | # https://developer.github.com/v3/apps/permissions/#permission-on-pull-requests 87 | # pull_requests: read 88 | 89 | # Manage the post-receive hooks for a repository. 90 | # https://developer.github.com/v3/apps/permissions/#permission-on-repository-hooks 91 | # repository_hooks: read 92 | 93 | # Manage repository projects, columns, and cards. 94 | # https://developer.github.com/v3/apps/permissions/#permission-on-repository-projects 95 | # repository_projects: read 96 | 97 | # Retrieve security vulnerability alerts. 98 | # https://developer.github.com/v4/object/repositoryvulnerabilityalert/ 99 | # vulnerability_alerts: read 100 | 101 | # Commit statuses. 102 | # https://developer.github.com/v3/apps/permissions/#permission-on-statuses 103 | # statuses: read 104 | 105 | # Organization members and teams. 106 | # https://developer.github.com/v3/apps/permissions/#permission-on-members 107 | # members: read 108 | 109 | # View and manage users blocked by the organization. 110 | # https://developer.github.com/v3/apps/permissions/#permission-on-organization-user-blocking 111 | # organization_user_blocking: read 112 | 113 | # Manage organization projects, columns, and cards. 114 | # https://developer.github.com/v3/apps/permissions/#permission-on-organization-projects 115 | # organization_projects: read 116 | 117 | # Manage team discussions and related comments. 118 | # https://developer.github.com/v3/apps/permissions/#permission-on-team-discussions 119 | # team_discussions: read 120 | 121 | # Manage the post-receive hooks for an organization. 122 | # https://developer.github.com/v3/apps/permissions/#permission-on-organization-hooks 123 | # organization_hooks: read 124 | 125 | # Get notified of, and update, content references. 126 | # https://developer.github.com/v3/apps/permissions/ 127 | # organization_administration: read 128 | 129 | 130 | # The name of the GitHub App. Defaults to the name specified in package.json 131 | # name: My Probot App 132 | 133 | # The homepage of your GitHub App. 134 | # url: https://example.com/ 135 | 136 | # A description of the GitHub App. 137 | # description: A description of my awesome app 138 | 139 | # Set to true when your GitHub App is available to the public or false when it is only accessible to the owner of the app. 140 | # Default: true 141 | # public: false 142 | -------------------------------------------------------------------------------- /database/config/config.js: -------------------------------------------------------------------------------- 1 | require('dotenv').config(); 2 | 3 | module.exports = { 4 | development: { 5 | database: process.env.DATABASE_DEV, 6 | dialect: 'postgres', 7 | host: '127.0.0.1', 8 | operatorsAliases: false, 9 | password: process.env.DATABASE_DEV_PASSWORD, 10 | username: process.env.DATABASE_DEV_USERNAME, 11 | url: process.env.DATABASE_DEV_URL 12 | }, 13 | test: { 14 | database: process.env.DATABASE_TEST, 15 | dialect: 'postgres', 16 | host: '127.0.0.1', 17 | operatorsAliases: false, 18 | password: process.env.DATABASE_TEST_PASSWORD, 19 | username: process.env.DATABASE_TEST_USERNAME, 20 | url: process.env.DATABASE_TEST_URL 21 | }, 22 | production: { 23 | dialect: 'postgres', 24 | url: process.env.DATABASE_URL, 25 | operatorsAliases: false, 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /database/migrations/20190209220258-create-installation.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | up: (queryInterface, Sequelize) => queryInterface.createTable('Installations', { 3 | id: { 4 | allowNull: false, 5 | autoIncrement: false, 6 | primaryKey: true, 7 | type: Sequelize.INTEGER 8 | }, 9 | accessTokenUrl: { 10 | allowNull: false, 11 | type: Sequelize.STRING 12 | }, 13 | accountName: { 14 | allowNull: false, 15 | type: Sequelize.STRING 16 | }, 17 | accountType: { 18 | allowNull: false, 19 | type: Sequelize.STRING 20 | }, 21 | accountUrl: { 22 | allowNull: false, 23 | type: Sequelize.STRING 24 | }, 25 | targetId: { 26 | allowNull: false, 27 | type: Sequelize.INTEGER 28 | }, 29 | targetType: { 30 | allowNull: false, 31 | type: Sequelize.STRING 32 | }, 33 | createdAt: { 34 | allowNull: false, 35 | type: Sequelize.DATE, 36 | defaultValue: Sequelize.literal('NOW()') 37 | }, 38 | updatedAt: { 39 | allowNull: false, 40 | type: Sequelize.DATE, 41 | defaultValue: Sequelize.literal('NOW()') 42 | } 43 | }), 44 | down: (queryInterface, Sequelize) => queryInterface.dropTable('Installations') 45 | }; 46 | -------------------------------------------------------------------------------- /database/migrations/20190209231258-create-repository.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | up: (queryInterface, Sequelize) => queryInterface.createTable('Repositories', { 3 | id: { 4 | allowNull: false, 5 | autoIncrement: false, 6 | primaryKey: true, 7 | type: Sequelize.INTEGER 8 | }, 9 | fullName: { 10 | allowNull: false, 11 | type: Sequelize.STRING 12 | }, 13 | installationId: { 14 | type: Sequelize.INTEGER, 15 | allowNull: false, 16 | onDelete: 'CASCADE', 17 | references: { 18 | model: 'Installations', 19 | key: 'id', 20 | as: 'installationId', 21 | }, 22 | }, 23 | name: { 24 | allowNull: false, 25 | type: Sequelize.STRING 26 | }, 27 | private: { 28 | allowNull: false, 29 | type: Sequelize.BOOLEAN 30 | }, 31 | createdAt: { 32 | allowNull: false, 33 | type: Sequelize.DATE, 34 | defaultValue: Sequelize.literal('NOW()') 35 | }, 36 | updatedAt: { 37 | allowNull: false, 38 | type: Sequelize.DATE, 39 | defaultValue: Sequelize.literal('NOW()') 40 | } 41 | }), 42 | down: (queryInterface, Sequelize) => queryInterface.dropTable('Repositories') 43 | }; 44 | -------------------------------------------------------------------------------- /database/models/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var fs = require('fs'); 4 | var path = require('path'); 5 | var Sequelize = require('sequelize'); 6 | var basename = path.basename(module.filename); 7 | var env = process.env.NODE_ENV || 'development'; 8 | var config = require(__dirname + '/../config/config.js')[env]; 9 | var db = {}; 10 | 11 | let sequelize; 12 | if (config.url) { 13 | sequelize = new Sequelize(config.url); 14 | } else { 15 | sequelize = new Sequelize( 16 | config.database, 17 | config.username, 18 | config.password, 19 | config 20 | ); 21 | } 22 | 23 | fs 24 | .readdirSync(__dirname) 25 | .filter(function(file) { 26 | return (file.indexOf('.') !== 0) && (file !== basename) && (file.slice(-3) === '.js'); 27 | }) 28 | .forEach(function(file) { 29 | var model = sequelize['import'](path.join(__dirname, file)); 30 | db[model.name] = model; 31 | }); 32 | 33 | Object.keys(db).forEach(function(modelName) { 34 | if (db[modelName].associate) { 35 | db[modelName].associate(db); 36 | } 37 | }); 38 | 39 | db.sequelize = sequelize; 40 | db.Sequelize = Sequelize; 41 | 42 | module.exports = db; -------------------------------------------------------------------------------- /database/models/installation.js: -------------------------------------------------------------------------------- 1 | // https://developer.github.com/v3/activity/events/types/#installationevent 2 | 3 | 'use strict'; 4 | module.exports = function(sequelize, DataTypes) { 5 | var Installation = sequelize.define('Installation', { 6 | id: { 7 | allowNull: false, 8 | autoIncrement: false, 9 | primaryKey: true, 10 | type: DataTypes.INTEGER 11 | }, 12 | accessTokenUrl: { 13 | allowNull: false, 14 | type: DataTypes.STRING 15 | }, 16 | accountName: { 17 | allowNull: false, 18 | type: DataTypes.STRING 19 | }, 20 | accountType: { 21 | allowNull: false, 22 | type: DataTypes.STRING 23 | }, 24 | accountUrl: { 25 | allowNull: false, 26 | type: DataTypes.STRING 27 | }, 28 | targetId: { 29 | allowNull: false, 30 | type: DataTypes.INTEGER 31 | }, 32 | targetType: { 33 | allowNull: false, 34 | type: DataTypes.STRING 35 | }, 36 | }, 37 | // { 38 | // classMethods: { 39 | // associate: function(models) { 40 | // // associations can be defined here 41 | // } 42 | // } 43 | // } 44 | ); 45 | 46 | Installation.associate = (models) => { 47 | Installation.hasMany(models.Repository, { 48 | as: 'repositories', 49 | foreignKey: 'installationId', 50 | }); 51 | }; 52 | 53 | return Installation; 54 | }; -------------------------------------------------------------------------------- /database/models/repository.js: -------------------------------------------------------------------------------- 1 | // https://developer.github.com/v3/activity/events/types/#events-api-payload-14 2 | 3 | 'use strict'; 4 | module.exports = function(sequelize, DataTypes) { 5 | var Repository = sequelize.define('Repository', { 6 | id: { 7 | allowNull: false, 8 | autoIncrement: false, 9 | primaryKey: true, 10 | type: DataTypes.INTEGER 11 | }, 12 | fullName: { 13 | allowNull: false, 14 | type: DataTypes.STRING 15 | }, 16 | installationId: { 17 | type: DataTypes.INTEGER, 18 | allowNull: false, 19 | onDelete: 'CASCADE', 20 | references: { 21 | model: 'Installations', 22 | key: 'id', 23 | as: 'installationId', 24 | }, 25 | }, 26 | name: { 27 | allowNull: false, 28 | type: DataTypes.STRING 29 | }, 30 | private: { 31 | allowNull: false, 32 | type: DataTypes.BOOLEAN 33 | } 34 | }, 35 | { 36 | // classMethods: { 37 | // associate: function(models) { 38 | // // associations can be defined here 39 | // } 40 | // }, 41 | 42 | // define the table's name 43 | tableName: 'Repositories', 44 | 45 | // Sequelize instance 46 | sequelize, 47 | } 48 | ); 49 | 50 | Repository.associate = (models) => { 51 | Repository.belongsTo(models.Installation, { 52 | as: 'installation', 53 | foreignKey: 'installationId', 54 | onDelete: 'CASCADE' 55 | }); 56 | }; 57 | 58 | return Repository; 59 | }; -------------------------------------------------------------------------------- /database/seeders/20190209220258-create-installation.js: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Chieze-Franklin/githint-bot/30da47dcb225a184f01328e38cfb27a347f3f60f/database/seeders/20190209220258-create-installation.js -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | // Checks API example 2 | // See: https://developer.github.com/v3/checks/ to learn more 3 | 4 | var cors = require('cors'); 5 | var corsOptions = { 6 | origin: ["https://githint.herokuapp.com", "https://githint.herokuapp.com/"] 7 | }; 8 | 9 | var models = require('./database/models'); 10 | var utils = require('./utils'); 11 | 12 | /** 13 | * This is the main entrypoint to your Probot app 14 | * @param {import('probot').Application} app 15 | */ 16 | module.exports = app => { 17 | const router = app.route('/api'); 18 | router.use(cors(corsOptions)); 19 | router.get('/stats', async (req, res) => { 20 | const installs = await models.Installation.count(); 21 | const repos = await models.Repository.count(); 22 | res.json({ 23 | data: { 24 | installs, 25 | repos 26 | } 27 | }); 28 | }); 29 | 30 | app.on(['check_suite.requested', 'check_run.rerequested'], handleCheckEvents); 31 | app.on([ 32 | 'installation.created', 33 | 'installation.deleted', 34 | 'installation_repositories.added', 35 | 'installation_repositories.removed' 36 | ], 37 | handleInstallationEvents); 38 | 39 | async function handleInstallationEvents(context) { 40 | try { 41 | const { payload } = context; 42 | if (context.name === 'installation') { 43 | if (payload.action === 'created') { 44 | const installation = await models.Installation.create({ 45 | id: payload.installation.id, 46 | accessTokenUrl: payload.installation.access_tokens_url, 47 | accountName: payload.installation.account.login, 48 | accountType: payload.installation.account.type, 49 | accountUrl: payload.installation.account.url, 50 | targetId: payload.installation.target_id, 51 | targetType: payload.installation.target_type, 52 | }); 53 | if (installation) { 54 | payload.repositories.forEach(async repo => { 55 | const repository = await models.Repository.create({ 56 | id: repo.id, 57 | fullName: repo.full_name, 58 | installationId: installation.id, 59 | name: repo.name, 60 | private: repo.private, 61 | }); 62 | }); 63 | } 64 | } else if (payload.action === 'deleted') { 65 | // deleting the installation will cascade delete its repos 66 | await models.Installation.destroy({ 67 | where: { 68 | id: payload.installation.id, 69 | } 70 | }); 71 | } 72 | } else if (context.name === 'installation_repositories') { 73 | payload.repositories_added.forEach(async repo => { 74 | // just in case it already exists 75 | // (which will be the case if payload.installation.repository_selection 76 | // is changing from 'all' to 'selected') 77 | let repository = await models.Repository.destroy({ 78 | where: { 79 | id: repo.id, 80 | } 81 | }); 82 | repository = await models.Repository.create({ 83 | id: repo.id, 84 | fullName: repo.full_name, 85 | installationId: payload.installation.id, 86 | name: repo.name, 87 | private: repo.private, 88 | }); 89 | }); 90 | payload.repositories_removed.forEach(async repo => { 91 | const repository = await models.Repository.destroy({ 92 | where: { 93 | id: repo.id, 94 | } 95 | }); 96 | }); 97 | } 98 | } catch (e) {} 99 | } 100 | 101 | async function handleCheckEvents(context) { 102 | const startTime = new Date() 103 | 104 | // extract info 105 | const { 106 | check_run: checkRun, 107 | check_suite, 108 | repository 109 | } 110 | = context.payload; 111 | let checkSuite = check_suite || checkRun.check_suite; 112 | const { 113 | head_branch: headBranch, 114 | head_sha: headSha, 115 | pull_requests: pullRequests 116 | } 117 | = checkSuite; 118 | 119 | // get the .githint.json file 120 | const gitHintResponse = await checkGitHintFile(context, { 121 | checkRun, 122 | headBranch, 123 | headSha, 124 | repository, 125 | startTime 126 | }); 127 | if (!gitHintResponse.data || !gitHintResponse.data.checks) { 128 | return; 129 | } 130 | const gitHintFile = gitHintResponse.data; 131 | const options = gitHintFile.options || {}; 132 | 133 | // get the branch 134 | var getBranchResponse = await context.github.repos.getBranch({ 135 | owner: repository.owner.login, 136 | repo: repository.name, 137 | branch: headBranch 138 | }); 139 | const branch = { ...getBranchResponse.data }; 140 | 141 | // get the commit 142 | var getCommitResponse = await context.github.repos.getCommit({ 143 | owner: repository.owner.login, 144 | repo: repository.name, 145 | sha: headSha 146 | }); 147 | const commit = { ...getCommitResponse.data }; 148 | 149 | // get the tree 150 | var getTreeResponse = await context.github.gitdata.getTree({ 151 | owner: repository.owner.login, 152 | repo: repository.name, 153 | tree_sha: commit.commit.tree.sha, 154 | recursive: 1 155 | }); 156 | const tree = { ...getTreeResponse.data }; 157 | 158 | // get the pull 159 | let pullResponse = {}; 160 | pullResponse = await checkPull(context, { 161 | checkRun, 162 | headBranch, 163 | headSha, 164 | options, 165 | pull: pullRequests[0], 166 | repository, 167 | scope: { 168 | branch, 169 | commit, 170 | tree 171 | }, 172 | startTime 173 | }); 174 | if (!pullResponse.data && pullResponse.detectPull) { 175 | return; 176 | } 177 | const pull = await getPullInnerObjects(context, { 178 | pull: pullResponse.data 179 | }); 180 | 181 | // run checks 182 | const checkNames = await getChecksToPerform({ 183 | checkRun: checkRun && checkRun.name === 'GitHint: check for pull request' ? undefined : checkRun, 184 | gitHintFile 185 | }); 186 | if (checkNames.length > 0) { 187 | runChecks(context, { 188 | checkRun: checkRun && checkRun.name === 'GitHint: check for pull request' ? undefined : checkRun, 189 | checkNames, 190 | gitHintFile, 191 | headBranch, 192 | headSha, 193 | options, 194 | scope: { 195 | branch, 196 | commit, 197 | pull, 198 | tree 199 | }, 200 | startTime 201 | }); 202 | } 203 | } 204 | 205 | async function runChecks(context, { 206 | checkRun, 207 | checkNames, 208 | gitHintFile, 209 | headBranch, 210 | headSha, 211 | options, 212 | scope, 213 | startTime 214 | }) { 215 | let allChecksPassed = true; 216 | let skippedChecks = []; 217 | for (let i = 0; i < checkNames.length; i++) { 218 | const name = checkNames[i]; 219 | let script = gitHintFile.checks[name]; 220 | let message = ''; 221 | let skip = options.skip || false; 222 | // first, if script is an object get script from script.script 223 | if (typeof script === 'object' && !Array.isArray(script)) { 224 | if (typeof script.skip !== 'undefined') { 225 | skip = script.skip; // override any global skip 226 | } 227 | message = script.message || message; 228 | script = script.script || 'false'; 229 | // if message is an array, join them 230 | if (Array.isArray(message)) { 231 | message = message.join("\n"); 232 | } 233 | } 234 | // if script is an array, join them 235 | if (Array.isArray(script)) { 236 | script = script.filter(line => !!(line.trim())).join("\n"); 237 | } 238 | // if script is string 239 | else if (typeof script === 'string') { 240 | script = `return ${script}`; 241 | } 242 | 243 | if (typeof skip === 'string') { 244 | skip = await utils.runScript(`return ${skip}`, scope); 245 | skip = skip.data || false; 246 | } 247 | // decide if check is to be skipped 248 | if (skip === true) { 249 | skippedChecks.push(name); 250 | continue; 251 | } 252 | 253 | const response = await utils.runScript(script, scope); 254 | let resData = response.data; 255 | let resMessage; 256 | if (response.data && typeof response.data === 'object') { 257 | resData = response.data.result; 258 | resMessage = response.data.message; 259 | } else if (response.error) { 260 | resMessage = response.error.message; 261 | } 262 | allChecksPassed = allChecksPassed && resData; 263 | if (!resData || (checkRun && checkRun.name === name)) { 264 | postCheckResult(context, { 265 | name, 266 | conclusion: !resData ? 'failure' : 'success', 267 | headBranch, 268 | headSha, 269 | startTime, 270 | status: 'completed', 271 | summary: 272 | resMessage 273 | ? resMessage 274 | : `The check '${name}' ${resData === true ? 'passed' : 'failed'}.`, 275 | text: message, 276 | title: name 277 | }); 278 | } 279 | } 280 | if (allChecksPassed && !checkRun) { 281 | const checksSkipped = skippedChecks.length; 282 | postCheckResult(context, { 283 | name: `All checks passed`, 284 | conclusion: 'success', 285 | headBranch, 286 | headSha, 287 | startTime, 288 | status: 'completed', 289 | summary: `All checks that were run passed.`, 290 | text: 291 | `${checksSkipped === 0 ? "No" : checksSkipped} check${checksSkipped < 2 ? " was" : "s were"} skipped.` + 292 | `${checksSkipped === 0 ? "" : "\n" + skippedChecks.map(c => ` * ${c}`).join("\n")}`, 293 | title: `All checks passed` 294 | }); 295 | } 296 | } 297 | async function checkPull(context, {checkRun, headBranch, headSha, options, pull, repository, scope, startTime}) { 298 | let response = {}; 299 | if (pull) { 300 | response = await context.github.pullRequests.get({ 301 | owner: repository.owner.login, 302 | repo: repository.name, 303 | number: pull.number 304 | }); 305 | } 306 | let { detectPull } = options; 307 | if (typeof detectPull === 'string') { 308 | detectPull = await utils.runScript(`return ${detectPull}`, scope); 309 | detectPull = detectPull.data || false; 310 | } 311 | response.detectPull = detectPull; 312 | const name = 'GitHint: check for pull request'; // if u change this here, change it somewhere above (Ctrl+F) 313 | if ((!response.data && detectPull) || (checkRun && checkRun.name === name)) { 314 | postCheckResult(context, { 315 | name, 316 | conclusion: !response.data ? 'failure' : 'success', 317 | headBranch, 318 | headSha, 319 | startTime, 320 | status: 'completed', 321 | summary: 322 | response.error 323 | ? response.error.message 324 | : `The check '${name}' ${!response.data ? 'failed' : 'passed'}.`, 325 | text: 326 | !response.data 327 | ? 'If a code commit was made before a pull request was created ' + 328 | 'then this check will fail. After a pull request is created you can ' + 329 | 're-run this check. If it\'s still failing you may want to wait a ' + 330 | 'few seconds before re-running it.' 331 | : 'The pull request has been detected successfully.', 332 | title: name 333 | }); 334 | } 335 | return response; 336 | } 337 | async function checkGitHintFile(context, {checkRun, headBranch, headSha, repository, startTime}) { 338 | const response = await utils.getGitHintFile(repository.owner.login, repository.name, headBranch); 339 | const name = 'GitHint: check for .githint.json file'; 340 | if (!response.data || (checkRun && checkRun.name === name)) { 341 | postCheckResult(context, { 342 | name, 343 | conclusion: !response.data ? 'failure' : 'success', 344 | headBranch, 345 | headSha, 346 | startTime, 347 | status: 'completed', 348 | summary: response.error ? response.error.message : `The check '${name}' passed.`, 349 | // text: "There's supposed to be a .githint.json file in the root directory", 350 | title: name 351 | }); 352 | } 353 | return response; 354 | } 355 | 356 | async function getChecksToPerform({ checkRun, gitHintFile }) { 357 | if (gitHintFile.checks) { 358 | let checkNames = Object.keys(gitHintFile.checks); 359 | if (checkRun) { 360 | let checkNameToReRun = checkNames.find(name => name === checkRun.name); 361 | if (checkNameToReRun) { 362 | checkNames = [checkNameToReRun]; 363 | } else { 364 | checkNames = []; 365 | } 366 | } 367 | return checkNames; 368 | } 369 | return []; 370 | } 371 | 372 | async function getPullInnerObjects(context, { pull }) { 373 | if (!pull) { 374 | return; 375 | } 376 | 377 | // get the reviews 378 | const reviewsResponse = await context.github.pullRequests.listReviews({ 379 | owner: pull.head.repo.owner.login, 380 | repo: pull.head.repo.name, 381 | number: pull.number 382 | }); 383 | pull.reviews = reviewsResponse.data; 384 | 385 | return pull; 386 | } 387 | 388 | async function postCheckResult (context, { 389 | conclusion, 390 | headBranch, 391 | headSha, 392 | name, 393 | startTime, 394 | status, 395 | summary, 396 | text, 397 | title 398 | }) { 399 | // Probot API note: context.repo() => {username: 'hiimbex', repo: 'testing-things'} 400 | context.github.checks.create(context.repo({ 401 | name, 402 | head_branch: headBranch, 403 | head_sha: headSha, 404 | status, 405 | started_at: startTime, 406 | conclusion, 407 | completed_at: new Date(), 408 | output: { 409 | title, 410 | summary, 411 | text 412 | } 413 | })) 414 | } 415 | 416 | // For more information on building apps: 417 | // https://probot.github.io/docs/ 418 | 419 | // To get your app running against GitHub, see: 420 | // https://probot.github.io/docs/development/ 421 | } 422 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "GitHint", 3 | "version": "1.0.0", 4 | "description": "githint ensures that your pull requests follow specified conventions.", 5 | "author": "Franklin Chieze (https://githint.herokuapp.com)", 6 | "license": "ISC", 7 | "repository": "https://github.com/githint-bot.git", 8 | "homepage": "https://github.com/githint-bot", 9 | "bugs": "https://github.com/githint-bot/issues", 10 | "keywords": [ 11 | "probot", 12 | "github", 13 | "probot-app", 14 | "githint", 15 | "githint-bot" 16 | ], 17 | "scripts": { 18 | "db:migrate": "./node_modules/.bin/sequelize db:migrate", 19 | "db:migrate:test": "NODE_ENV=test ./node_modules/.bin/sequelize db:migrate", 20 | "db:reset": "./node_modules/.bin/sequelize db:migrate:undo:all && ./node_modules/.bin/sequelize db:migrate", 21 | "db:reset:test": "NODE_ENV=test ./node_modules/.bin/sequelize db:migrate:undo:all && NODE_ENV=test ./node_modules/.bin/sequelize db:migrate", 22 | "dev": "nodemon", 23 | "heroku-postbuild": "npm run db:migrate", 24 | "start": "probot run ./index.js", 25 | "lint": "standard --fix", 26 | "test": "jest && standard", 27 | "test:watch": "jest --watch --notify --notifyMode=change --coverage" 28 | }, 29 | "dependencies": { 30 | "cors": "^2.8.5", 31 | "pg": "^7.11.0", 32 | "pg-hstore": "^2.3.3", 33 | "probot": "^7.2.0", 34 | "request": "^2.88.0", 35 | "request-promise-native": "^1.0.7", 36 | "sequelize": "^5.15.1" 37 | }, 38 | "devDependencies": { 39 | "jest": "^24.0.0", 40 | "nock": "^10.0.0", 41 | "nodemon": "^1.17.2", 42 | "sequelize-cli": "^5.5.0", 43 | "smee-client": "^1.0.2", 44 | "standard": "^12.0.1" 45 | }, 46 | "engines": { 47 | "node": ">= 8.3.0" 48 | }, 49 | "standard": { 50 | "env": [ 51 | "jest" 52 | ] 53 | }, 54 | "nodemonConfig": { 55 | "exec": "npm start", 56 | "watch": [ 57 | ".env", 58 | "." 59 | ] 60 | }, 61 | "jest": { 62 | "testEnvironment": "node" 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /test/fixtures/check_run.created.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "My app!", 3 | "head_branch": "hiimbex-patch-46", 4 | "head_sha": "50e5628cda538bcbb6f3fec3edebe4bb5afb3891", 5 | "status": "completed", 6 | "started_at": "2018-10-05T17:35:21.594Z", 7 | "conclusion": "success", 8 | "completed_at": "2018-10-05T17:35:53.683Z", 9 | "output": { 10 | "title": "Probot check!", 11 | "summary": "The check has passed!" 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /test/fixtures/check_suite.requested.json: -------------------------------------------------------------------------------- 1 | { 2 | "action": "requested", 3 | "check_suite": { 4 | "id": 17597619, 5 | "node_id": "MDEwOkNoZWNrU3VpdGUxNzU5NzYxOQ==", 6 | "head_branch": "hiimbex-patch-46", 7 | "head_sha": "50e5628cda538bcbb6f3fec3edebe4bb5afb3891", 8 | "status": "queued", 9 | "conclusion": null, 10 | "url": "https://api.github.com/repos/hiimbex/testing-things/check-suites/17597619", 11 | "before": "f5c621190066c1b2333b3e2ff01742b6696be443", 12 | "after": "50e5628cda538bcbb6f3fec3edebe4bb5afb3891", 13 | "pull_requests": [ 14 | { 15 | "url": "https://api.github.com/repos/hiimbex/testing-things/pulls/121", 16 | "id": 220762830, 17 | "number": 121, 18 | "head": { 19 | "ref": "hiimbex-patch-46", 20 | "sha": "50e5628cda538bcbb6f3fec3edebe4bb5afb3891", 21 | "repo": { 22 | "id": 95162387, 23 | "url": "https://api.github.com/repos/hiimbex/testing-things", 24 | "name": "testing-things" 25 | } 26 | }, 27 | "base": { 28 | "ref": "master", 29 | "sha": "43fd9749bea5ea6de51bdf01b3b1e7264cbe83bc", 30 | "repo": { 31 | "id": 95162387, 32 | "url": "https://api.github.com/repos/hiimbex/testing-things", 33 | "name": "testing-things" 34 | } 35 | } 36 | } 37 | ], 38 | "app": { 39 | "id": 18586, 40 | "node_id": "MDM6QXBwMTg1ODY=", 41 | "owner": { 42 | "login": "hiimbex", 43 | "id": 13410355, 44 | "node_id": "MDQ6VXNlcjEzNDEwMzU1", 45 | "avatar_url": "https://avatars1.githubusercontent.com/u/13410355?v=4", 46 | "gravatar_id": "", 47 | "url": "https://api.github.com/users/hiimbex", 48 | "html_url": "https://github.com/hiimbex", 49 | "followers_url": "https://api.github.com/users/hiimbex/followers", 50 | "following_url": "https://api.github.com/users/hiimbex/following{/other_user}", 51 | "gists_url": "https://api.github.com/users/hiimbex/gists{/gist_id}", 52 | "starred_url": "https://api.github.com/users/hiimbex/starred{/owner}{/repo}", 53 | "subscriptions_url": "https://api.github.com/users/hiimbex/subscriptions", 54 | "organizations_url": "https://api.github.com/users/hiimbex/orgs", 55 | "repos_url": "https://api.github.com/users/hiimbex/repos", 56 | "events_url": "https://api.github.com/users/hiimbex/events{/privacy}", 57 | "received_events_url": "https://api.github.com/users/hiimbex/received_events", 58 | "type": "User", 59 | "site_admin": false 60 | }, 61 | "name": "checksoo", 62 | "description": "A Probot app", 63 | "external_url": "https://github.com/hiimbex/checksoo", 64 | "html_url": "https://github.com/apps/checksoo", 65 | "created_at": "2018-10-05T16:46:52Z", 66 | "updated_at": "2018-10-05T16:46:52Z" 67 | }, 68 | "created_at": "2018-10-05T16:53:21Z", 69 | "updated_at": "2018-10-05T16:53:21Z", 70 | "latest_check_runs_count": 0, 71 | "check_runs_url": "https://api.github.com/repos/hiimbex/testing-things/check-suites/17597619/check-runs", 72 | "head_commit": { 73 | "id": "50e5628cda538bcbb6f3fec3edebe4bb5afb3891", 74 | "tree_id": "1d785b00b8f8163d656ffedddcb773a7077592f4", 75 | "message": "Update README.md", 76 | "timestamp": "2018-10-05T16:53:19Z", 77 | "author": { 78 | "name": "Bex Warner", 79 | "email": "bexmwarner@gmail.com" 80 | }, 81 | "committer": { 82 | "name": "GitHub", 83 | "email": "noreply@github.com" 84 | } 85 | } 86 | }, 87 | "repository": { 88 | "id": 95162387, 89 | "node_id": "MDEwOlJlcG9zaXRvcnk5NTE2MjM4Nw==", 90 | "name": "testing-things", 91 | "full_name": "hiimbex/testing-things", 92 | "private": false, 93 | "owner": { 94 | "login": "hiimbex", 95 | "id": 13410355, 96 | "node_id": "MDQ6VXNlcjEzNDEwMzU1", 97 | "avatar_url": "https://avatars1.githubusercontent.com/u/13410355?v=4", 98 | "gravatar_id": "", 99 | "url": "https://api.github.com/users/hiimbex", 100 | "html_url": "https://github.com/hiimbex", 101 | "followers_url": "https://api.github.com/users/hiimbex/followers", 102 | "following_url": "https://api.github.com/users/hiimbex/following{/other_user}", 103 | "gists_url": "https://api.github.com/users/hiimbex/gists{/gist_id}", 104 | "starred_url": "https://api.github.com/users/hiimbex/starred{/owner}{/repo}", 105 | "subscriptions_url": "https://api.github.com/users/hiimbex/subscriptions", 106 | "organizations_url": "https://api.github.com/users/hiimbex/orgs", 107 | "repos_url": "https://api.github.com/users/hiimbex/repos", 108 | "events_url": "https://api.github.com/users/hiimbex/events{/privacy}", 109 | "received_events_url": "https://api.github.com/users/hiimbex/received_events", 110 | "type": "User", 111 | "site_admin": false 112 | }, 113 | "html_url": "https://github.com/hiimbex/testing-things", 114 | "description": "this repo is radical dude ugh", 115 | "fork": false, 116 | "url": "https://api.github.com/repos/hiimbex/testing-things", 117 | "forks_url": "https://api.github.com/repos/hiimbex/testing-things/forks", 118 | "keys_url": "https://api.github.com/repos/hiimbex/testing-things/keys{/key_id}", 119 | "collaborators_url": "https://api.github.com/repos/hiimbex/testing-things/collaborators{/collaborator}", 120 | "teams_url": "https://api.github.com/repos/hiimbex/testing-things/teams", 121 | "hooks_url": "https://api.github.com/repos/hiimbex/testing-things/hooks", 122 | "issue_events_url": "https://api.github.com/repos/hiimbex/testing-things/issues/events{/number}", 123 | "events_url": "https://api.github.com/repos/hiimbex/testing-things/events", 124 | "assignees_url": "https://api.github.com/repos/hiimbex/testing-things/assignees{/user}", 125 | "branches_url": "https://api.github.com/repos/hiimbex/testing-things/branches{/branch}", 126 | "tags_url": "https://api.github.com/repos/hiimbex/testing-things/tags", 127 | "blobs_url": "https://api.github.com/repos/hiimbex/testing-things/git/blobs{/sha}", 128 | "git_tags_url": "https://api.github.com/repos/hiimbex/testing-things/git/tags{/sha}", 129 | "git_refs_url": "https://api.github.com/repos/hiimbex/testing-things/git/refs{/sha}", 130 | "trees_url": "https://api.github.com/repos/hiimbex/testing-things/git/trees{/sha}", 131 | "statuses_url": "https://api.github.com/repos/hiimbex/testing-things/statuses/{sha}", 132 | "languages_url": "https://api.github.com/repos/hiimbex/testing-things/languages", 133 | "stargazers_url": "https://api.github.com/repos/hiimbex/testing-things/stargazers", 134 | "contributors_url": "https://api.github.com/repos/hiimbex/testing-things/contributors", 135 | "subscribers_url": "https://api.github.com/repos/hiimbex/testing-things/subscribers", 136 | "subscription_url": "https://api.github.com/repos/hiimbex/testing-things/subscription", 137 | "commits_url": "https://api.github.com/repos/hiimbex/testing-things/commits{/sha}", 138 | "git_commits_url": "https://api.github.com/repos/hiimbex/testing-things/git/commits{/sha}", 139 | "comments_url": "https://api.github.com/repos/hiimbex/testing-things/comments{/number}", 140 | "issue_comment_url": "https://api.github.com/repos/hiimbex/testing-things/issues/comments{/number}", 141 | "contents_url": "https://api.github.com/repos/hiimbex/testing-things/contents/{+path}", 142 | "compare_url": "https://api.github.com/repos/hiimbex/testing-things/compare/{base}...{head}", 143 | "merges_url": "https://api.github.com/repos/hiimbex/testing-things/merges", 144 | "archive_url": "https://api.github.com/repos/hiimbex/testing-things/{archive_format}{/ref}", 145 | "downloads_url": "https://api.github.com/repos/hiimbex/testing-things/downloads", 146 | "issues_url": "https://api.github.com/repos/hiimbex/testing-things/issues{/number}", 147 | "pulls_url": "https://api.github.com/repos/hiimbex/testing-things/pulls{/number}", 148 | "milestones_url": "https://api.github.com/repos/hiimbex/testing-things/milestones{/number}", 149 | "notifications_url": "https://api.github.com/repos/hiimbex/testing-things/notifications{?since,all,participating}", 150 | "labels_url": "https://api.github.com/repos/hiimbex/testing-things/labels{/name}", 151 | "releases_url": "https://api.github.com/repos/hiimbex/testing-things/releases{/id}", 152 | "deployments_url": "https://api.github.com/repos/hiimbex/testing-things/deployments", 153 | "created_at": "2017-06-22T22:38:49Z", 154 | "updated_at": "2018-09-15T14:44:14Z", 155 | "pushed_at": "2018-10-05T16:53:21Z", 156 | "git_url": "git://github.com/hiimbex/testing-things.git", 157 | "ssh_url": "git@github.com:hiimbex/testing-things.git", 158 | "clone_url": "https://github.com/hiimbex/testing-things.git", 159 | "svn_url": "https://github.com/hiimbex/testing-things", 160 | "homepage": null, 161 | "size": 99, 162 | "stargazers_count": 0, 163 | "watchers_count": 0, 164 | "language": "JavaScript", 165 | "has_issues": true, 166 | "has_projects": true, 167 | "has_downloads": true, 168 | "has_wiki": true, 169 | "has_pages": false, 170 | "forks_count": 2, 171 | "mirror_url": null, 172 | "archived": false, 173 | "open_issues_count": 75, 174 | "license": null, 175 | "forks": 2, 176 | "open_issues": 75, 177 | "watchers": 0, 178 | "default_branch": "master" 179 | }, 180 | "sender": { 181 | "login": "hiimbex", 182 | "id": 13410355, 183 | "node_id": "MDQ6VXNlcjEzNDEwMzU1", 184 | "avatar_url": "https://avatars1.githubusercontent.com/u/13410355?v=4", 185 | "gravatar_id": "", 186 | "url": "https://api.github.com/users/hiimbex", 187 | "html_url": "https://github.com/hiimbex", 188 | "followers_url": "https://api.github.com/users/hiimbex/followers", 189 | "following_url": "https://api.github.com/users/hiimbex/following{/other_user}", 190 | "gists_url": "https://api.github.com/users/hiimbex/gists{/gist_id}", 191 | "starred_url": "https://api.github.com/users/hiimbex/starred{/owner}{/repo}", 192 | "subscriptions_url": "https://api.github.com/users/hiimbex/subscriptions", 193 | "organizations_url": "https://api.github.com/users/hiimbex/orgs", 194 | "repos_url": "https://api.github.com/users/hiimbex/repos", 195 | "events_url": "https://api.github.com/users/hiimbex/events{/privacy}", 196 | "received_events_url": "https://api.github.com/users/hiimbex/received_events", 197 | "type": "User", 198 | "site_admin": false 199 | }, 200 | "installation": { 201 | "id": 2, 202 | "node_id": "MDIzOkludGVncmF0aW9uSW5zdGFsbGF0aW9uMzY3NDI4" 203 | } 204 | } 205 | -------------------------------------------------------------------------------- /test/index.test.js: -------------------------------------------------------------------------------- 1 | const nock = require('nock') 2 | // Requiring our app implementation 3 | const myProbotApp = require('..') 4 | const { Probot } = require('probot') 5 | // Requiring our fixtures 6 | const checkSuitePayload = require('./fixtures/check_suite.requested') 7 | const checkRunSuccess = require('./fixtures/check_run.created') 8 | 9 | nock.disableNetConnect() 10 | 11 | describe('My Probot app', () => { 12 | let probot 13 | 14 | beforeEach(() => { 15 | probot = new Probot({}) 16 | // Load our app into probot 17 | const app = probot.load(myProbotApp) 18 | 19 | // just return a test token 20 | app.app = () => 'test' 21 | }) 22 | 23 | test('creates a passing check', async () => { 24 | nock('https://api.github.com') 25 | .post('/app/installations/2/access_tokens') 26 | .reply(200, { token: 'test' }) 27 | 28 | nock('https://api.github.com') 29 | .post('/repos/hiimbex/testing-things/check-runs', (body) => { 30 | body.started_at = '2018-10-05T17:35:21.594Z' 31 | body.completed_at = '2018-10-05T17:35:53.683Z' 32 | expect(body).toMatchObject(checkRunSuccess) 33 | return true 34 | }) 35 | .reply(200) 36 | 37 | // Receive a webhook event 38 | await probot.receive({ name: 'check_suite', payload: checkSuitePayload }) 39 | }) 40 | }) 41 | 42 | // For more information about testing with Jest see: 43 | // https://facebook.github.io/jest/ 44 | 45 | // For more information about testing with Nock see: 46 | // https://github.com/nock/nock 47 | -------------------------------------------------------------------------------- /utils.js: -------------------------------------------------------------------------------- 1 | const request = require('request-promise-native'); 2 | const vm = require('vm'); 3 | 4 | module.exports = { 5 | async getGitHintFile(owner, repo, branch) { 6 | try { 7 | let data = await request({ 8 | url: `https://raw.githubusercontent.com/${owner}/${repo}/${branch}/.ghint.json`, 9 | method: 'GET' 10 | }); 11 | return { data: JSON.parse(data) }; 12 | } catch (error) { 13 | try { 14 | let data = await request({ 15 | url: `https://raw.githubusercontent.com/${owner}/${repo}/${branch}/.githint.json`, 16 | method: 'GET' 17 | }); 18 | return { data: JSON.parse(data) }; 19 | } catch (error) { 20 | return { error }; 21 | } 22 | } 23 | }, 24 | async runScript(source, scope) { 25 | try { 26 | const script = new vm.Script(`(function(){${source}})()`); 27 | const context = new vm.createContext(scope); 28 | let data = script.runInContext(context); 29 | return { data }; 30 | } catch (error) { 31 | return { error }; 32 | } 33 | } 34 | } 35 | --------------------------------------------------------------------------------