├── src ├── tasks │ └── .gitkeep ├── task-runner │ ├── mocks │ │ ├── folder-with-no-tasks │ │ │ └── .gitkeep │ │ └── example-tasks │ │ │ ├── task-1 │ │ │ ├── metadata.json │ │ │ └── index.js │ │ │ ├── task-2 │ │ │ ├── metadata.json │ │ │ └── index.js │ │ │ ├── rejecting-task │ │ │ ├── index.js │ │ │ └── metadata.json │ │ │ └── task-without-metadata │ │ │ └── index.js │ ├── index.js │ └── spec.js ├── __tests__ │ ├── setup.spec.js │ └── webhooks.spec.js ├── index.js └── scripts │ └── run-command │ └── index.js ├── .dockerignore ├── .env-example ├── screenshot.png ├── jest.config.js ├── examples ├── async-example │ ├── metadata.json │ └── index.js └── npm-test │ └── index.js ├── .travis.yml ├── codecov.yml ├── Dockerfile ├── release.sh ├── .gitignore ├── package.json ├── LICENSE └── README.md /src/tasks/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | node_modules -------------------------------------------------------------------------------- /.env-example: -------------------------------------------------------------------------------- 1 | GITHUB_TOKEN= 2 | GITHUB_SECRET= -------------------------------------------------------------------------------- /src/task-runner/mocks/folder-with-no-tasks/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boyney123/status-checks/HEAD/screenshot.png -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | coveragePathIgnorePatterns: ["/node_modules/", "/src/task-runner/mocks/*"] 3 | }; 4 | -------------------------------------------------------------------------------- /src/task-runner/mocks/example-tasks/task-1/metadata.json: -------------------------------------------------------------------------------- 1 | { 2 | "title": "task-1", 3 | "description": "My custom task" 4 | } 5 | -------------------------------------------------------------------------------- /src/task-runner/mocks/example-tasks/task-2/metadata.json: -------------------------------------------------------------------------------- 1 | { 2 | "title": "task-2", 3 | "description": "My custom task" 4 | } 5 | -------------------------------------------------------------------------------- /examples/async-example/metadata.json: -------------------------------------------------------------------------------- 1 | { 2 | "title": "async-example", 3 | "description": "Async example of GitHub function" 4 | } 5 | -------------------------------------------------------------------------------- /src/task-runner/mocks/example-tasks/task-1/index.js: -------------------------------------------------------------------------------- 1 | module.exports = () => { 2 | return Promise.resolve("Finished task-1"); 3 | }; 4 | -------------------------------------------------------------------------------- /src/task-runner/mocks/example-tasks/task-2/index.js: -------------------------------------------------------------------------------- 1 | module.exports = () => { 2 | return Promise.resolve("Finished task-2"); 3 | }; 4 | -------------------------------------------------------------------------------- /src/task-runner/mocks/example-tasks/rejecting-task/index.js: -------------------------------------------------------------------------------- 1 | module.exports = () => { 2 | return Promise.reject("Failed task-1"); 3 | }; 4 | -------------------------------------------------------------------------------- /src/task-runner/mocks/example-tasks/rejecting-task/metadata.json: -------------------------------------------------------------------------------- 1 | { 2 | "title": "failed-task", 3 | "description": "Failed Task" 4 | } 5 | -------------------------------------------------------------------------------- /src/task-runner/mocks/example-tasks/task-without-metadata/index.js: -------------------------------------------------------------------------------- 1 | module.exports = () => { 2 | return Promise.resolve("Finished task"); 3 | }; 4 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "12" 4 | - "10" 5 | install: 6 | - npm install -g codecov 7 | - npm install 8 | script: 9 | - npm test 10 | - codecov 11 | -------------------------------------------------------------------------------- /codecov.yml: -------------------------------------------------------------------------------- 1 | coverage: 2 | status: 3 | project: 4 | default: 5 | # Fail the status if coverage drops by >= 5% 6 | threshold: 5 7 | patch: 8 | default: 9 | threshold: 5 10 | 11 | comment: 12 | layout: diff 13 | require_changes: yes 14 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:12.3.1 2 | 3 | LABEL maintainer="David Boyne " 4 | 5 | WORKDIR /usr/src/pullreq 6 | 7 | COPY src /usr/src/pullreq/src 8 | COPY package.json /usr/src/pullreq 9 | COPY .env /usr/src/pullreq 10 | 11 | RUN npm install 12 | 13 | # RUN chown -R nobody /usr/src/pullreq 14 | # USER nobody 15 | 16 | CMD [ "npm", "start" ] 17 | 18 | -------------------------------------------------------------------------------- /release.sh: -------------------------------------------------------------------------------- 1 | set -ex 2 | # SET THE FOLLOWING VARIABLES 3 | # docker hub username 4 | USERNAME=boyney123 5 | # image name 6 | IMAGE=pullreq 7 | # run build 8 | docker build -t $USERNAME/$IMAGE:latest . 9 | # tag it 10 | git tag -a "$1" -m "version $1" 11 | git push 12 | git push --tags 13 | 14 | docker tag $USERNAME/$IMAGE:latest $USERNAME/$IMAGE:$1 15 | # push it 16 | docker push $USERNAME/$IMAGE:latest 17 | docker push $USERNAME/$IMAGE:$1 -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | ### Node ### 3 | # Logs 4 | logs 5 | *.log 6 | npm-debug.log* 7 | yarn-debug.log* 8 | yarn-error.log* 9 | 10 | # Directory for instrumented libs generated by jscoverage/JSCover 11 | lib-cov 12 | 13 | # Coverage directory used by tools like istanbul 14 | coverage 15 | 16 | # nyc test coverage 17 | .nyc_output 18 | 19 | # Compiled binary addons (https://nodejs.org/api/addons.html) 20 | build/Release 21 | 22 | # Dependency directories 23 | node_modules/ 24 | 25 | # dotenv environment variables file 26 | .env 27 | .env.test 28 | 29 | TODO.js -------------------------------------------------------------------------------- /examples/npm-test/index.js: -------------------------------------------------------------------------------- 1 | module.exports = async options => { 2 | const { setDescription, runCommand } = options; 3 | 4 | // Updates the github status 5 | await setDescription("Running testing"); 6 | 7 | try { 8 | // run custom script 9 | await runCommand("npm run test"); 10 | 11 | // update status 12 | await setDescription("Npm test passed"); 13 | 14 | // Status will be marked as successful 15 | return Promise.resolve(); 16 | } catch (error) { 17 | // update status 18 | await setDescription("Failed to run test"); 19 | 20 | // status marked as failed 21 | return Promise.reject(); 22 | } 23 | }; 24 | -------------------------------------------------------------------------------- /examples/async-example/index.js: -------------------------------------------------------------------------------- 1 | module.exports = options => { 2 | const { setDescription } = options; 3 | 4 | // pretend to do something... 5 | const someAsyncTask = info => { 6 | // Update the Github status 7 | setDescription(info); 8 | return new Promise(function(resolve) { 9 | setTimeout(resolve, 3000); 10 | }); 11 | }; 12 | 13 | return new Promise(async (resolve, reject) => { 14 | await someAsyncTask("Setting up CI project..."); 15 | await someAsyncTask("Running eslint..."); 16 | await someAsyncTask("Running unit tests..."); 17 | await someAsyncTask("Running e2e tests..."); 18 | resolve("Everything passed"); 19 | }); 20 | }; 21 | -------------------------------------------------------------------------------- /src/__tests__/setup.spec.js: -------------------------------------------------------------------------------- 1 | const octokit = require("@octokit/rest"); 2 | const webhooks = require("@octokit/webhooks"); 3 | const runner = require("../task-runner"); 4 | 5 | process.env.GITHUB_SECRET = "my_secret"; 6 | process.env.GITHUB_TOKEN = "my_token"; 7 | 8 | jest.mock("@octokit/rest", () => { 9 | return jest.fn(); 10 | }); 11 | 12 | jest.mock("@octokit/webhooks", () => { 13 | return jest.fn(() => { 14 | return { 15 | on: jest.fn() 16 | }; 17 | }); 18 | }); 19 | 20 | jest.mock("../task-runner", () => { 21 | return jest.fn(); 22 | }); 23 | 24 | describe("pullreq", () => { 25 | describe("setup", () => { 26 | it("sets up ocotokit with the required params", () => { 27 | const app = require("../"); 28 | expect(octokit).toHaveBeenCalledWith({ auth: "my_token", baseUrl: "https://api.github.com", secret: "my_secret", userAgent: "pullreq" }); 29 | }); 30 | }); 31 | }); 32 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "pullreq", 3 | "version": "0.0.1", 4 | "description": "Small platform that supports functions as github status checks", 5 | "main": "index.js", 6 | "scripts": { 7 | "start": "node src/index.js", 8 | "test": "GITHUB_TOKEN=token NODE_ENV=test jest --coverage --silent", 9 | "test:watch": "GITHUB_TOKEN=token NODE_ENV=test jest --watchAll --notify --notifyMode=change" 10 | }, 11 | "keywords": [ 12 | "docker", 13 | "github", 14 | "status-api", 15 | "functions" 16 | ], 17 | "author": "", 18 | "license": "ISC", 19 | "dependencies": { 20 | "@octokit/rest": "^16.27.0", 21 | "@octokit/webhooks": "^6.2.2", 22 | "chalk": "^2.4.2", 23 | "child-process-promise": "^2.2.1", 24 | "cli-spinners": "^2.1.0", 25 | "log-update": "^3.2.0" 26 | }, 27 | "devDependencies": { 28 | "dotenv": "^8.0.0", 29 | "jest": "^24.8.0", 30 | "request": "^2.88.0", 31 | "request-promise": "^4.2.4" 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | require("dotenv").config(); 2 | 3 | const fs = require("fs"); 4 | const path = require("path"); 5 | const runner = require("./task-runner"); 6 | const chalk = require("chalk"); 7 | const log = console.log; 8 | 9 | const WebhooksApi = require("@octokit/webhooks"); 10 | const webhooks = new WebhooksApi({ 11 | secret: process.env.GITHUB_SECRET 12 | }); 13 | 14 | const Octokit = require("@octokit/rest"); 15 | 16 | const octokit = Octokit({ 17 | secret: process.env.GITHUB_SECRET, 18 | auth: process.env.GITHUB_TOKEN, 19 | userAgent: "pullreq", 20 | baseUrl: "https://api.github.com" 21 | }); 22 | 23 | webhooks.on("pull_request", async ({ id, name, payload }) => { 24 | try { 25 | log(chalk.green("New pull_request event has come in. Running tasks...")); 26 | runner({ octokit, payload }); 27 | } catch (error) { 28 | console.log(error); 29 | } 30 | }); 31 | 32 | if (process.env.NODE_ENV !== "test") { 33 | require("http") 34 | .createServer(webhooks.middleware) 35 | .listen(3000); 36 | console.log("Listening on port 3000"); 37 | } 38 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) David Boyne 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/scripts/run-command/index.js: -------------------------------------------------------------------------------- 1 | const { exec } = require("child-process-promise"); 2 | const logUpdate = require("log-update"); 3 | const { dots } = require("cli-spinners"); 4 | const chalk = require("chalk"); 5 | 6 | const logPromise = async (promise, text, isLongRunningTask = false) => { 7 | const { frames, interval } = dots; 8 | 9 | let index = 0; 10 | 11 | const inProgressMessage = `- this may take a few ${isLongRunningTask ? "minutes" : "seconds"}`; 12 | 13 | const id = setInterval(() => { 14 | index = ++index % frames.length; 15 | logUpdate(`${chalk.yellow(frames[index])} ${text} ${chalk.gray(inProgressMessage)}`); 16 | }, interval); 17 | 18 | try { 19 | const returnValue = await promise; 20 | 21 | clearInterval(id); 22 | 23 | logUpdate(`${chalk.green("✓")} ${text}`); 24 | logUpdate.done(); 25 | 26 | return returnValue; 27 | } catch (error) { 28 | logUpdate.clear(); 29 | 30 | throw error; 31 | } 32 | }; 33 | 34 | const runCommand = async (command, options) => { 35 | return new Promise(async (resolve, reject) => { 36 | try { 37 | const result = await exec(command, options); 38 | const { stdout, stderr } = result; 39 | console.log(stdout); 40 | console.log(stderr); 41 | resolve(); 42 | } catch (error) { 43 | console.error(error); 44 | reject(error); 45 | } 46 | }); 47 | }; 48 | 49 | module.exports = runCommand; 50 | -------------------------------------------------------------------------------- /src/__tests__/webhooks.spec.js: -------------------------------------------------------------------------------- 1 | const octokit = require("@octokit/rest"); 2 | const webhooks = require("@octokit/webhooks"); 3 | const runner = require("../task-runner"); 4 | 5 | process.env.GITHUB_SECRET = "my_secret"; 6 | process.env.GITHUB_TOKEN = "my_token"; 7 | 8 | jest.mock("@octokit/rest", () => { 9 | return jest.fn(); 10 | }); 11 | 12 | jest.mock("@octokit/webhooks", () => { 13 | return jest.fn(() => { 14 | return { 15 | on: jest.fn() 16 | }; 17 | }); 18 | }); 19 | 20 | jest.mock("../task-runner", () => { 21 | return jest.fn(); 22 | }); 23 | 24 | describe("webhooks", () => { 25 | describe("webhook", () => { 26 | it("when a GitHub pull_request payload comes in the `runner` is executed with the octokit instance and payload", () => { 27 | const octokitMock = jest.fn(); 28 | let simulatePullRequestEvent, webhookEvent; 29 | 30 | webhooks.mockImplementation(() => { 31 | return { 32 | on: (event, callback) => { 33 | webhookEvent = event; 34 | simulatePullRequestEvent = callback; 35 | } 36 | }; 37 | }); 38 | 39 | octokit.mockImplementation(() => { 40 | return octokitMock; 41 | }); 42 | 43 | const app = require("../"); 44 | const payload = { test: true }; 45 | 46 | simulatePullRequestEvent({ id: "1", name: "test", payload }); 47 | 48 | expect(webhooks).toHaveBeenCalledWith({ secret: "my_secret" }); 49 | expect(webhookEvent).toEqual("pull_request"); 50 | expect(runner).toHaveBeenCalledWith({ octokit: octokitMock, payload }); 51 | }); 52 | }); 53 | }); 54 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
2 | 3 |

