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

Use GitHub app auth #13

Merged
merged 3 commits into from
Sep 9, 2024
Merged
Show file tree
Hide file tree
Changes from all 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
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
richpjames marked this conversation as resolved.
Show resolved Hide resolved
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):
richpjames marked this conversation as resolved.
Show resolved Hide resolved
- 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;
Copy link
Contributor

Choose a reason for hiding this comment

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

I think it would be good to abstract this out of the callback passed to createServer into a named function, but that can come in a later PR.


// 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
Loading