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

74 manage listening GitHub repository #78

Merged
merged 3 commits into from
Nov 17, 2023
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
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