status-checks: GitHub status checks as JS functions.

4 | 5 |

Quickly get setup and integrate with GitHub status checks. 6 | You provide the functions and we will do the rest.

7 | 8 |
9 | 10 | [![Travis](https://img.shields.io/travis/boyney123/status-checks/master.svg)](https://travis-ci.org/boyney123/status-checks) 11 | [![CodeCov](https://codecov.io/gh/boyney123/status-checks/branch/master/graph/badge.svg?token=AoXW3EFgMP)](https://codecov.io/gh/boyney123/status-checks) 12 | [![MIT License][license-badge]][license] 13 | [![PRs Welcome][prs-badge]][prs] 14 | 15 | [![Watch on GitHub][github-watch-badge]][github-watch] 16 | [![Star on GitHub][github-star-badge]][github-star] 17 | [![Tweet][twitter-badge]][twitter] 18 | 19 | [Donate ☕](https://www.paypal.me/boyney123/5) 20 | 21 |
22 | header 23 |

Features: GitHub integration, functions, docker support, exposed GitHub API helpers, setup within minutes, easily host and more...

24 | 25 | [Read the Docs](https://status-checks.netlify.com/) | [Edit the Docs](https://github.com/boyney123/status-checks) 26 | 27 |
28 | 29 |
30 | 31 | 32 | ## The problem 33 | 34 | Code quality is important. To help with code quality we have various tasks / scripts / apps and bots that we need to run to assert our quality does not drop. 35 | 36 | Continuous integration is a great way to make sure our quality does not drop and we have confidence with our software. 37 | 38 | GitHub have done a great job allowing us to integrate with the platform and run various checks before code gets merged. You can automate these checks with GitHub using status checks and GitHub actions. 39 | 40 | In the past I have setup multiple projects with GitHub to integrate and run various commands through status checks, and I wanted to create an application that could handle most the integration for me. I wanted to create a platform that allowed me to specify the functions I want to run on each status check. 41 | 42 | This is when `status-checks` was born. `status-checks` takes a folder of functions (defined by you) and runs them through any pull request that comes in and integrated back with GitHub. 43 | 44 | [You can read the documentation on how to get started](https://status-checks.netlify.com/docs/how-it-works). 45 | 46 | ## This solution 47 | 48 | `status-checks` was built and designed to help developers integrate with GitHub status checks easier. You define the functions to run and this project will do the rest. 49 | 50 | ## Documentation 51 | 52 | - [Getting Started](https://status-checks.netlify.com/docs/getting-started/installation) 53 | - [Contributing](https://status-checks.netlify.com/docs/contributing/contributing) 54 | 55 | :star: Huge thanks to the [all-contributors project](https://allcontributors.org/) for allowing us to use their theme for the documentation website. :star: 56 | 57 | ## Tools 58 | 59 | - [@octokit/rest](https://github.com/octokit/rest.js) 60 | - [@octokit/webhooks](https://github.com/octokit/webhooks.js) 61 | 62 | ### Testing 63 | 64 | - [jest](https://jestjs.io/) 65 | 66 | ## Contributing 67 | 68 | If you have any questions, features or issues please raise any issue or pull requests you like. 69 | 70 | [spectrum-badge]: https://withspectrum.github.io/badge/badge.svg 71 | [spectrum]: https://spectrum.chat/explore-tech 72 | [license-badge]: https://img.shields.io/github/license/boyney123/status-checks.svg 73 | [license]: https://github.com/boyney123/status-checks/blob/master/LICENSE 74 | [prs-badge]: https://img.shields.io/badge/PRs-welcome-brightgreen.svg?style=flat-square 75 | [prs]: http://makeastatus-checksuest.com 76 | [github-watch-badge]: https://img.shields.io/github/watchers/boyney123/status-checks.svg?style=social 77 | [github-watch]: https://github.com/boyney123/status-checks/watchers 78 | [twitter]: https://twitter.com/intent/tweet?text=Check%20out%20status-checks%20by%20%40boyney123%20https%3A%2F%2Fgithub.com%2Fboyney123%2Fstatus-checks%20%F0%9F%91%8D 79 | [twitter-badge]: https://img.shields.io/twitter/url/https/github.com/boyney123/status-checks.svg?style=social 80 | [github-star-badge]: https://img.shields.io/github/stars/boyney123/status-checks.svg?style=social 81 | [github-star]: https://github.com/boyney123/status-checks/stargazers 82 | 83 | # Donating 84 | 85 | If you find this tool useful, feel free to buy me a ☕ 👍 86 | 87 | [Buy a drink](https://www.paypal.me/boyney123/5) 88 | 89 | # License 90 | 91 | MIT. 92 | -------------------------------------------------------------------------------- /src/task-runner/index.js: -------------------------------------------------------------------------------- 1 | const fs = require("fs"); 2 | const path = require("path"); 3 | const chalk = require("chalk"); 4 | const runCommand = require("../scripts/run-command"); 5 | const log = console.log; 6 | 7 | const GITHUB_TOKEN = process.env.GITHUB_TOKEN; 8 | 9 | const tasksDirectory = process.env.TASK_DIRECTORY || path.join(__dirname, "../tasks"); 10 | 11 | const statusUpdater = (octokit, owner, repo, sha) => (context, defaultDescription) => (state, description) => { 12 | return octokit.repos.createStatus({ 13 | owner, 14 | repo, 15 | sha, 16 | state, 17 | context, 18 | description: description || defaultDescription 19 | }); 20 | }; 21 | 22 | const main = async (options = {}) => { 23 | const { octokit, payload, taskDir = tasksDirectory } = options; 24 | const { pull_request = {} } = payload; 25 | const { head: { sha, repo: { name, owner: { login } = {}, clone_url } = {} } = {} } = pull_request; 26 | const statusAPI = statusUpdater(octokit, login, name, sha); 27 | const updateProjectStatus = statusAPI("status-check-app", "status-check-app"); 28 | 29 | updateProjectStatus("pending", "Checking for tasks..."); 30 | 31 | const isDirectory = source => fs.lstatSync(source).isDirectory(); 32 | const getDirectories = source => 33 | fs 34 | .readdirSync(source) 35 | .map(name => path.join(source, name)) 36 | .filter(isDirectory); 37 | 38 | const functionDirs = getDirectories(taskDir); 39 | 40 | if (functionDirs.length === 0) { 41 | log(chalk.red("No tasks found in the tasks directory. Please add some tasks to run. More can be found in the documentation website")); 42 | updateProjectStatus("failure", "No tasks found"); 43 | return new Error("Please provide tasks"); 44 | } 45 | 46 | const promises = functionDirs.map(dir => { 47 | const hasMetadata = fs.existsSync(path.join(dir, "metadata.json")); 48 | console.log("here", path.join(dir, "/metadata.json"), hasMetadata); 49 | return { 50 | promise: require(dir), 51 | metadata: hasMetadata ? require(path.join(dir, "/metadata.json")) : {}, 52 | dir 53 | }; 54 | }); 55 | 56 | const rootDir = path.join(__dirname, "../../"); 57 | const projectDir = path.join(rootDir, sha); 58 | 59 | updateProjectStatus("pending", "Cleanup and Cloning project..."); 60 | log(chalk.green(`Cleaning up, before cloning: ${projectDir}`)); 61 | await runCommand(`rm -rf ${sha}`, { cwd: rootDir }); 62 | 63 | log(chalk.green(`Cloning the repo: ${projectDir}`)); 64 | // pass token in just in case its private? 65 | const cloneUrl = clone_url.replace("github.com", `${GITHUB_TOKEN}@github.com`); 66 | await runCommand(`git clone ${cloneUrl} ${sha}`, { cwd: rootDir }); 67 | 68 | const hasPackageJSON = fs.existsSync(path.join(projectDir, `package.json`)); 69 | 70 | // What if not node stuff? 71 | if (hasPackageJSON) { 72 | updateProjectStatus("pending", "Installing dependencies before running tasks..."); 73 | log(chalk.green(`Installing dependencies from package.json`)); 74 | await runCommand(`npm install`, { cwd: projectDir }); 75 | } 76 | 77 | log(chalk.green(`Running tasks...`)); 78 | updateProjectStatus("pending", "Running tasks..."); 79 | 80 | const allTasks = promises.map(({ promise, dir, metadata }) => { 81 | return new Promise(async (resolve, reject) => { 82 | const parts = dir.split("/"); 83 | const func = parts[parts.length - 1]; 84 | const { title = func, description = "Custom task" } = metadata; 85 | const setStatusCheck = statusAPI(title, description); 86 | try { 87 | await setStatusCheck("pending"); 88 | } catch (error) { 89 | console.log("error"); 90 | } 91 | 92 | try { 93 | log(`${chalk.blue("Running function:")} ${chalk.green(func)}`); 94 | const { taskDir, ...promiseArgs } = options; 95 | await promise({ 96 | ...promiseArgs, 97 | setDescription: async description => { 98 | console.log('setStatusCheck("pending", description)', description); 99 | await setStatusCheck("pending", description); 100 | }, 101 | // Api for custom tasks, run anything you want inside that folder... 102 | runCommand: command => { 103 | return runCommand(command, { cwd: path.join(rootDir, sha) }); 104 | } 105 | }); 106 | await setStatusCheck("success", "Passed"); 107 | log(chalk.green(`Function was successful: ${func}`)); 108 | resolve(); 109 | } catch (error) { 110 | await setStatusCheck("failure"); 111 | log(chalk.red(`Function failed: ${func}`)); 112 | reject(); 113 | } 114 | }); 115 | }); 116 | 117 | return Promise.all(allTasks.map(p => p.catch(e => e))).then(async results => { 118 | log(chalk.green("Finished running all tasks")); 119 | await runCommand(`rm -rf ${sha}`, { cwd: rootDir }); 120 | await updateProjectStatus("success", "Finished running all tasks"); 121 | }); 122 | }; 123 | 124 | module.exports = main; 125 | -------------------------------------------------------------------------------- /src/task-runner/spec.js: -------------------------------------------------------------------------------- 1 | const runner = require("./"); 2 | const task1 = require("./mocks/example-tasks/task-1"); 3 | const task2 = require("./mocks/example-tasks/task-2"); 4 | const runCommand = require("../scripts/run-command"); 5 | const path = require("path"); 6 | 7 | const taskDir = path.join(__dirname, "./mocks/example-tasks"); 8 | const containTask = task => expect.arrayContaining([[task]]); 9 | 10 | jest.mock("./mocks/example-tasks/task-1", () => { 11 | return jest.fn(); 12 | }); 13 | 14 | jest.mock("./mocks/example-tasks/task-2", () => { 15 | return jest.fn(); 16 | }); 17 | 18 | jest.mock("../scripts/run-command", () => { 19 | return jest.fn(); 20 | }); 21 | 22 | const buildRunner = async overrides => { 23 | const octokit = { 24 | repos: { 25 | createStatus: jest.fn() 26 | } 27 | }; 28 | const payload = { 29 | pull_request: { 30 | head: { 31 | sha: "1234", 32 | repo: { 33 | name: "test-repo", 34 | clone_url: "https://github.com/boyney123/repo.git", 35 | owner: { 36 | login: "boyney123" 37 | } 38 | } 39 | } 40 | } 41 | }; 42 | const options = { octokit, payload, taskDir, ...overrides }; 43 | return { octokit, options, runner: await runner(options) }; 44 | }; 45 | 46 | describe("task-runner", () => { 47 | beforeEach(() => { 48 | task1.mockReset(); 49 | task2.mockReset(); 50 | runCommand.mockReset(); 51 | }); 52 | 53 | describe("setup", () => { 54 | it("cleans and checks-out the code of the pull request in the root directory of the project", async () => { 55 | await buildRunner(); 56 | const runCommandCalls = runCommand.mock.calls; 57 | expect(runCommandCalls[0]).toEqual(["rm -rf 1234", { cwd: path.join(__dirname, "../../") }]); 58 | expect(runCommandCalls[1]).toEqual(["git clone https://token@github.com/boyney123/repo.git 1234", { cwd: path.join(__dirname, "../../") }]); 59 | }); 60 | 61 | it("once all tasks have finished the clone folder is removed", async done => { 62 | await buildRunner(); 63 | const runCommandCalls = runCommand.mock.calls; 64 | 65 | // hack, to wait for the async tasks to finish. await not working? 66 | setTimeout(() => { 67 | expect(runCommandCalls[2]).toEqual(["rm -rf 1234", { cwd: path.join(__dirname, "../../") }]); 68 | done(); 69 | }, 100); 70 | }); 71 | 72 | it("installs dependencies from the package.json file if it finds one after the clone process", async () => { 73 | const fs = require("fs"); 74 | const old = fs.existsSync; 75 | 76 | fs.existsSync = jest.fn(file => { 77 | return file.indexOf("package.json") > -1; 78 | }); 79 | 80 | await buildRunner(); 81 | const runCommandCalls = runCommand.mock.calls; 82 | 83 | expect(runCommandCalls[2]).toEqual(["npm install", { cwd: path.join(__dirname, "../../1234") }]); 84 | 85 | fs.existsSync = old; 86 | }); 87 | it("if no tasks can be found to run then it stops running and logs out an error", async () => { 88 | const { runner } = await buildRunner({ taskDir: path.join(__dirname, "./mocks/folder-with-no-tasks") }); 89 | expect(runner.message).toBe("Please provide tasks"); 90 | }); 91 | }); 92 | 93 | describe("status-check-app status check", () => { 94 | it("when the runner starts it sends a status check to GitHub to represent the checks have started", async () => { 95 | const { octokit } = await buildRunner(); 96 | 97 | const octokitCalls = octokit.repos.createStatus.mock.calls; 98 | 99 | expect(octokitCalls).toEqual( 100 | containTask({ 101 | context: "status-check-app", 102 | description: "Checking for tasks...", 103 | owner: "boyney123", 104 | repo: "test-repo", 105 | sha: "1234", 106 | state: "pending" 107 | }) 108 | ); 109 | }); 110 | it("when the runner starts to checkout and clone the project the status is sent to GitHub", async () => { 111 | const { octokit } = await buildRunner(); 112 | 113 | const octokitCalls = octokit.repos.createStatus.mock.calls; 114 | 115 | expect(octokitCalls).toEqual( 116 | containTask({ 117 | context: "status-check-app", 118 | description: "Cleanup and Cloning project...", 119 | owner: "boyney123", 120 | repo: "test-repo", 121 | sha: "1234", 122 | state: "pending" 123 | }) 124 | ); 125 | }); 126 | it("when the project has checked out and project has a package.json file a status is sent to GitHub", async () => { 127 | const fs = require("fs"); 128 | const old = fs.existsSync; 129 | 130 | fs.existsSync = jest.fn(file => { 131 | return file.indexOf("package.json") > -1; 132 | }); 133 | 134 | const { octokit } = await buildRunner(); 135 | 136 | const octokitCalls = octokit.repos.createStatus.mock.calls; 137 | 138 | expect(octokitCalls).toEqual( 139 | containTask({ 140 | context: "status-check-app", 141 | description: "Installing dependencies before running tasks...", 142 | owner: "boyney123", 143 | repo: "test-repo", 144 | sha: "1234", 145 | state: "pending" 146 | }) 147 | ); 148 | fs.existsSync = old; 149 | }); 150 | 151 | it("before the tasks are run the status is sent to GitHub", async () => { 152 | const { octokit } = await buildRunner(); 153 | 154 | const octokitCalls = octokit.repos.createStatus.mock.calls; 155 | 156 | expect(octokitCalls).toEqual( 157 | containTask({ 158 | context: "status-check-app", 159 | description: "Running tasks...", 160 | owner: "boyney123", 161 | repo: "test-repo", 162 | sha: "1234", 163 | state: "pending" 164 | }) 165 | ); 166 | }); 167 | it("Once all tasks have finished the status is marked as successful", async () => { 168 | const { octokit } = await buildRunner(); 169 | 170 | const octokitCalls = octokit.repos.createStatus.mock.calls; 171 | 172 | expect(octokitCalls).toEqual( 173 | containTask({ 174 | context: "status-check-app", 175 | description: "Finished running all tasks", 176 | owner: "boyney123", 177 | repo: "test-repo", 178 | sha: "1234", 179 | state: "success" 180 | }) 181 | ); 182 | }); 183 | it("when no tasks have been found the runner sends a status check to GitHub with a failure and message", async () => { 184 | const { octokit } = await buildRunner({ taskDir: path.join(__dirname, "./mocks/folder-with-no-tasks") }); 185 | 186 | const octokitCalls = octokit.repos.createStatus.mock.calls; 187 | 188 | expect(octokitCalls).toEqual( 189 | containTask({ 190 | context: "status-check-app", 191 | description: "No tasks found", 192 | owner: "boyney123", 193 | repo: "test-repo", 194 | sha: "1234", 195 | state: "failure" 196 | }) 197 | ); 198 | }); 199 | }); 200 | 201 | describe("tasks", () => { 202 | it("loops through all tasks and calls them", async () => { 203 | await buildRunner(); 204 | expect(task1).toHaveBeenCalled(); 205 | expect(task2).toHaveBeenCalled(); 206 | }); 207 | 208 | it("sets the title and description to the default values if no `metadata` file is set", async () => { 209 | const { octokit } = await buildRunner(); 210 | 211 | const octokitCalls = octokit.repos.createStatus.mock.calls; 212 | 213 | expect(octokitCalls).toEqual( 214 | containTask({ 215 | context: "task-without-metadata", 216 | description: "Custom task", 217 | owner: "boyney123", 218 | repo: "test-repo", 219 | sha: "1234", 220 | state: "pending" 221 | }) 222 | ); 223 | }); 224 | it("before the task is called the status is set to pending", async () => { 225 | const { octokit } = await buildRunner(); 226 | 227 | const octokitCalls = octokit.repos.createStatus.mock.calls; 228 | 229 | expect(octokitCalls).toEqual( 230 | containTask({ 231 | context: "task-1", 232 | description: "My custom task", 233 | owner: "boyney123", 234 | repo: "test-repo", 235 | sha: "1234", 236 | state: "pending" 237 | }) 238 | ); 239 | }); 240 | it("sets the status to success if the task resolves", async () => { 241 | const { octokit } = await buildRunner(); 242 | 243 | const octokitCalls = octokit.repos.createStatus.mock.calls; 244 | 245 | expect(octokitCalls).toEqual( 246 | containTask({ 247 | context: "task-1", 248 | description: "Passed", 249 | owner: "boyney123", 250 | repo: "test-repo", 251 | sha: "1234", 252 | state: "success" 253 | }) 254 | ); 255 | }); 256 | it("sets the status to failure if the task rejects", async done => { 257 | const { octokit } = await buildRunner(); 258 | 259 | setTimeout(() => { 260 | const octokitCalls = octokit.repos.createStatus.mock.calls; 261 | 262 | expect(octokitCalls).toEqual( 263 | containTask({ 264 | context: "failed-task", 265 | description: "Failed Task", 266 | owner: "boyney123", 267 | repo: "test-repo", 268 | sha: "1234", 269 | state: "failure" 270 | }) 271 | ); 272 | 273 | done(); 274 | }, 1); 275 | }); 276 | }); 277 | 278 | describe("task API", () => { 279 | describe("runCommand", () => { 280 | it("when called it runs the given command in the current working directory of the cloned project", async () => { 281 | const { options, octokit } = await buildRunner(); 282 | 283 | const runCommandCalls = runCommand.mock.calls; 284 | 285 | const task1Args = task1.mock.calls[0][0]; 286 | 287 | await task1Args.runCommand("echo Hello"); 288 | 289 | expect(runCommandCalls[3][0]).toEqual("echo Hello"); 290 | }); 291 | }); 292 | describe("setDescription", () => { 293 | it("when called it sets setDescription of the status check", async () => { 294 | const { options, octokit } = await buildRunner(); 295 | 296 | const octokitCalls = octokit.repos.createStatus.mock.calls; 297 | 298 | const task1Args = task1.mock.calls[0][0]; 299 | 300 | await task1Args.setDescription("Here is an update on my description"); 301 | 302 | expect(octokitCalls).toEqual( 303 | containTask({ 304 | context: "task-1", 305 | description: "Here is an update on my description", 306 | owner: "boyney123", 307 | repo: "test-repo", 308 | sha: "1234", 309 | state: "pending" 310 | }) 311 | ); 312 | }); 313 | }); 314 | 315 | it("each task that gets called gets called with an API", async () => { 316 | const { options } = await buildRunner(); 317 | 318 | const task1Args = task1.mock.calls[0][0]; 319 | 320 | const { taskDir: _taskDir, ...expectedArgsForPromise } = options; 321 | 322 | expect(task1Args).toEqual(expect.objectContaining({ ...expectedArgsForPromise })); 323 | expect(task1Args.setDescription).toBeDefined(); 324 | expect(task1Args.runCommand).toBeDefined(); 325 | expect(Object.keys(task1Args)).toHaveLength(4); 326 | }); 327 | }); 328 | }); 329 | --------------------------------------------------------------------------------