├── .gitignore ├── LICENSE.md ├── README.md ├── index.js ├── package-lock.json ├── package.json ├── tests ├── __snapshots__ │ └── index.test.js.snap ├── index.test.js └── setup.js └── views └── probot.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | functions 3 | .vscode -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2019 Edward Thomson 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## Azure Functions Extension for Probot 2 | 3 | This tool is deprecated. Please use Probot's [adapter-azure-functions](https://github.com/probot/adapter-azure-functions). 4 | 5 | ## Usage 6 | 7 | A [Probot](https://github.com/probot/probot) extension to make it 8 | easier to run your Probot Apps in Azure Functions. 9 | 10 | ```shell 11 | $ npm install probot-serverless-azurefunctions 12 | ``` 13 | 14 | ```javascript 15 | // index.js 16 | const { serverless } = require('probot-serverless-azurefunctions') 17 | 18 | const appFn = (app) => { 19 | app.on(['*'], async (context) => { 20 | app.log(`Received event: ${context.event}`) 21 | }) 22 | } 23 | 24 | module.exports.probot = serverless(appFn) 25 | ``` 26 | 27 | ## Configuration 28 | This package moves the functionality of `probot run` into a handler suitable for usage in Azure Functions. Follow the documentation on [Environment Configuration](https://probot.github.io/docs/configuration/) to setup your app's environment variables. You can add these to `.env`, but for security reasons you may want to use the [Azure CLI](https://docs.microsoft.com/en-us/cli/azure/?view=azure-cli-latest) or the user-interface to set Environment Variables for the function so you don't have to include any secrets in the deployed package. 29 | 30 | ## Differences from `probot run` 31 | 32 | #### Local Development 33 | Since Azure Functions do not start a normal node process, the best way we've found to test this out locally is to use the [Azure Functions Core Tools](https://docs.microsoft.com/en-us/azure/azure-functions/functions-run-local) or [another local emulator](https://docs.microsoft.com/en-us/azure/azure-functions/functions-develop-local) on your local machine before deploying your function to Azure. 34 | 35 | #### Long running tasks 36 | Some Probot Apps that depend on long running processes or intervals will not work with this extension. This is due to the inherent architecture of serverless functions, which are designed to respond to events and stop running as quickly as possible. For longer running apps we recommend using [other deployment options](https://probot.github.io/docs/deployment). 37 | 38 | #### Only responds to Webhooks from GitHub 39 | This extension is designed primarily for receiving webhooks from GitHub and responding back as a GitHub App. 40 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | const { Probot } = require('probot') 2 | const { resolve } = require('probot/lib/helpers/resolve-app-function') 3 | const { findPrivateKey } = require('probot/lib/helpers/get-private-key') 4 | const { template } = require('./views/probot') 5 | 6 | let probot 7 | 8 | const loadProbot = appFn => { 9 | probot = probot || new Probot({ 10 | id: process.env.APP_ID, 11 | secret: process.env.WEBHOOK_SECRET, 12 | privateKey: findPrivateKey() 13 | }) 14 | 15 | if (typeof appFn === 'string') { 16 | appFn = resolve(appFn) 17 | } 18 | 19 | probot.load(appFn) 20 | 21 | return probot 22 | } 23 | 24 | const lowerCaseKeys = (obj = {}) => 25 | Object.keys(obj).reduce((accumulator, key) => 26 | Object.assign(accumulator, {[key.toLocaleLowerCase()]: obj[key]}), {}) 27 | 28 | 29 | module.exports.serverless = appFn => { 30 | return async (context, req) => { 31 | // 🤖 A friendly homepage if there isn't a payload 32 | if (req.method === 'GET') { 33 | context.res = { 34 | status: 200, 35 | headers: { 36 | 'Content-Type': 'text/html' 37 | }, 38 | body: template 39 | } 40 | 41 | context.done() 42 | return 43 | } 44 | 45 | // Bail for null body 46 | if (!req.body) { 47 | context.res = { 48 | status: 400, 49 | headers: { 50 | 'Content-Type': 'application/json' 51 | }, 52 | body: JSON.stringify({ message: 'Event body is null' }) 53 | } 54 | context.done(); 55 | return 56 | } 57 | 58 | probot = loadProbot(appFn) 59 | 60 | const headers = lowerCaseKeys(req.headers) 61 | 62 | // Determine incoming webhook event type and event ID 63 | const name = headers['x-github-event'] 64 | const id = headers['x-github-delivery'] 65 | 66 | // Do the thing 67 | context.log(`Received event: ${name}${req.body.action ? ('.' + req.body.action) : ''}`) 68 | 69 | // Verify the signature and then execute 70 | const response = await probot.webhooks.verifyAndReceive({ id, name, payload: req.body, signature: headers['x-hub-signature'] }) 71 | .then(res => { 72 | return { 73 | status: 200, 74 | headers: { 75 | 'Content-Type': 'application/json' 76 | }, 77 | body: JSON.stringify({ message: 'Executed' }) 78 | } 79 | }) 80 | .catch(error => { 81 | return { 82 | status: error.event.status, 83 | headers: { 84 | 'Content-Type': 'application/json' 85 | }, 86 | body: JSON.stringify(error.errors) 87 | } 88 | }) 89 | 90 | context.log(`Event executed with status ${response.status} and output of: `, JSON.parse(response.body)) 91 | context.res = response 92 | context.done(); 93 | return 94 | } 95 | } 96 | 97 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "probot-serverless-azurefunctions", 3 | "version": "0.0.2", 4 | "description": "An extension for running Probot in Azure Functions", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "jest" 8 | }, 9 | "repository": { 10 | "type": "git", 11 | "url": "git+https://github.com/ethomson/probot-serverless-azurefunctions.git" 12 | }, 13 | "keywords": [ 14 | "azure", 15 | "azurefunctions", 16 | "probot", 17 | "github" 18 | ], 19 | "author": "Edward Thomson ", 20 | "license": "MIT", 21 | "bugs": { 22 | "url": "https://github.com/ethomson/probot-serverless-azurefunctions/issues" 23 | }, 24 | "homepage": "https://github.com/ethomson/probot-serverless-azurefunctions#readme", 25 | "devDependencies": { 26 | "jest": "^26.6.1" 27 | }, 28 | "dependencies": { 29 | "probot": "10.10.0" 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /tests/__snapshots__/index.test.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`probot-serverless-azurefunctions responds with 400 when payload has been changed 1`] = ` 4 | [MockFunction] { 5 | "calls": Array [ 6 | Array [ 7 | "Received event: issues", 8 | ], 9 | Array [ 10 | "Event executed with status 400 and output of: ", 11 | Array [ 12 | Object { 13 | "event": Object { 14 | "id": 123, 15 | "name": "issues", 16 | "payload": Object { 17 | "installation": Object { 18 | "id": 123123, 19 | }, 20 | }, 21 | "signature": "sha1=11daab007b2f76317fa026fdb6f3d66337a9d701", 22 | }, 23 | "status": 400, 24 | }, 25 | ], 26 | ], 27 | ], 28 | "results": Array [ 29 | Object { 30 | "type": "return", 31 | "value": undefined, 32 | }, 33 | Object { 34 | "type": "return", 35 | "value": undefined, 36 | }, 37 | ], 38 | } 39 | `; 40 | 41 | exports[`probot-serverless-azurefunctions responds with 400 when payload has been changed 2`] = `[MockFunction]`; 42 | 43 | exports[`probot-serverless-azurefunctions responds with the homepage 1`] = ` 44 | " 45 | 46 | 47 | 48 | 49 | 50 | 51 | Your App | built with Probot 52 | 53 | 54 | 55 |
56 | \\"Probot 57 |
58 |

