diff options
author | Elizabeth Alexander Hunt <me@liz.coffee> | 2025-05-12 09:40:12 -0700 |
---|---|---|
committer | Elizabeth Alexander Hunt <me@liz.coffee> | 2025-05-12 09:54:58 -0700 |
commit | 723fa00cb14513eb1a517728d4464c4f148a29cc (patch) | |
tree | d32e2f725397d41b3ad7f886d61c16458dde5b37 /hooks | |
parent | 30729a0cf707d9022bae0a7baaba77379dc31fd5 (diff) | |
download | ci-723fa00cb14513eb1a517728d4464c4f148a29cc.tar.gz ci-723fa00cb14513eb1a517728d4464c4f148a29cc.zip |
The big refactor
Diffstat (limited to 'hooks')
-rwxr-xr-x | hooks/mod.ts | 321 |
1 files changed, 267 insertions, 54 deletions
diff --git a/hooks/mod.ts b/hooks/mod.ts index f858ad0..63a05fc 100755 --- a/hooks/mod.ts +++ b/hooks/mod.ts @@ -3,76 +3,289 @@ import { getRequiredEnv, getStdout, - loggerWithPrefix, - validateIdentifier, + invalidExecutionEntriesOf, + type Traceable, + TraceableImpl, } from "@liz-ci/utils"; +import type { Job } from "@liz-ci/model"; -const getRequestLogger = (req: Request) => { - const url = new URL(req.url); - const traceId = crypto.randomUUID(); - const getPrefix = () => - `[${ - new Date().toISOString() - }] RequestTrace=[${traceId}] @ [${url.pathname}] -X [${req.method}] |`; - return loggerWithPrefix(getPrefix); +const SERVER_CONFIG = { + host: "0.0.0.0", + port: 9000, }; -const addr = { port: 9000, hostname: "0.0.0.0" }; -Deno.serve(addr, async (req) => { - const logger = getRequestLogger(req); - logger.log("start"); +type QueuePosition = string; +interface IJobQueuer { + queue: ( + job: Traceable<Job>, + ) => Promise<{ ok?: QueuePosition; err?: unknown }>; +} - try { - const { pathname } = new URL(req.url); - if (pathname === "/health") { +class LaminarJobQueuer implements IJobQueuer { + constructor( + private readonly queuePositionPrefix: string, + ) {} + + public async queue({ item: job, logger }: Traceable<Job>) { + try { + const laminarCommand = [ + "laminarc", + "queue", + job.type, + ...Object.entries(job.arguments).map(([key, val]) => + `"${key}"="${val}"` + ), + ]; + + logger.log( + `im so excited to see how this queue job will end!! (>ᴗ<)`, + laminarCommand, + ); + + const output = await getStdout(laminarCommand); + logger.log(output); + + const [jobName, jobId] = output.split(":"); + const jobUrl = `${this.queuePositionPrefix}/jobs/${jobName}/${jobId}`; + + logger.log(`all queued up and weady to go~ (˘ω˘) => ${jobUrl}\n`); + return { ok: jobUrl, err: undefined }; + } catch (e) { + return { ok: undefined, err: e }; + } + } +} + +interface IHealthCheckActivity { + healthCheck(req: Traceable<Request>): Traceable<Promise<Response>>; +} + +class HealthCheckActivity implements IHealthCheckActivity { + public healthCheck( + req: Traceable<Request>, + ) { + return req.map(async ({ logger }) => { try { getRequiredEnv("LAMINAR_HOST"); await getStdout(["laminarc", "show-jobs"]); - return new Response("think im healthy!! lets get to work.\n", { - status: 200, - }); - } catch (e) { - logger.error(e); - return new Response("i need to eat more vegetables -.-\n", { - status: 500, - }); + const msg = `think im healthy!! (✿˘◡˘) ready to do work~\n`; + logger.log(msg); + return new Response( + msg, + { status: 200 }, + ); + } catch (error) { + logger.error(error); + return new Response( + "oh no, i need to eat more vegetables (。•́︿•̀。)...\n", + { status: 500 }, + ); } + }); + } +} + +interface IJobHookActivity { + processHook(req: Traceable<Request>): Traceable<Promise<Response>>; +} +type GetJobRequest = { jobType: string; args: unknown }; +class JobHookActivityImpl implements IJobHookActivity { + constructor(private readonly queuer: IJobQueuer) {} + + private getJob( + { item: { args, jobType }, logger }: Traceable<GetJobRequest>, + ): { ok?: Job; err?: string } { + if (Array.isArray(args) || typeof args !== "object" || args === null) { + return { err: "your reqwest seems compwetewy mawfomed (-.-)/\n" }; } - if (req.method !== "POST") { - return new Response("invalid method", { - status: 405, - }); + const invalidArgEntries = invalidExecutionEntriesOf({ + ...args, + jobType, + }); + if (invalidArgEntries.length > 0) { + const err = "your reqwest seems invawid (´。﹏。`) can you fix? uwu"; + logger.error(err, jobType, args); + return { err }; } - if (pathname === "/checkout_ci") { - const { remote, rev, refname } = await req.json(); - if (![remote, rev, refname].every(validateIdentifier)) { - logger.log("invalid reqwest"); - return new Response("invalid reqwest >:D\n", { - status: 400, - }); + return { + ok: <Job> { + type: jobType, + arguments: args, + }, + }; + } + + public processHook(r: Traceable<Request>) { + return r.map(async ({ item: request, logger }) => { + const { method } = request; + if (method !== "POST") { + const msg = "that's not how you pet me (⋟﹏⋞) try post instead~"; + logger.log(msg); + const r405 = new Response(msg, { status: 405 }); + return { err: r405 }; } - const laminar = await getStdout([ - "laminarc", - "queue", - "checkout_ci", - `remote="${remote}"`, - `rev="${rev}"`, - `refname="${refname}"`, - ]); - logger.log(`successful queue :D`, laminar); - return new Response(laminar + "\n", { - status: 200, - }); + const jobType = new URL(request.url).pathname.split("/")[1]; + const args = await request.json(); + return { ok: <GetJobRequest> { jobType, args } }; + }) + .map(TraceableImpl.promiseify((g) => { + const { item: { ok: jobRequest, err } } = g; + if (jobRequest) { + return g.map(() => jobRequest) + .map(this.getJob) + .map( + ({ item: { ok: jobRequest, err } }) => { + if (err) return { err: new Response(err, { status: 400 }) }; + return { ok: jobRequest }; + }, + ); + } + return g.map(() => ({ ok: undefined, err })); + })) + .map(TraceableImpl.promiseify(({ item: t }) => { + const { item: { ok: job, err } } = t; + if (err) return t.map(() => Promise.resolve(err)); + + return t.map(() => job!) + .map(this.queuer.queue) + .map(TraceableImpl.promiseify(({ item, logger }) => { + if (item.ok) { + return new Response(item.ok, { status: 200 }); + } + logger.error(item.err); + return new Response("i messed up D:\n", { status: 500 }); + })); + })); + } +} + +class LizCIServerImpl implements ILizCIServer { + constructor( + private readonly healthCheckActivity: IHealthCheckActivity, + private readonly jobHookActivity: IJobHookActivity, + ) {} + + private route( + req: Traceable<Request & { pathname: string }>, + ): Traceable<Promise<Response>> { + return req.flatMap((req) => { + const { logger, item: { method, pathname } } = req; + if (pathname === "/health") { + return this.healthCheckActivity.healthCheck(req); + } + return this.jobHookActivity.processHook(req); + }); + } + + public async serve(req: Request): Promise<Response> { + const traceId = crypto.randomUUID(); + const { pathname } = new URL(req.url); + const traceSupplier = () => `[${traceId} <- ${req.method}'d @ ${pathname}]`; + return TraceableImpl.from(req) + .bimap(({ item: req }) => [{ req, pathname }, traceSupplier]) + .flatMap(this.route) + .map(({ item, logger }) => + item.catch((e) => { + const errorMessage = `oh noes! something went wrong (ಥ_ಥ) so sowwy!`; + logger.error(errorMessage, e); + return new Response(`${errorMessage}\n`, { status: 500 }); + }) + ) + .item; + } +} + +class JobQueue { + private readonly logger: PrefixLogger; + private readonly url: URL; + private readonly pathname: string; + + constructor(private readonly request: Request, private readonly) { + this.url = new URL(request.url); + this.pathname = this.url.pathname; + this.logger = this.createLogger(); + } + + /** + * Creates a logger with request-specific context + */ + + /** + * Performs health checks on dependent services + */ + private async performHealthCheck(): Promise<void> { + } + + /** + * Handles health check requests + */ + private async handleHealthCheck(): Promise<Response> { + try { + await this.performHealthCheck(); + } catch (error) { + } + } + + /** + * Queues a job in the laminar system + */ + private async queueJob(jobName: string, args: JobRequest): Promise<Response> { + } + + /** + * Validates job request parameters + */ + private validateJobRequest( + jobName: string, + args: unknown, + ): { valid: boolean; response?: Response } { + } + + /** + * Main method to handle the request + */ + public async handle(): Promise<Response> { + this.logger.log("go! :DDD"); + + // Handle health check requests + if (this.pathname === "/health") { + return this.handleHealthCheck(); } - return new Response("idk what that is bro :((\n", { status: 404 }); - } catch (e) { - logger.error("uncaught exception", e); - return new Response("womp womp D:\n", { status: 500 }); - } finally { - logger.log("finish"); + // Validate HTTP method + if (this.request.method !== "POST") { + } + + // Extract job name from path + + if (!validation.valid) { + return validation.response!; + } + + // Queue the job + return this.queueJob(jobName, requestBody as JobRequest); + } + + /** + * Handles the entire request lifecycle, including error handling + */ + public async processRequest(): Promise<Response> { + try { + return await this.handle(); + } catch (error) { + } finally { + this.logger.log("allll done!"); + } } +} + +/** + * Entry point - starts the server + */ +Deno.serve(SERVER_CONFIG, async (request: Request) => { + const handler = new RequestHandler(request); + return handler.processRequest(); }); |