diff --git a/.env.example b/.env.example index 6e94876..8f9d0f5 100644 --- a/.env.example +++ b/.env.example @@ -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 diff --git a/README.md b/README.md index eb63f45..ea8cadf 100644 --- a/README.md +++ b/README.md @@ -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:///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. diff --git a/index.js b/index.js index 2340a0e..f896f05 100644 --- a/index.js +++ b/index.js @@ -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, })); }) diff --git a/index.njk b/index.njk index 595dc8d..3d0c287 100644 --- a/index.njk +++ b/index.njk @@ -10,10 +10,9 @@

Towtruck

- diff --git a/octokitApp.js b/octokitApp.js new file mode 100644 index 0000000..331985b --- /dev/null +++ b/octokitApp.js @@ -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 }; diff --git a/package-lock.json b/package-lock.json index 9ff775b..199f2ad 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,7 +9,8 @@ "version": "0.0.1", "license": "MIT", "dependencies": { - "@octokit/rest": "^21.0.2" + "@octokit/app": "^15.1.0", + "nunjucks": "^3.2.4" }, "devDependencies": { "@eslint/js": "^9.9.1", @@ -196,6 +197,90 @@ "node": ">= 8" } }, + "node_modules/@octokit/app": { + "version": "15.1.0", + "resolved": "https://registry.npmjs.org/@octokit/app/-/app-15.1.0.tgz", + "integrity": "sha512-TkBr7QgOmE6ORxvIAhDbZsqPkF7RSqTY4pLTtUQCvr6dTXqvi2fFo46q3h1lxlk/sGMQjqyZ0kEahkD/NyzOHg==", + "license": "MIT", + "dependencies": { + "@octokit/auth-app": "^7.0.0", + "@octokit/auth-unauthenticated": "^6.0.0", + "@octokit/core": "^6.1.2", + "@octokit/oauth-app": "^7.0.0", + "@octokit/plugin-paginate-rest": "^11.0.0", + "@octokit/types": "^13.0.0", + "@octokit/webhooks": "^13.0.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/@octokit/auth-app": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/@octokit/auth-app/-/auth-app-7.1.1.tgz", + "integrity": "sha512-kRAd6yelV9OgvlEJE88H0VLlQdZcag9UlLr7dV0YYP37X8PPDvhgiTy66QVhDXdyoT0AleFN2w/qXkPdrSzINg==", + "license": "MIT", + "dependencies": { + "@octokit/auth-oauth-app": "^8.1.0", + "@octokit/auth-oauth-user": "^5.1.0", + "@octokit/request": "^9.1.1", + "@octokit/request-error": "^6.1.1", + "@octokit/types": "^13.4.1", + "lru-cache": "^10.0.0", + "universal-github-app-jwt": "^2.2.0", + "universal-user-agent": "^7.0.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/@octokit/auth-oauth-app": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/@octokit/auth-oauth-app/-/auth-oauth-app-8.1.1.tgz", + "integrity": "sha512-5UtmxXAvU2wfcHIPPDWzVSAWXVJzG3NWsxb7zCFplCWEmMCArSZV0UQu5jw5goLQXbFyOr5onzEH37UJB3zQQg==", + "license": "MIT", + "dependencies": { + "@octokit/auth-oauth-device": "^7.0.0", + "@octokit/auth-oauth-user": "^5.0.1", + "@octokit/request": "^9.0.0", + "@octokit/types": "^13.0.0", + "universal-user-agent": "^7.0.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/@octokit/auth-oauth-device": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/@octokit/auth-oauth-device/-/auth-oauth-device-7.1.1.tgz", + "integrity": "sha512-HWl8lYueHonuyjrKKIup/1tiy0xcmQCdq5ikvMO1YwkNNkxb6DXfrPjrMYItNLyCP/o2H87WuijuE+SlBTT8eg==", + "license": "MIT", + "dependencies": { + "@octokit/oauth-methods": "^5.0.0", + "@octokit/request": "^9.0.0", + "@octokit/types": "^13.0.0", + "universal-user-agent": "^7.0.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/@octokit/auth-oauth-user": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/@octokit/auth-oauth-user/-/auth-oauth-user-5.1.1.tgz", + "integrity": "sha512-rRkMz0ErOppdvEfnemHJXgZ9vTPhBuC6yASeFaB7I2yLMd7QpjfrL1mnvRPlyKo+M6eeLxrKanXJ9Qte29SRsw==", + "license": "MIT", + "dependencies": { + "@octokit/auth-oauth-device": "^7.0.1", + "@octokit/oauth-methods": "^5.0.0", + "@octokit/request": "^9.0.1", + "@octokit/types": "^13.0.0", + "universal-user-agent": "^7.0.0" + }, + "engines": { + "node": ">= 18" + } + }, "node_modules/@octokit/auth-token": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/@octokit/auth-token/-/auth-token-5.1.1.tgz", @@ -205,6 +290,19 @@ "node": ">= 18" } }, + "node_modules/@octokit/auth-unauthenticated": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/@octokit/auth-unauthenticated/-/auth-unauthenticated-6.1.0.tgz", + "integrity": "sha512-zPSmfrUAcspZH/lOFQnVnvjQZsIvmfApQH6GzJrkIunDooU1Su2qt2FfMTSVPRp7WLTQyC20Kd55lF+mIYaohQ==", + "license": "MIT", + "dependencies": { + "@octokit/request-error": "^6.0.1", + "@octokit/types": "^13.0.0" + }, + "engines": { + "node": ">= 18" + } + }, "node_modules/@octokit/core": { "version": "6.1.2", "resolved": "https://registry.npmjs.org/@octokit/core/-/core-6.1.2.tgz", @@ -250,43 +348,65 @@ "node": ">= 18" } }, - "node_modules/@octokit/openapi-types": { - "version": "22.2.0", - "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-22.2.0.tgz", - "integrity": "sha512-QBhVjcUa9W7Wwhm6DBFu6ZZ+1/t/oYxqc2tp81Pi41YNuJinbFRx8B133qVOrAaBbF7D/m0Et6f9/pZt9Rc+tg==", - "license": "MIT" - }, - "node_modules/@octokit/plugin-paginate-rest": { - "version": "11.3.3", - "resolved": "https://registry.npmjs.org/@octokit/plugin-paginate-rest/-/plugin-paginate-rest-11.3.3.tgz", - "integrity": "sha512-o4WRoOJZlKqEEgj+i9CpcmnByvtzoUYC6I8PD2SA95M+BJ2x8h7oLcVOg9qcowWXBOdcTRsMZiwvM3EyLm9AfA==", + "node_modules/@octokit/oauth-app": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/@octokit/oauth-app/-/oauth-app-7.1.3.tgz", + "integrity": "sha512-EHXbOpBkSGVVGF1W+NLMmsnSsJRkcrnVmDKt0TQYRBb6xWfWzoi9sBD4DIqZ8jGhOWO/V8t4fqFyJ4vDQDn9bg==", "license": "MIT", "dependencies": { - "@octokit/types": "^13.5.0" + "@octokit/auth-oauth-app": "^8.0.0", + "@octokit/auth-oauth-user": "^5.0.1", + "@octokit/auth-unauthenticated": "^6.0.0-beta.1", + "@octokit/core": "^6.0.0", + "@octokit/oauth-authorization-url": "^7.0.0", + "@octokit/oauth-methods": "^5.0.0", + "@types/aws-lambda": "^8.10.83", + "universal-user-agent": "^7.0.0" }, "engines": { "node": ">= 18" - }, - "peerDependencies": { - "@octokit/core": ">=6" } }, - "node_modules/@octokit/plugin-request-log": { - "version": "5.3.1", - "resolved": "https://registry.npmjs.org/@octokit/plugin-request-log/-/plugin-request-log-5.3.1.tgz", - "integrity": "sha512-n/lNeCtq+9ofhC15xzmJCNKP2BWTv8Ih2TTy+jatNCCq/gQP/V7rK3fjIfuz0pDWDALO/o/4QY4hyOF6TQQFUw==", + "node_modules/@octokit/oauth-authorization-url": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/@octokit/oauth-authorization-url/-/oauth-authorization-url-7.1.1.tgz", + "integrity": "sha512-ooXV8GBSabSWyhLUowlMIVd9l1s2nsOGQdlP2SQ4LnkEsGXzeCvbSbCPdZThXhEFzleGPwbapT0Sb+YhXRyjCA==", "license": "MIT", "engines": { "node": ">= 18" + } + }, + "node_modules/@octokit/oauth-methods": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/@octokit/oauth-methods/-/oauth-methods-5.1.2.tgz", + "integrity": "sha512-C5lglRD+sBlbrhCUTxgJAFjWgJlmTx5bQ7Ch0+2uqRjYv7Cfb5xpX4WuSC9UgQna3sqRGBL9EImX9PvTpMaQ7g==", + "license": "MIT", + "dependencies": { + "@octokit/oauth-authorization-url": "^7.0.0", + "@octokit/request": "^9.1.0", + "@octokit/request-error": "^6.1.0", + "@octokit/types": "^13.0.0" }, - "peerDependencies": { - "@octokit/core": ">=6" + "engines": { + "node": ">= 18" } }, - "node_modules/@octokit/plugin-rest-endpoint-methods": { - "version": "13.2.4", - "resolved": "https://registry.npmjs.org/@octokit/plugin-rest-endpoint-methods/-/plugin-rest-endpoint-methods-13.2.4.tgz", - "integrity": "sha512-gusyAVgTrPiuXOdfqOySMDztQHv6928PQ3E4dqVGEtOvRXAKRbJR4b1zQyniIT9waqaWk/UDaoJ2dyPr7Bk7Iw==", + "node_modules/@octokit/openapi-types": { + "version": "22.2.0", + "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-22.2.0.tgz", + "integrity": "sha512-QBhVjcUa9W7Wwhm6DBFu6ZZ+1/t/oYxqc2tp81Pi41YNuJinbFRx8B133qVOrAaBbF7D/m0Et6f9/pZt9Rc+tg==", + "license": "MIT" + }, + "node_modules/@octokit/openapi-webhooks-types": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/@octokit/openapi-webhooks-types/-/openapi-webhooks-types-8.3.0.tgz", + "integrity": "sha512-vKLsoR4xQxg4Z+6rU/F65ItTUz/EXbD+j/d4mlq2GW8TsA4Tc8Kdma2JTAAJ5hrKWUQzkR/Esn2fjsqiVRYaQg==", + "license": "MIT" + }, + "node_modules/@octokit/plugin-paginate-rest": { + "version": "11.3.3", + "resolved": "https://registry.npmjs.org/@octokit/plugin-paginate-rest/-/plugin-paginate-rest-11.3.3.tgz", + "integrity": "sha512-o4WRoOJZlKqEEgj+i9CpcmnByvtzoUYC6I8PD2SA95M+BJ2x8h7oLcVOg9qcowWXBOdcTRsMZiwvM3EyLm9AfA==", "license": "MIT", "dependencies": { "@octokit/types": "^13.5.0" @@ -325,30 +445,50 @@ "node": ">= 18" } }, - "node_modules/@octokit/rest": { - "version": "21.0.2", - "resolved": "https://registry.npmjs.org/@octokit/rest/-/rest-21.0.2.tgz", - "integrity": "sha512-+CiLisCoyWmYicH25y1cDfCrv41kRSvTq6pPWtRroRJzhsCZWZyCqGyI8foJT5LmScADSwRAnr/xo+eewL04wQ==", + "node_modules/@octokit/types": { + "version": "13.5.0", + "resolved": "https://registry.npmjs.org/@octokit/types/-/types-13.5.0.tgz", + "integrity": "sha512-HdqWTf5Z3qwDVlzCrP8UJquMwunpDiMPt5er+QjGzL4hqr/vBVY/MauQgS1xWxCDT1oMx1EULyqxncdCY/NVSQ==", "license": "MIT", "dependencies": { - "@octokit/core": "^6.1.2", - "@octokit/plugin-paginate-rest": "^11.0.0", - "@octokit/plugin-request-log": "^5.3.1", - "@octokit/plugin-rest-endpoint-methods": "^13.0.0" + "@octokit/openapi-types": "^22.2.0" + } + }, + "node_modules/@octokit/webhooks": { + "version": "13.3.0", + "resolved": "https://registry.npmjs.org/@octokit/webhooks/-/webhooks-13.3.0.tgz", + "integrity": "sha512-TUkJLtI163Bz5+JK0O+zDkQpn4gKwN+BovclUvCj6pI/6RXrFqQvUMRS2M+Rt8Rv0qR3wjoMoOPmpJKeOh0nBg==", + "license": "MIT", + "dependencies": { + "@octokit/openapi-webhooks-types": "8.3.0", + "@octokit/request-error": "^6.0.1", + "@octokit/webhooks-methods": "^5.0.0" }, "engines": { "node": ">= 18" } }, - "node_modules/@octokit/types": { - "version": "13.5.0", - "resolved": "https://registry.npmjs.org/@octokit/types/-/types-13.5.0.tgz", - "integrity": "sha512-HdqWTf5Z3qwDVlzCrP8UJquMwunpDiMPt5er+QjGzL4hqr/vBVY/MauQgS1xWxCDT1oMx1EULyqxncdCY/NVSQ==", + "node_modules/@octokit/webhooks-methods": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/@octokit/webhooks-methods/-/webhooks-methods-5.1.0.tgz", + "integrity": "sha512-yFZa3UH11VIxYnnoOYCVoJ3q4ChuSOk2IVBBQ0O3xtKX4x9bmKb/1t+Mxixv2iUhzMdOl1qeWJqEhouXXzB3rQ==", "license": "MIT", - "dependencies": { - "@octokit/openapi-types": "^22.2.0" + "engines": { + "node": ">= 18" } }, + "node_modules/@types/aws-lambda": { + "version": "8.10.145", + "resolved": "https://registry.npmjs.org/@types/aws-lambda/-/aws-lambda-8.10.145.tgz", + "integrity": "sha512-dtByW6WiFk5W5Jfgz1VM+YPA21xMXTuSFoLYIDY0L44jDLLflVPtZkYuu3/YxpGcvjzKFBZLU+GyKjR0HOYtyw==", + "license": "MIT" + }, + "node_modules/a-sync-waterfall": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/a-sync-waterfall/-/a-sync-waterfall-1.0.1.tgz", + "integrity": "sha512-RYTOHHdWipFUliRFMCS4X2Yn2X8M87V/OpSqWzKKOGhzqyUxzyVmhHDH9sAvG+ZuQf/TAOFsLCpMw09I1ufUnA==", + "license": "MIT" + }, "node_modules/acorn": { "version": "8.12.1", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.12.1.tgz", @@ -455,6 +595,19 @@ "integrity": "sha512-Nik3Sc0ncrMK4UUdXQmAnRtzmNQTAAXmXIopizwZ1W1t8QmfJj+zL4OA2I7XPTPW5z5TDqv4hRo/JzouDJnX3A==", "license": "Apache-2.0" }, + "node_modules/binary-extensions": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", + "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", + "devOptional": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/brace-expansion": { "version": "1.1.11", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", @@ -1000,7 +1153,7 @@ "version": "2.1.1", "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", - "dev": true, + "devOptional": true, "license": "MIT", "engines": { "node": ">=0.10.0" @@ -1010,7 +1163,7 @@ "version": "4.0.3", "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "is-extglob": "^2.1.1" @@ -1127,6 +1280,12 @@ "dev": true, "license": "MIT" }, + "node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "license": "ISC" + }, "node_modules/minimatch": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", @@ -1479,6 +1638,12 @@ "node": ">= 0.8.0" } }, + "node_modules/universal-github-app-jwt": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/universal-github-app-jwt/-/universal-github-app-jwt-2.2.0.tgz", + "integrity": "sha512-G5o6f95b5BggDGuUfKDApKaCgNYy2x7OdHY0zSMF081O0EJobw+1130VONhrA7ezGSV2FNOGyM+KQpQZAr9bIQ==", + "license": "MIT" + }, "node_modules/universal-user-agent": { "version": "7.0.2", "resolved": "https://registry.npmjs.org/universal-user-agent/-/universal-user-agent-7.0.2.tgz", diff --git a/package.json b/package.json index f9c3d0c..b2fc985 100644 --- a/package.json +++ b/package.json @@ -15,7 +15,7 @@ "author": "dxw", "license": "MIT", "dependencies": { - "@octokit/rest": "^21.0.2", + "@octokit/app": "^15.1.0", "nunjucks": "^3.2.4" }, "devDependencies": {
- dxw's repos + {{ name }}'s repos