-
Notifications
You must be signed in to change notification settings - Fork 36
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat: Probot v11 #59
feat: Probot v11 #59
Changes from 2 commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,15 @@ | ||
ISC License | ||
|
||
Copyright (c) 2021 Probot Contributors | ||
|
||
Permission to use, copy, modify, and/or distribute this software for any | ||
purpose with or without fee is hereby granted, provided that the above | ||
copyright notice and this permission notice appear in all copies. | ||
|
||
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES | ||
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF | ||
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR | ||
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES | ||
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN | ||
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF | ||
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,49 +1,60 @@ | ||
## AWS Lambda Extension for Probot | ||
# `@probot/adapter-aws-lambda-serverless` | ||
|
||
A [Probot](https://github.com/probot/probot) extension to make it easier to run your Probot Apps in AWS Lambda. | ||
> Adapter to run a [Probot](https://probot.github.io/) application function in [AWS Lambda](https://aws.amazon.com/lambda/) using the [Serverless Framework](https://github.com/serverless/serverless) | ||
|
||
[![Build Status](https://github.com/probot/adapter-aws-lambda-serverless/workflows/Test/badge.svg)](https://github.com/probot/adapter-aws-lambda-serverless/actions) | ||
|
||
## Usage | ||
|
||
```shell | ||
$ npm install @probot/serverless-lambda | ||
npm install @probot/adapter-aws-lambda-serverless | ||
``` | ||
|
||
```javascript | ||
// handler.js | ||
const { serverless } = require("@probot/serverless-lambda"); | ||
const { | ||
createLambdaFunction, | ||
createProbot, | ||
} = require("@probot/adapter-aws-lambda-serverless"); | ||
const appFn = require("./"); | ||
module.exports.probot = serverless(appFn); | ||
module.exports.webhooks = createLambdaFunction(appFn, { | ||
probot: createProbot(), | ||
}); | ||
``` | ||
|
||
## Configuration | ||
|
||
This package moves the functionality of `probot run` into a handler suitable for usage on AWS Lambda + API Gateway. 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 [AWS CLI](https://aws.amazon.com/cli/) 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. | ||
|
||
To use `.env` files with the [Serverless Framework](https://github.com/serverless/serverless), you can install the [serverless-dotenv-plugin](https://www.serverless.com/plugins/serverless-dotenv-plugin). This will take care of keeping your secrets out of your deployed package. | ||
|
||
### Serverless dotenv plugin usage | ||
|
||
```yaml | ||
plugins: | ||
- serverless-dotenv-plugin # Load .env as environment variables | ||
You need to add [environment variables to configure Probot](https://probot.github.io/docs/configuration/) to your Lambda function. If you use the [Serverless App](https://app.serverless.com/), you can add parameters for `APP_ID`, `PRIVATE_KEY`, `WEBHOOK_SECRET`, the use these parameters in `serverless.yaml`. | ||
|
||
```yml | ||
provider: | ||
name: aws | ||
runtime: nodejs12.x | ||
lambdaHashingVersion: 20201221 | ||
environment: | ||
APP_ID: ${param:APP_ID} | ||
PRIVATE_KEY: ${param:PRIVATE_KEY} | ||
WEBHOOK_SECRET: ${param:WEBHOOK_SECRET} | ||
NODE_ENV: production | ||
LOG_LEVEL: debug | ||
|
||
functions: | ||
webhooks: | ||
handler: handler.webhooks | ||
events: | ||
- httpApi: | ||
path: /api/github/webhooks | ||
method: post | ||
``` | ||
|
||
For the private key, since AWS environment variables cannot be multiline strings, you could [Base64 encode](https://nodejs.org/api/buffer.html#buffer_buffers_and_character_encodings) the `.pem` file you get from the GitHub App or use [KMS](https://aws.amazon.com/kms/) to encrypt and store the key. | ||
|
||
## Differences from `probot run` | ||
|
||
#### Local Development | ||
Make sure to configure your GitHub App registration's webhook URL to `<your lambda's URL>/api/github/webhooks`. | ||
|
||
Since Lambda 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 to AWS. | ||
## Examples | ||
|
||
#### Long running tasks | ||
- [example-aws-lambda-serverless](https://github.com/probot/example-aws-lambda-serverless/#readme) - Official example application that is continuously deployed to AWS Lambda | ||
|
||
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). | ||
Add yours! | ||
|
||
#### If you use [HTTP routes](https://probot.github.io/docs/http/) or [WEBHOOK_PATH](https://probot.github.io/docs/configuration/) | ||
## LICENSE | ||
|
||
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`. | ||
[ISC](LICENSE) |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,108 +1,17 @@ | ||
const { Probot } = require("probot"); | ||
const { resolve } = require("probot/lib/helpers/resolve-app-function"); | ||
const { getPrivateKey } = require("@probot/get-private-key"); | ||
const { template } = require("./views/probot"); | ||
|
||
let probot; | ||
|
||
const loadProbot = (appFn) => { | ||
probot = | ||
probot || | ||
new Probot({ | ||
id: process.env.APP_ID, | ||
secret: process.env.WEBHOOK_SECRET, | ||
privateKey: getPrivateKey(), | ||
}); | ||
|
||
if (typeof appFn === "string") { | ||
appFn = resolve(appFn); | ||
} | ||
|
||
probot.load(appFn); | ||
|
||
return probot; | ||
}; | ||
|
||
const lowerCaseKeys = (obj = {}) => | ||
Object.keys(obj).reduce( | ||
(accumulator, key) => | ||
Object.assign(accumulator, { [key.toLocaleLowerCase()]: obj[key] }), | ||
{} | ||
); | ||
|
||
module.exports.serverless = (appFn) => { | ||
return async (event, context) => { | ||
// 🤖 A friendly homepage if there isn't a payload | ||
if (event.httpMethod === "GET" && event.path === "/probot") { | ||
const res = { | ||
statusCode: 200, | ||
headers: { | ||
"Content-Type": "text/html", | ||
}, | ||
body: template, | ||
}; | ||
return res; | ||
} | ||
|
||
// Otherwise let's listen handle the payload | ||
probot = probot || loadProbot(appFn); | ||
|
||
// Ends function immediately after callback | ||
context.callbackWaitsForEmptyEventLoop = false; | ||
|
||
// Determine incoming webhook event type | ||
const headers = lowerCaseKeys(event.headers); | ||
const e = headers["x-github-event"]; | ||
if (!e) { | ||
return { | ||
statusCode: 400, | ||
body: "X-Github-Event header is missing", | ||
}; | ||
} | ||
|
||
// If body is expected to be base64 encoded, decode it and continue | ||
if (event.isBase64Encoded) { | ||
event.body = Buffer.from(event.body, "base64").toString("utf8"); | ||
} | ||
|
||
// Convert the payload to an Object if API Gateway stringifies it | ||
event.body = | ||
typeof event.body === "string" ? JSON.parse(event.body) : event.body; | ||
|
||
// Bail for null body | ||
if (!event.body) { | ||
return { | ||
statusCode: 400, | ||
body: "Event body is null.", | ||
}; | ||
} | ||
|
||
// Do the thing | ||
console.log( | ||
`Received event ${e}${event.body.action ? "." + event.body.action : ""}` | ||
); | ||
if (event) { | ||
try { | ||
await probot.receive({ | ||
name: e, | ||
payload: event.body, | ||
}); | ||
return { | ||
statusCode: 200, | ||
body: JSON.stringify({ | ||
message: `Received ${e}.${event.body.action}`, | ||
}), | ||
}; | ||
} catch (err) { | ||
console.error(err); | ||
return { | ||
statusCode: 500, | ||
body: JSON.stringify(err), | ||
}; | ||
} | ||
} else { | ||
console.error({ event, context }); | ||
throw new Error("unknown error"); | ||
} | ||
}; | ||
}; | ||
const ProbotExports = require("probot"); | ||
const lambdaFunction = require("./lambda-function"); | ||
|
||
module.exports = { ...ProbotExports, createLambdaFunction }; | ||
|
||
/** | ||
* | ||
* @param {import('probot').ApplicationFunction} app | ||
* @param { { probot: import('probot').Probot } } options | ||
*/ | ||
function createLambdaFunction(app, { probot }) { | ||
// load app once outside of the function to prevent double | ||
// event handlers in case of container reuse | ||
probot.load(app); | ||
|
||
return lambdaFunction.bind(null, probot); | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,35 @@ | ||
module.exports = lambdaFunction; | ||
|
||
async function lambdaFunction(probot, event, context) { | ||
try { | ||
// Ends function immediately after callback | ||
context.callbackWaitsForEmptyEventLoop = false; | ||
|
||
// this could will be simpler once we ship `verifyAndParse()` | ||
gr2m marked this conversation as resolved.
Show resolved
Hide resolved
|
||
// see https://github.com/octokit/webhooks.js/issues/379 | ||
await probot.webhooks.verifyAndReceive({ | ||
id: | ||
event.headers["X-GitHub-Delivery"] || | ||
event.headers["x-github-delivery"], | ||
name: event.headers["X-GitHub-Event"] || event.headers["x-github-event"], | ||
signature: | ||
event.headers["X-Hub-Signature-256"] || | ||
event.headers["x-hub-signature-256"] || | ||
event.headers["X-Hub-Signature"] || | ||
event.headers["x-hub-signature"], | ||
payload: JSON.parse(event.body), | ||
}); | ||
|
||
return { | ||
statusCode: 200, | ||
body: '{"ok":true}', | ||
}; | ||
} catch (error) { | ||
probot.log.error(error); | ||
|
||
gr2m marked this conversation as resolved.
Show resolved
Hide resolved
|
||
return { | ||
statusCode: error.status || 500, | ||
error: "ooops", | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Could this be more helpful? For example maybe point users at docs for Cloudwatch logs on how to debug? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm not sure how? By adding a "documentation_url" key to the response? I'll create a follow up issue for further discussion to unblock this PR for now |
||
}; | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
👍🏽