Skip to content

Commit

Permalink
build: Netlify deploy (#9)
Browse files Browse the repository at this point in the history
  • Loading branch information
gr2m authored Jul 31, 2023
1 parent 74b7ba9 commit a16cf90
Show file tree
Hide file tree
Showing 17 changed files with 19,168 additions and 2,543 deletions.
9 changes: 9 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
TEST_REPOSITORY="your-user-name/your-test-repository"
TEST_REPOSITORY_CREATE_HOOK_TOKEN="<your-token-with-admin-access-to-test-repository>"

GITHUB_APP_ID="..."
GITHUB_APP_PRIVATE_KEY="-----BEGIN PRIVATE KEY-----\n...\n-----END PRIVATE KEY-----\n"
GITHUB_APP_CLIENT_ID="Iv1..."
GITHUB_APP_CLIENT_SECRET="..."
GITHUB_APP_WEBHOOK_SECRET="..."
DISCORD_WEBHOOKS_URL="https://discord.com/api/webhooks/..."
43 changes: 13 additions & 30 deletions .eslintrc.cjs
Original file line number Diff line number Diff line change
@@ -1,49 +1,32 @@
module.exports = {
plugins: [
"jest"
],
extends: [
"airbnb-base/legacy",
"airbnb-base/whitespace",
],
plugins: ["jest"],
extends: ["airbnb-base/legacy", "airbnb-base/whitespace"],
env: {
browser: true,
serviceworker: true,
es2021: true,
"jest/globals": true
},
globals: {
// secrets
APP_ID: "readonly",
APP_PK: "readonly",
CLIENT_ID: "readonly",
CLIENT_SECRET: "readonly",
WEBHOOK_SECRET: "readonly",
DISCORD_URL: "readonly",
"jest/globals": true,
},
parserOptions: {
ecmaVersion: 13,
ecmaFeatures: {
impliedStrict: true
impliedStrict: true,
},
sourceType: "module",
allowImportExportEverywhere: false
allowImportExportEverywhere: false,
},
ignorePatterns: [
'dist/',
'node_modules/',
'worker/',
],
ignorePatterns: ["dist/", "node_modules/", "worker/"],
rules: {
"no-console": 0,
"import/no-unresolved": 0,
"import/extensions": 0,
"no-restricted-syntax": 0,
"no-restricted-globals": 0,
camelcase: [2, {
allow: [
"avatar_url"
],
}]
}
camelcase: [
2,
{
allow: ["avatar_url"],
},
],
},
};
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -281,3 +281,6 @@ build
**/supabase/.env

worker

# Local Netlify folder
.netlify
6 changes: 6 additions & 0 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"deno.enable": true,
"deno.enablePaths": ["netlify/edge-functions"],
"deno.unstable": true,
"deno.importMap": ".netlify/edge-functions-import-map.json"
}
57 changes: 9 additions & 48 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,65 +37,26 @@ In order to run the project locally we need `node>=16` and `npm>=8` installed on

## 🖥️ Local development

To install the application:
To run the GitHub App code against a test repository, add `TEST_REPOSITORY` to your local `.env` file.

```shell
npm ci
```

Befor you can run the application we need to start the smee proxy:

```shell
npm run proxy
```

To start a local copy of the app on port `3000`:
To start the server locally at port `8888`:

```shell
npm start
```

The preconfigured app is of almost no use to anyone as it can only be installed by the preconfigured user and send webhooks to a dead server.

It is quite possible that some of the secrets are rendered invalid as well. THey serve as placeholders and should be replaced with values provided by your testing app.

## 📦 Deploy to production

### Cloudflare account

Set up a cloudflare account and enable workers, change `account_id` in [wrangler.toml](./wrangler.toml) to your account id.
### Netlify account

Go to your workers dashboard and create a new worker, select any template, adjust `name` in [wrangler.toml](./wrangler.toml) if it is taken.

Select the "Settings" tab on your newly created worker and click "Variables", add the following placeholders for now:
- `APP_ID`
- `APP_PK`
- `DISCORD_URL`
- `CLIENT_ID`
- `CLIENT_SECRET`
- `WEBHOOK_SECRET`

