Skip to content

Commit

Permalink
Merge pull request #78 from bywhitebird/74-manage-listening-github-re…
Browse files Browse the repository at this point in the history
…pository

74 manage listening GitHub repository
  • Loading branch information
arthur-fontaine authored Nov 17, 2023
2 parents b8abe0c + 579fc9e commit 8055215
Show file tree
Hide file tree
Showing 10 changed files with 459 additions and 22 deletions.
12 changes: 9 additions & 3 deletions apps/alakazam/nuxt.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,13 @@ export default defineNuxtConfig({
github: {
clientId: process.env.NUXT_OAUTH_GITHUB_CLIENT_ID,
clientSecret: process.env.NUXT_OAUTH_GITHUB_CLIENT_SECRET,
}
}
}
},
},
github: {
webhookSecret: process.env.NUXT_GITHUB_WEBHOOK_SECRET,
webhookUrl: process.env.NODE_ENV === 'development'
? undefined
: process.env.NUXT_GITHUB_WEBHOOK_URL,
},
},
})
2 changes: 2 additions & 0 deletions apps/alakazam/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,11 @@
"@octokit/rest": "^20.0.2",
"@pandacss/dev": "^0.14.0",
"@whitebird/ui": "workspace:*",
"consola": "^3.2.3",
"edgedb": "^1.4.1",
"eslint-plugin-vue": "^9.17.0",
"glob": "^10.3.4",
"ngrok": "4.3.3",
"npm-run-all": "^4.1.5",
"nuxt": "^3.7.1",
"nuxt-auth-utils": "^0.0.5",
Expand Down
47 changes: 47 additions & 0 deletions apps/alakazam/src/modules/ngrok.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import { defineNuxtModule } from '@nuxt/kit'
import { colors } from 'consola/utils'
import ngrok from 'ngrok'

const CONFIG_KEY = 'ngrok'

export default defineNuxtModule({
meta: {
name: 'nuxt-ngrok',
configKey: CONFIG_KEY,
},
async setup(resolvedOptions: Partial<ngrok.Ngrok.Options>, nuxt) {
if (nuxt.options.dev === false) {
return
}

const ngrokOptions: ngrok.Ngrok.Options = {
addr: nuxt.options.devServer.port,
...resolvedOptions,
}
const url = await ngrok.connect(ngrokOptions)

nuxt.options.runtimeConfig[CONFIG_KEY] = {
url,
}

nuxt.addHooks({
'devtools:initialized'() {
console.log(String.prototype.concat(
colors.blue(` ➜ Tunnel: `),
colors.underline(colors.cyan(url)),
))
},
'app:resolve'() {
if (!ngrokOptions.auth) {
console.warn(
`ngrok tunnel is exposed to the public without password protection! Consider setting the ${colors.bold(`${CONFIG_KEY}.auth`)} option.`,
)
}
},
'close'() {
ngrok.disconnect()
ngrok.kill()
}
})
},
})
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import * as valibot from "valibot"

import { updateComponents } from "~/server/handlers/component-generation/update-components"