59 | Welcome to your Probot App! 60 |

61 |

This bot was built using Probot, a framework for building GitHub Apps.

62 |
63 |
64 |

Need help?

65 |
66 | Documentation 67 | Chat on Slack 68 |
69 |
70 |
71 | 72 | 73 | " 74 | `; 75 | 76 | exports[`probot-serverless-azurefunctions responds with the homepage 2`] = `[MockFunction]`; 77 | 78 | exports[`probot-serverless-azurefunctions responds with the homepage 3`] = `[MockFunction]`; 79 | -------------------------------------------------------------------------------- /tests/index.test.js: -------------------------------------------------------------------------------- 1 | const { serverless } = require('../') 2 | const crypto = require('crypto') 3 | 4 | const getEvent = (body = {installation: { id: 1 }}) => { 5 | 6 | const secret = 'iamasecret' 7 | 8 | process.env.WEBHOOK_SECRET = secret 9 | 10 | // Create Signature 11 | let hmac = crypto.createHmac('sha1', secret); 12 | hmac.update(JSON.stringify(body)); 13 | const hash = hmac.digest('hex'); 14 | 15 | return { 16 | body, 17 | headers: { 18 | 'X-Github-Event': 'issues', 19 | 'x-Github-Delivery': 123, 20 | 'X-Hub-Signature': `sha1=${hash}` 21 | } 22 | } 23 | } 24 | 25 | describe('probot-serverless-azurefunctions', () => { 26 | let spy, handler, context 27 | 28 | beforeEach(() => { 29 | context = { done: jest.fn(), log: console.log } 30 | context.log = jest.fn() 31 | process.env = { } 32 | spy = jest.fn() 33 | handler = serverless(async app => { 34 | app.auth = () => Promise.resolve({}) 35 | app.on('issues', spy) 36 | }) 37 | }) 38 | 39 | it('responds with the homepage', async () => { 40 | const event = { method: 'GET', path: '/probot' } 41 | await handler(context, event) 42 | expect(context.done).toHaveBeenCalled() 43 | expect(context.res.body).toMatchSnapshot() 44 | expect(context.log).toMatchSnapshot() 45 | }) 46 | 47 | it('calls the event handler', async () => { 48 | const event = getEvent() 49 | 50 | await handler(context, event) 51 | expect(context.done).toHaveBeenCalled() 52 | expect(spy).toHaveBeenCalled() 53 | }) 54 | 55 | it('responds with a 400 error when body is null', async () => { 56 | const event = getEvent(null) 57 | 58 | await handler(context, event) 59 | expect(context.res).toEqual(expect.objectContaining({status: 400})) 60 | expect(context.done).toHaveBeenCalled() 61 | expect(spy).not.toHaveBeenCalled() 62 | }) 63 | 64 | it('responds with 400 when payload has been changed', async () => { 65 | const secret = "iamasecret" 66 | 67 | const body = { 68 | installation: { id: 1 } 69 | } 70 | 71 | // Create Signature 72 | let hmac = crypto.createHmac('sha1', secret); 73 | hmac.update(JSON.stringify(body)); 74 | const hash = hmac.digest('hex'); 75 | 76 | const event = { 77 | body: { 78 | installation: { id: 123123 } // different to what 'github' would have sent 79 | }, 80 | headers: { 81 | 'X-Github-Event': 'issues', 82 | 'x-Github-Delivery': 123, 83 | 'X-Hub-Signature': `sha1=${hash}` 84 | } 85 | } 86 | 87 | process.env.WEBHOOK_SECRET = secret 88 | 89 | await handler(context, event) 90 | expect(context.res).toEqual(expect.objectContaining({status: 400})) 91 | expect(context.done).toHaveBeenCalled() 92 | expect(spy).not.toHaveBeenCalled() 93 | expect(context.log).toMatchSnapshot() 94 | }) 95 | }) -------------------------------------------------------------------------------- /tests/setup.js: -------------------------------------------------------------------------------- 1 | // This is a fake key generated for www.example.com 2 | process.env.PRIVATE_KEY = `-----BEGIN RSA PRIVATE KEY----- 3 | MIIEpAIBAAKCAQEAuKKQtSoihtvoN2ewvJh2sBriQ6aMqS59yrOdwXhNIp1PfX0U 4 | SgQbbhcyg9TjI91YMTAlIBazLtDfg1PXmnNDqXkWHrcwosvVi6IzopouwCXmeLPL 5 | 1nUrkymqU4s+J2eJ8otrRRB0c7l+3/hbdHELvn6LSXIU4hAzMwWnFZ7N18tHErco 6 | 1i1yVmMxYb9o/DkR6lhhSapxd7VnEaoNHb2NuoHSFZ9MAMpkaU6y9Z9Y+yTSGt8I 7 | I9MxKUyQVy9X0+qMeQZSqA+NXwgQ/pCZybX5KP7/ETAts5LyXkm6jXJH7N6Grmsx 8 | ZbxramCQSnQzMiiZ/f0nchGWIamP3/WqgATBdQIDAQABAoIBAQCExDE2dKP7QaqM 9 | HL3T//Zo0AwgBWVkSpAd8GbiNjWRTHlajVTHnIh0861Zav4iTgoa5LnthyU15vCy 10 | qNmCsJvZA79KOwR4LAbUr8Bdjm1LsnU2GmPbRMoeunlGNfxtrWBezq5GLXzvslVv 11 | jFGHO7rsMmbKW4R0wp1udQQe4eC26LUVb58wibzb8yiP2quc00qszVM784X3UzzF 12 | fYRYoD6H8Qgsd99jpfUB6WEPaYRvVL+8K4XUPdUPBKoOHg7k5mkDakomN2JdFWNy 13 | NlT4VxuxrTZJssqTv/pefWGvp39uYCZ6bmtqPufTrl/zJU6fSf5smlJXJWWdubEP 14 | oczuaX2BAoGBAPKoXIXlVo0wtFwatPbLkUYNPg1J2lorwGwbzqEL878EpwMMmKAW 15 | QOQByxgbUJM61Bk1Zif9TDB4RSGeFW0+/2viJbcBYPBYPbUPKwuIPdq7Ll5PBl1X 16 | S2W/WS8uYp64COGoVIPM7YTXRNScNL/VZzKha+Qr8bbu1pOSTFjqHH6lAoGBAMLJ 17 | es2HWd6sdPSHdeYVlkveXPW4nR2Br6oR/pz1e6y0GNgnPW3qROC9kdI8UvH8oOhC 18 | tFN6dRdI+G8F5A45EB8RvKCPIWH5uMnSHjwYRUiiHJe5oPwpxHf0PPQT4iICZJsX 19 | KaX6L1Vg3q07xdLsU43wKxlOqli2NlOBZLec5A6RAoGAPPxRXJl5+jwuaCOSLaCV 20 | 30w+typDhXwPfVwzv0f4t55ctyh4R2uwXV2SBHoA8y/K1JcWGKDRgDEJ9tv7OJyn 21 | px6MKgVfrqgOwi2QvPI90XZPvgYQbG8fFPBVYsU+pfNM0CH1M7bSTxunQeQMYdYp 22 | fJETQ6JDup0mMqqHI6WbCb0CgYAbA5hNYsUa9a3ur86xDzNd6EPaLDVV/0Negcpe 23 | +EijpgKAD8kcMk5FIOVVU9ppBxFFxOJ/ZU9R4GPb+eQr+Mv8kxgm6FLH5Ls0+jgJ 24 | O5B4R0tR24OxFRXTUQMXEp7c+pn7TFYRV8YywBGB0vVXkEDyQWmow9kqHnMgV6Sh 25 | NlgGkQKBgQC7J8JbDbME42WxJ3ips6bLkawO5OrwATVuQJ8noJ3wnmuLLWcmzAdp 26 | xIORyezH7FvEutfBZQVwWLvYpCLwr9KOUiFeXcflCsEXPgLdm4VbZLUPbIilqGtL 27 | W/e5K17T/px3HVyJZCQKvBG3jf/hHPMnjNzC4uEKohHA9uO5npg/MA== 28 | -----END RSA PRIVATE KEY-----` -------------------------------------------------------------------------------- /views/probot.js: -------------------------------------------------------------------------------- 1 | module.exports.template = ` 2 | 3 | 4 | 5 | 6 | 7 | 8 | Your App | built with Probot 9 | 10 | 11 | 12 |
13 | Probot Logo 14 |
15 |

16 | Welcome to your Probot App! 17 |

18 |

This bot was built using Probot, a framework for building GitHub Apps.

19 |
20 |
21 |

Need help?

22 |
23 | Documentation 24 | Chat on Slack 25 |
26 |
27 |
28 | 29 | 30 | ` 31 | --------------------------------------------------------------------------------