Skip to content
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

Merged
merged 5 commits into from
Feb 10, 2021
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions LICENSE
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.
59 changes: 35 additions & 24 deletions README.md
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)
125 changes: 17 additions & 108 deletions index.js
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);
}
35 changes: 35 additions & 0 deletions lambda-function.js
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()`
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍🏽

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",
Copy link
Collaborator

Choose a reason for hiding this comment

The 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?

Copy link
Contributor Author

Choose a reason for hiding this comment

The 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

};
}
}
Loading