├── .gitignore ├── .github ├── renovate.json └── workflows │ ├── release.yml │ └── test.yml ├── package.json ├── tests ├── index.test.js ├── __snapshots__ │ └── index.test.js.snap └── setup.js ├── views └── probot.js ├── index.js └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | coverage 3 | -------------------------------------------------------------------------------- /.github/renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "github>probot/.github" 4 | ] 5 | } 6 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | "on": 3 | push: 4 | branches: 5 | - master 6 | - next 7 | - beta 8 | - "*.x" 9 | # These are recommended by the semantic-release docs: https://github.com/semantic-release/npm#npm-provenance 10 | permissions: 11 | contents: write # to be able to publish a GitHub release 12 | issues: write # to be able to comment on released issues 13 | pull-requests: write # to be able to comment on released pull requests 14 | id-token: write # to enable use of OIDC for npm provenance 15 | 16 | jobs: 17 | release: 18 | name: release 19 | runs-on: ubuntu-latest 20 | steps: 21 | - uses: actions/checkout@v4 22 | - uses: actions/setup-node@v4 23 | with: 24 | node-version: lts/* 25 | cache: npm 26 | - run: npm ci 27 | - run: npx semantic-release 28 | env: 29 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 30 | NPM_TOKEN: ${{ secrets.PROBOTBOT_NPM_TOKEN }} 31 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Test 2 | on: 3 | push: 4 | branches: 5 | - dependabot/npm_and_yarn/** 6 | pull_request: 7 | types: 8 | - opened 9 | - synchronize 10 | - reopened 11 | 12 | jobs: 13 | test-building: 14 | runs-on: ubuntu-latest 15 | steps: 16 | - uses: actions/checkout@v4 17 | - uses: actions/setup-node@v4 18 | with: 19 | cache: npm 20 | node-version: lts/* 21 | - run: npm ci 22 | - run: npm run build 23 | 24 | test_matrix: 25 | needs: test-building 26 | runs-on: ubuntu-latest 27 | strategy: 28 | matrix: 29 | node_version: 30 | - 20 31 | - 22 32 | - 24 33 | name: Node ${{ matrix.node_version }} 34 | steps: 35 | - uses: actions/checkout@v4 36 | - uses: actions/setup-node@v4 37 | with: 38 | node-version: ${{ matrix.node_version }} 39 | cache: npm 40 | - run: npm ci 41 | - run: npm test 42 | 43 | test: 44 | runs-on: ubuntu-latest 45 | needs: 46 | - test_matrix 47 | steps: 48 | - run: exit 1 49 | if: ${{ needs.test_matrix.result != 'success' }} 50 | if: ${{ always() }} 51 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@probot/adapter-google-cloud-functions", 3 | "version": "0.0.0-development", 4 | "description": "An extension for running Probot in Google Cloud Functions", 5 | "main": "index.js", 6 | "type": "commonjs", 7 | "scripts": { 8 | "test": "jest --coverage && standard" 9 | }, 10 | "files": [ 11 | "index.js", 12 | "README.md", 13 | "views/*" 14 | ], 15 | "repository": { 16 | "type": "git", 17 | "url": "git+https://github.com/probot/adapter-google-cloud-functions.git" 18 | }, 19 | "devDependencies": { 20 | "jest": "^29.2.2", 21 | "probot": "^13.0.0", 22 | "standard": "^17.0.0" 23 | }, 24 | "peerDependencies": { 25 | "probot": "^13.0.0" 26 | }, 27 | "engines": { 28 | "node": ">= 18.0.0" 29 | }, 30 | "keywords": [ 31 | "google cloud functions", 32 | "probot", 33 | "github" 34 | ], 35 | "standard": { 36 | "env": [ 37 | "jest" 38 | ] 39 | }, 40 | "jest": { 41 | "testEnvironment": "node", 42 | "setupFiles": [ 43 | "./tests/setup.js" 44 | ] 45 | }, 46 | "author": "Jason Etcovitch ", 47 | "license": "ISC", 48 | "publishConfig": { 49 | "access": "public", 50 | "provenance": true 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /tests/index.test.js: -------------------------------------------------------------------------------- 1 | const { serverless } = require('../index.js') 2 | 3 | describe('serverless-gcf', () => { 4 | let spy, handler, response 5 | 6 | beforeEach(() => { 7 | response = { send: jest.fn(), sendStatus: jest.fn() } 8 | spy = jest.fn() 9 | handler = serverless(async app => { 10 | app.auth = () => Promise.resolve({}) 11 | app.on('issues', spy) 12 | }) 13 | }) 14 | 15 | it('responds with the homepage', async () => { 16 | const request = { method: 'GET', path: '/probot' } 17 | await handler(request, response) 18 | expect(response.send).toHaveBeenCalled() 19 | expect(response.send.mock.calls[0][0]).toMatchSnapshot() 20 | }) 21 | 22 | it('calls the event handler', async () => { 23 | const request = { 24 | body: { 25 | installation: { id: 1 } 26 | }, 27 | get (string) { 28 | return this[string] 29 | }, 30 | 'x-github-event': 'issues', 31 | 'x-github-delivery': 123 32 | } 33 | 34 | await handler(request, response) 35 | expect(response.send).toHaveBeenCalled() 36 | expect(spy).toHaveBeenCalled() 37 | }) 38 | 39 | it('does nothing if there are missing headers', async () => { 40 | const request = { 41 | body: { 42 | installation: { id: 1 } 43 | }, 44 | get (string) { 45 | return this[string] 46 | } 47 | } 48 | 49 | await handler(request, response) 50 | expect(response.send).not.toHaveBeenCalled() 51 | expect(spy).not.toHaveBeenCalled() 52 | expect(response.sendStatus).toHaveBeenCalledWith(400) 53 | }) 54 | }) 55 | -------------------------------------------------------------------------------- /views/probot.js: -------------------------------------------------------------------------------- 1 | const { name, version } = require(`${process.cwd()}/package`) 2 | 3 | module.exports.template = ` 4 | 5 | 6 | 7 | 8 | 9 | 10 | ${name} | built with Probot 11 | 12 | 13 | 14 |
15 | Probot Logo 16 |
17 |