export default defineEventHandler(async (event) => {
const githubEventName = event.headers.get("X-GitHub-Event")

switch (githubEventName) {
case "push": {
const body = await readBody(event)
const parsedBody = valibot.parse(valibot.object({
repository: valibot.object({
id: valibot.string(),
full_name: valibot.string(),
html_url: valibot.string(),
}),
}), body)

return await updateComponents({
repository: {
id: parsedBody.repository.id,
name: parsedBody.repository.full_name,
url: parsedBody.repository.html_url,
},
})
}
case "ping": {
return "pong"
}
default: {
throw createError({
statusCode: 400,
statusMessage: `Invalid GitHub event: ${githubEventName}`
})
}
}
})
39 changes: 33 additions & 6 deletions apps/alakazam/src/server/api/project/edit-project.post.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,14 +11,41 @@ const EditProjectSchema = valibot.object({

export default defineEventHandler(async (event) => {
const session = await getUserSession(event)
const configuration = useRuntimeConfig()

const body = await readBody(event)

const parsedBody = valibot.parse(EditProjectSchema, body)

return await editProject({
userId: session.user.id,
projectId: parsedBody.project.id,
projectName: parsedBody.project.name,
repositoryUrl: parsedBody.project.repositoryUrl,
})
const url = new URL(
configuration.github.webhookUrl !== undefined && configuration.github.webhookUrl !== ''
? configuration.github.webhookUrl
: configuration.ngrok.url
)
const webhookUrl = `${url.origin}/api/handle-github-webhook`

let editProjectParameters: Parameters<typeof editProject>[0] = {
user: { id: session.user.id },
project: {
id: parsedBody.project.id,
name: parsedBody.project.name,
},
}

if (parsedBody.project.repositoryUrl) {
editProjectParameters = {
...editProjectParameters,
repository: {
url: parsedBody.project.repositoryUrl,
},
webhook: {
url: webhookUrl,
secret: configuration.github.webhookSecret,
events: ['push'],
},
githubAccessToken: session.user.token,
}
}

return await editProject(editProjectParameters)
})
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
export const updateComponents = async (params: {
repository: {
id: string
name: string
url: string
}
}) => {
// TODO: Implement
}
45 changes: 36 additions & 9 deletions apps/alakazam/src/server/handlers/project/edit-project.ts
Original file line number Diff line number Diff line change
@@ -1,37 +1,64 @@
import { addWebhookToRepository } from "../repository/add-webhook-to-repository"
import { getProject } from "./get-project"

export const editProject = async (
{ userId, projectId, projectName, repositoryUrl }:
{ userId: string, projectId: string, projectName?: string, repositoryUrl?: string },
{ user, project, repository, webhook, githubAccessToken }: {
user: { id: string }
project: { id: string, name?: string, repositoryUrl?: string }
repository?: { url: string }
} & (
{
webhook?: never
githubAccessToken?: never
}
| {
webhook?: Parameters<typeof addWebhookToRepository>[1]['webhook']
githubAccessToken: string
repository: { url: string }
}
),
) => {
if (await getProject({ userId, projectId }) === null) {
if (await getProject({ userId: user.id, projectId: project.id }) === null) {
throw new Error('User is not a member of the organization')
}

return database
let updateDatabasePromise = database
.update(
database.Project,
(databaseProject) => ({
filter: database.op(databaseProject.id, '=', database.uuid(projectId)),
filter: database.op(databaseProject.id, '=', database.uuid(project.id)),
set: {
...(projectName !== undefined ? { name: projectName } : {}),
...(repositoryUrl !== undefined ? {
...project.name !== undefined && { name: project.name },
...repository?.url !== undefined && {
sources: {
'+=': database.insert(
database.ProjectSource,
{
githubRepository: database.insert(
database.GitHubRepository,
{
url: repositoryUrl,
url: repository.url,
},
),
},
)
}
} : {}),
},
},
}),
)
.run(database.client)

if (webhook !== undefined && repository !== undefined) {
updateDatabasePromise = updateDatabasePromise.then(async (d) => {
await addWebhookToRepository(githubAccessToken, {
repository: { url: repository.url },
webhook,
})

return d
})
}

return updateDatabasePromise
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
import { Octokit } from "@octokit/rest";

export const addWebhookToRepository = async (
githubAccessToken: string,
options: {
repository: (
|{
owner: string
name: string
}
|{
url: string
}
),
webhook: {
url: string
secret: string
events: string[],
comment?: string
}
}
) => {
const octokit = new Octokit({
auth: githubAccessToken,
})

if ('url' in options.repository) {
const { owner, name } = /^https:\/\/github.com\/(?<owner>[\w-]+)\/(?<name>[\w-]+)$/.exec(options.repository.url)?.groups ?? {}

if (owner === undefined || name === undefined) {
throw new Error('Invalid repository URL')
}

options.repository = {
owner,
name,
}
}

const repo = await octokit.repos.get({
owner: options.repository.owner,
repo: options.repository.name,
})

if (repo.data.permissions === undefined || repo.data.permissions.admin !== true) {
throw new Error('User does not have admin permissions on this repository')
}

const webhookUrlQuery = options.webhook.events.reduce<URLSearchParams>(
(query, event) => {
query.append('webhook_events', event)
return query
},
new URLSearchParams({
...(options.webhook.comment ?
{ webhook_events: options.webhook.comment } :
{})
})
)

const webhookUrl = new URL(options.webhook.url)
webhookUrl.search = webhookUrlQuery.toString()

octokit.repos.createWebhook({
owner: options.repository.owner,
repo: options.repository.name,
name: 'web',
active: true,
events: options.webhook.events,
config: {
url: webhookUrl.toString(),
content_type: 'json',
secret: options.webhook.secret,
insecure_ssl: process.env.NODE_ENV === 'development' ? Number(true) : Number(false),
},
headers: {
'X-GitHub-Api-Version': '2022-11-28'
},
})
}
Loading

0 comments on commit 8055215

Please sign in to comment.