Skip to content

Commit

Permalink
Merge pull request #13 from dxw/feature/use-github-app-auth
Browse files Browse the repository at this point in the history
Use GitHub app auth
  • Loading branch information
danlivings-dxw authored Sep 9, 2024
2 parents ecd0e38 + fb45cf4 commit f4c0a5c
Show file tree
Hide file tree
Showing 7 changed files with 317 additions and 62 deletions.
6 changes: 5 additions & 1 deletion .env.example
Original file line number Diff line number Diff line change
@@ -1 +1,5 @@
GITHUB_PERSONAL_ACCESS_TOKEN=
APP_ID=123456
PRIVATE_KEY_PATH=/path/to/app.private-key.pem
CLIENT_ID=SomeClientId123
CLIENT_SECRET=secret
WEBHOOK_SECRET=secret
46 changes: 46 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,2 +1,48 @@
Towtruck is an application to aid maintenence of dxw's repos.
It aims to make it easier to keep on top of which repos need updates applying.


## Configuration

Towtruck is set up as a [GitHub App](https://docs.github.com/en/apps).


### GitHub App settings

The first step is to register a new app as described [here](https://docs.github.com/en/apps/creating-github-apps/registering-a-github-app/registering-a-github-app):
- For **9**, no callback URL is currently required, so this step can be skipped.
- For **11**, no user authentication is currently required, so this step can be skipped.
- Skip **12** as Towtruck does not use device flow authentication.
- For **13** and **14**, there is no additional in-app setup performed by Towtruck, so these steps can be skipped.
- Skip **15** as Towtruck does receive GitHub webhooks and should be configured to listen for them.
- For **16**, the webhook URL should be configured to `https://<base Towtruck URL>/api/github/webhooks`.
Alternatively, for development, a [Smee.io](https://smee.io/) channel can be used.
- For **17**, a strong, randomly-generated secret should be used.
- For **18**, SSL verification should be used.
- For **19**, see the **Permissions** section below for a list of required permissions.
- For **20**, see the **Webhooks** section below for a list of required webhooks to listen to.
- For **21**, **Any account** should be used in production when Towtruck is used to monitor multiple organisations.
Otherwise, **Only this account** should be used.

Once the app is registered, it should be installed to an account to allow Towtruck to track it.
GitHub have instructions to do this [here](https://docs.github.com/en/apps/using-github-apps/installing-your-own-github-app).


#### Permissions

Towtruck is still in early development so the exact set of needed permissions has not been finalised.


#### Webhooks

Towtruck is still in early development so the exact set of needed webhooks has not been finalised.


### Environment variables

In order for Towtruck to communicate with the GitHub API, it needs several pieces of information, configured through environment variables:
- `APP_ID`: The unique numeric ID assigned to the GitHub App.
- `PRIVATE_KEY_PATH`: The private key used to sign access token requests. Towtruck expects this to be an absolute path to a `.pem` file generated by GitHub in the app settings.
- `CLIENT_ID`: A unique alphanumeric ID assigned to the GitHub App.
- `CLIENT_SECRET`: A token used to authenticate API requests. These are generated by GitHub in the app settings.
- `WEBHOOK_SECRET`: A user-defined secret used to authenticate GitHub to Towtruck for receiving webhooks. This must be exactly the same as it is entered in the app settings on GitHub.
44 changes: 27 additions & 17 deletions index.js
Original file line number Diff line number Diff line change
@@ -1,35 +1,45 @@
import { createServer } from "http";
import { Octokit } from "@octokit/rest";
import nunjucks from "nunjucks";
import { OctokitApp } from "./octokitApp.js";

nunjucks.configure({
autoescape: true,
watch: true,
});

const ACCESS_TOKEN = process.env.GITHUB_PERSONAL_ACCESS_TOKEN;

const octokit = new Octokit({
auth: ACCESS_TOKEN,
});

const httpServer = createServer(async (_, response) => {
const repos = await getRepos({ org: "dxw" });

const template = nunjucks.render("index.njk", {
repos,
const httpServer = createServer(async (request, response) => {
if (await OctokitApp.middleware(request, response)) return;

// Currently we only want to support single-account installations.
// There doesn't seem to be a neat way to get the installation ID from an account name,
// so we will use `eachInstallation` to loop (hopefully once) and just take the first (hopefully only)
// element from `installations` so that we can have more meaningful template names in Nunjucks.
//
// We can enforce this one-installation approach through GitHub by configuring the app to be
// "Only on this account" when registering the app.

const installations = [];
await OctokitApp.app.eachInstallation(async octokit => {
const name = octokit.installation.account.login;

const repos = await getReposForInstallation(octokit);

installations.push({
name,
repos,
});
});

const template = nunjucks.render("index.njk", installations[0]);

return response.end(template);
});

const getRepos = async ({ org }) => {
const getReposForInstallation = async ({ octokit, installation }) => {
return octokit
.request(`GET /orgs/${org}/repos`, {
org,
})
.request(installation.repositories_url)
.then(({ data }) => {
return data.map((repo) => ({
return data.repositories.map((repo) => ({
name: repo.name,
}));
})
Expand Down
3 changes: 1 addition & 2 deletions index.njk
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,9 @@
</head>
<body>
<h1>Towtruck</h1>

<table>
<caption>
dxw's repos
{{ name }}'s repos
</caption>
<thead>
<tr>
Expand Down
31 changes: 31 additions & 0 deletions octokitApp.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { readFileSync } from "fs";
import { App, createNodeMiddleware } from "@octokit/app";

const APP_ID = process.env.APP_ID;
const PRIVATE_KEY_PATH = process.env.PRIVATE_KEY_PATH;
const CLIENT_ID = process.env.CLIENT_ID;
const CLIENT_SECRET = process.env.CLIENT_SECRET;
const WEBHOOK_SECRET = process.env.WEBHOOK_SECRET;

const privateKey = readFileSync(PRIVATE_KEY_PATH).toString();

const app = new App({
appId: APP_ID,
privateKey,
oauth: {
clientId: CLIENT_ID,
clientSecret: CLIENT_SECRET,
},
webhooks: {
secret: WEBHOOK_SECRET,
},
});

// eslint-disable-next-line no-unused-vars
app.webhooks.onAny(({ id, name, payload }) => {
console.log(name, "event received");
});

const middleware = createNodeMiddleware(app);

export const OctokitApp = { app, middleware };
Loading

0 comments on commit f4c0a5c

Please sign in to comment.