**Note**: At the very end of this process you will have to encrypt all the values for the publish command to work.

Copy the "Routes" URL provided by the worker for the next part.
Install the Netlify GitHub app in the repository and configure the environment variables
listed in [.env.example](.env.example).

### GitHub App

[Create a new GitHub application](https://docs.github.com/en/developers/apps/building-github-apps/creating-a-github-app) with scopes `issues:write` and `metadata:read` while also enabling tracking events.

Upon creation you should have plain-text values for `APP_ID`, `CLIENT_ID`.

Add the following secrets to your Cloudflare worker like so:

```
wrangler secret put APP_ID
```

Add the remaingin variables using the same CLI command

Click the "Generate a new client secret" button and copy the value of `CLIENT_SECRET`.
[Register a new GitHub application](https://docs.github.com/en/apps/creating-github-apps/registering-a-github-app/registering-a-github-app) with the permissions `issues:write` and `metadata:read`. Set the webhook URL to `<your netlify domain>/api/github/webhooks`.

In the webhook return URL copy the value of your worker route as described in the last step of the Cloudflare setup.
Once registered, you will be able to obtain all the `GITHUB_APP_*` credentials from the app settings.

It is advised you generate the `WEBHOOK_SECRET` using the following command:

Expand All @@ -112,12 +73,12 @@ Rename this file to `private-key.pem` for the next command to work:
openssl pkcs8 -topk8 -inform PEM -outform PEM -nocrypt -in private-key.pem -out private-key-pkcs8.key
```

Copy the contents of `private-key-pkcs8.key` to `APP_PK`. Note the
Copy the contents of `private-key-pkcs8.key` to `GITHUB_APP_PRIVATE_KEY`. Note the
string will need to be on one line joined with `\n`.

### Discord

Go to your server of choice, click "Settings" and then "Integrations", create a new webhook and copy the URL and paste that value into `DISCORD_URL`.
Go to your server of choice, click "Settings" and then "Integrations", create a new webhook and copy the URL and paste that value into `DISCORD_WEBHOOKS_URL`.

Now you are good to use the wrangler release workflows and deploy to production!

Expand Down
47 changes: 47 additions & 0 deletions dev.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import AppWebhookRelay from 'github-app-webhook-relay';
import { App } from '@octokit/app';

import githubApp from './github-app.js';

run(process.env);

async function run(env) {
const app = new App({
appId: Number(env.GITHUB_APP_ID),
privateKey: env.GITHUB_APP_PRIVATE_KEY,
webhooks: {
secret: env.GITHUB_APP_WEBHOOK_SECRET,
},
oauth: {
clientId: env.GITHUB_APP_CLIENT_ID,
clientSecret: env.GITHUB_APP_CLIENT_SECRET,
},
log: console,
});

const { data: appInfo } = await app.octokit.request('GET /app');

app.log.info(`Authenticated as ${appInfo.html_url}`);
app.log.info(`events: ${JSON.stringify(appInfo.events)}`);

// get installation access token for test repository
const [owner, repo] = env.TEST_REPOSITORY.split('/');

await githubApp(env, app);

const repositoryRelay = new AppWebhookRelay({
owner,
repo,
createHookToken: env.TEST_REPOSITORY_CREATE_HOOK_TOKEN,
app,
events: appInfo.events,
});

repositoryRelay.on('error', (error) => {
app.log.error('error: %s', error);
});

await repositoryRelay.start();

app.log.info('Started local webhook relay server');
}
38 changes: 38 additions & 0 deletions github-app.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
const GOOD_FIRST_REGEX = /^good\sfirst\sissue$/i;

/**
* @param {Record<string, string>} env
* @param {import("@octokit/app").App} app
*/
export default async function githubApp(env, app) {
app.log.info('App loaded');

app.webhooks.onAny(async ({ name, payload }) => {
const eventNameWithAction = payload.action ? `${name}.${payload.action}` : name;

app.log.info(`Event received: ${eventNameWithAction}`);
});

app.webhooks.on('issues.labeled', async (context) => {
const { name } = context.payload.label;

if (!GOOD_FIRST_REGEX.test(name)) return;

// send message to discord
const discordWebhookUrl = env.DISCORD_WEBHOOKS_URL;
const params = {
username: 'GFI-Catsup [beta]',
avatar_url: '/~https://github.com/open-sauced/assets/blob/master/logo.png?raw=true',
content: `New good first issue: ${context.payload.issue.html_url}`,
};

// send post request using fetch to webhook
await fetch(discordWebhookUrl, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(params),
});
});
}
70 changes: 70 additions & 0 deletions netlify/edge-functions/github-webhooks.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
// @ts-check

import { App } from "https://esm.sh/@octokit/app@14.0.0";
import githubApp from "../../github-app.js";

const requiredEnvironmentVariables = [
"GITHUB_APP_ID",
"GITHUB_APP_PRIVATE_KEY",
"GITHUB_APP_CLIENT_ID",
"GITHUB_APP_CLIENT_SECRET",
"GITHUB_APP_WEBHOOK_SECRET",
"DISCORD_WEBHOOKS_URL",
];

const missingEnvironmentVariables = requiredEnvironmentVariables.filter((name) => !Deno.env.get(name));

if (missingEnvironmentVariables.length) {
throw new Error(`Missing environment variables: ${missingEnvironmentVariables.join(", ")}`);
}

const app = new App({
appId: Number(Deno.env.get("GITHUB_APP_ID")),
privateKey: Deno.env.get("GITHUB_APP_PRIVATE_KEY"),
oauth: {
clientId: Deno.env.get("GITHUB_APP_CLIENT_ID"),
clientSecret: Deno.env.get("GITHUB_APP_CLIENT_SECRET"),
},
webhooks: {
secret: Deno.env.get("GITHUB_APP_WEBHOOK_SECRET"),
},
log: console,
});

githubApp(Deno.env.toObject(), app);

export const config = {
path: "/api/github/webhooks",
};

/**
* @param {Request} request
*/
export default async (request) => {
if (request.method !== "POST") {
app.log.warn(`Method ${request.method} not allowed`);
return new Response("Not found", { status: 404 });
}

const event = {
id: request.headers.get("x-github-delivery"),
name: request.headers.get("x-github-event"),
signature: request.headers.get("x-hub-signature-256"),
payload: await request.text(),
};

try {
await app.webhooks.verifyAndReceive(event);

return new Response('{ "ok": true }', {
headers: { "content-type": "application/json" },
});
} catch (error) {
app.log.error(error);

return new Response(`{ "error": "${error.message}" }`, {
status: 500,
headers: { "content-type": "application/json" },
});
}
};
45 changes: 45 additions & 0 deletions netlify/edge-functions/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
// @ts-check

import { App } from 'https://esm.sh/@octokit/app@13.1.8';

const requiredEnvironmentVariables = ['GITHUB_APP_ID', 'GITHUB_APP_PRIVATE_KEY'];

const missingEnvironmentVariables = requiredEnvironmentVariables.filter((name) => !Deno.env.get(name));

if (missingEnvironmentVariables.length) {
throw new Error(`Missing environment variables: ${missingEnvironmentVariables.join(', ')}`);
}

const app = new App({
appId: Number(Deno.env.get('GITHUB_APP_ID')),
privateKey: Deno.env.get('GITHUB_APP_PRIVATE_KEY'),
});

export const config = {
path: '/',
};

export default async () => {
const { data } = await app.octokit.request('GET /app');

return new Response(
`
<h1>GitHub App: ${data.name}</h1>
<p>Installation count: ${data.installations_count}</p>
<p>
<a href="/~https://github.com/apps/${data.slug}">
<img src="https://img.shields.io/static/v1?label=Install%20App:&message=${data.slug}&color=orange&logo=github&style=for-the-badge" alt="Install ${data.name}"/>
</a>
</p>
<p>
<a href="/~https://github.com/open-sauced/catsup/#readme">source code</a>
</p>
`,
{
headers: { 'content-type': 'text/html' },
},
);
};
Loading

0 comments on commit a16cf90

Please sign in to comment.