18 | Welcome to ${name}! 19 | v${version} 20 |

21 |

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

22 |
23 | 24 |
25 |

Need help?

26 |
27 | Documentation 28 | Chat on Slack 29 |
30 |
31 |
32 | 33 | 34 | ` 35 | -------------------------------------------------------------------------------- /tests/__snapshots__/index.test.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`serverless-gcf responds with the homepage 1`] = ` 4 | { 5 | "body": " 6 | 7 | 8 | 9 | 10 | 11 | 12 | @probot/adapter-google-cloud-functions | built with Probot 13 | 14 | 15 | 16 |
17 | Probot Logo 18 |
19 |

20 | Welcome to @probot/adapter-google-cloud-functions! 21 | v0.2.0 22 |

23 |

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

24 |
25 | 26 |
27 |

Need help?

28 |
29 | Documentation 30 | Chat on Slack 31 |
32 |
33 |
34 | 35 | 36 | ", 37 | "headers": { 38 | "Content-Type": "text/html", 39 | }, 40 | "statusCode": 200, 41 | } 42 | `; 43 | -------------------------------------------------------------------------------- /tests/setup.js: -------------------------------------------------------------------------------- 1 | process.env.PRIVATE_KEY = `-----BEGIN RSA PRIVATE KEY----- 2 | MIIEpAIBAAKCAQEAuKKQtSoihtvoN2ewvJh2sBriQ6aMqS59yrOdwXhNIp1PfX0U 3 | SgQbbhcyg9TjI91YMTAlIBazLtDfg1PXmnNDqXkWHrcwosvVi6IzopouwCXmeLPL 4 | 1nUrkymqU4s+J2eJ8otrRRB0c7l+3/hbdHELvn6LSXIU4hAzMwWnFZ7N18tHErco 5 | 1i1yVmMxYb9o/DkR6lhhSapxd7VnEaoNHb2NuoHSFZ9MAMpkaU6y9Z9Y+yTSGt8I 6 | I9MxKUyQVy9X0+qMeQZSqA+NXwgQ/pCZybX5KP7/ETAts5LyXkm6jXJH7N6Grmsx 7 | ZbxramCQSnQzMiiZ/f0nchGWIamP3/WqgATBdQIDAQABAoIBAQCExDE2dKP7QaqM 8 | HL3T//Zo0AwgBWVkSpAd8GbiNjWRTHlajVTHnIh0861Zav4iTgoa5LnthyU15vCy 9 | qNmCsJvZA79KOwR4LAbUr8Bdjm1LsnU2GmPbRMoeunlGNfxtrWBezq5GLXzvslVv 10 | jFGHO7rsMmbKW4R0wp1udQQe4eC26LUVb58wibzb8yiP2quc00qszVM784X3UzzF 11 | fYRYoD6H8Qgsd99jpfUB6WEPaYRvVL+8K4XUPdUPBKoOHg7k5mkDakomN2JdFWNy 12 | NlT4VxuxrTZJssqTv/pefWGvp39uYCZ6bmtqPufTrl/zJU6fSf5smlJXJWWdubEP 13 | oczuaX2BAoGBAPKoXIXlVo0wtFwatPbLkUYNPg1J2lorwGwbzqEL878EpwMMmKAW 14 | QOQByxgbUJM61Bk1Zif9TDB4RSGeFW0+/2viJbcBYPBYPbUPKwuIPdq7Ll5PBl1X 15 | S2W/WS8uYp64COGoVIPM7YTXRNScNL/VZzKha+Qr8bbu1pOSTFjqHH6lAoGBAMLJ 16 | es2HWd6sdPSHdeYVlkveXPW4nR2Br6oR/pz1e6y0GNgnPW3qROC9kdI8UvH8oOhC 17 | tFN6dRdI+G8F5A45EB8RvKCPIWH5uMnSHjwYRUiiHJe5oPwpxHf0PPQT4iICZJsX 18 | KaX6L1Vg3q07xdLsU43wKxlOqli2NlOBZLec5A6RAoGAPPxRXJl5+jwuaCOSLaCV 19 | 30w+typDhXwPfVwzv0f4t55ctyh4R2uwXV2SBHoA8y/K1JcWGKDRgDEJ9tv7OJyn 20 | px6MKgVfrqgOwi2QvPI90XZPvgYQbG8fFPBVYsU+pfNM0CH1M7bSTxunQeQMYdYp 21 | fJETQ6JDup0mMqqHI6WbCb0CgYAbA5hNYsUa9a3ur86xDzNd6EPaLDVV/0Negcpe 22 | +EijpgKAD8kcMk5FIOVVU9ppBxFFxOJ/ZU9R4GPb+eQr+Mv8kxgm6FLH5Ls0+jgJ 23 | O5B4R0tR24OxFRXTUQMXEp7c+pn7TFYRV8YywBGB0vVXkEDyQWmow9kqHnMgV6Sh 24 | NlgGkQKBgQC7J8JbDbME42WxJ3ips6bLkawO5OrwATVuQJ8noJ3wnmuLLWcmzAdp 25 | xIORyezH7FvEutfBZQVwWLvYpCLwr9KOUiFeXcflCsEXPgLdm4VbZLUPbIilqGtL 26 | W/e5K17T/px3HVyJZCQKvBG3jf/hHPMnjNzC4uEKohHA9uO5npg/MA== 27 | -----END RSA PRIVATE KEY-----` 28 | process.env.APP_ID = '123456' 29 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | const { createProbot } = require('probot') 2 | const { resolveAppFunction } = require('probot/lib/helpers/resolve-app-function.js') 3 | const { template } = require('./views/probot.js') 4 | 5 | /** @type {import("probot").Probot} */ 6 | let probot 7 | 8 | const loadProbot = appFn => { 9 | probot = probot || createProbot({ 10 | id: process.env.APP_ID, 11 | secret: process.env.WEBHOOK_SECRET 12 | }) 13 | 14 | if (typeof appFn === 'string') { 15 | appFn = resolveAppFunction(appFn) 16 | } 17 | 18 | probot.load(appFn) 19 | 20 | return probot 21 | } 22 | 23 | module.exports.serverless = appFn => { 24 | return async (request, response) => { 25 | // 🤖 A friendly homepage if there isn't a payload 26 | if (request.method === 'GET' && request.path === '/probot') { 27 | return response.send({ 28 | statusCode: 200, 29 | headers: { 'Content-Type': 'text/html' }, 30 | body: template 31 | }) 32 | } 33 | 34 | // Otherwise let's listen handle the payload 35 | probot = probot || loadProbot(appFn) 36 | 37 | // Determine incoming webhook event type 38 | const name = request.get('x-github-event') || request.get('X-GitHub-Event') 39 | const id = request.get('x-github-delivery') || request.get('X-GitHub-Delivery') 40 | 41 | // Do the thing 42 | console.log(`Received event ${name}${request.body.action ? ('.' + request.body.action) : ''}`) 43 | if (name) { 44 | try { 45 | await probot.receive({ 46 | name, 47 | id, 48 | payload: request.body 49 | }) 50 | response.send({ 51 | statusCode: 200, 52 | body: JSON.stringify({ message: 'Executed' }) 53 | }) 54 | } catch (err) { 55 | console.error(err) 56 | response.send({ 57 | statusCode: 500, 58 | body: JSON.stringify({ message: err }) 59 | }) 60 | } 61 | } else { 62 | console.error(request) 63 | response.sendStatus(400) 64 | } 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## Google Cloud Function Handler for Probot 2 | 3 | A [Probot](https://github.com/probot/probot) extension to make it easier to run your Probot Apps in Google Cloud Functions. 4 | 5 | ## Usage 6 | 7 | ```shell 8 | $ npm install @probot/serverless-gcf 9 | ``` 10 | 11 | ```javascript 12 | // handler.js 13 | const { serverless } = require('@probot/serverless-gcf'); 14 | const appFn = require('./') 15 | module.exports.probot = serverless(appFn) 16 | ``` 17 | 18 | ## Configuration 19 | This package moves the functionality of `probot run` into a handler suitable for usage on Google Cloud 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 [gcloud cli](https://cloud.google.com/sdk/gcloud/) or [Serverless Framework](https://github.com/serverless/serverless) to set Environment Variables for the function so you don't have to include any secrets in the deployed package. 20 | 21 | ## Differences from `probot run` 22 | 23 | #### Local Development 24 | Since GCF functions do not start a normal node process, the best way we've found to test this out locally is to use [`serverless-offline`](https://github.com/dherault/serverless-offline). This plugin for the serverless framework emulates AWS Lambda and API Gateway on your local machine, allowing you to continue working from `https://localhost:3000/probot` before deploying your function. 25 | 26 | #### Long running tasks 27 | 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). 28 | 29 | #### Only responds to Webhooks from GitHub 30 | This extension is designed primarily for receiving webhooks from GitHub and responding back as a GitHub App. If you are using [HTTP Routes](https://probot.github.io/docs/http/) in your app to serve additional pages, you should take a look at [`serverless-http`](https://github.com/dougmoscrop/serverless-http), which can be used with Probot's [express server](https://github.com/probot/probot/blob/master/src/server.ts) by wrapping `probot.server`. 31 | --------------------------------------------------------------------------------