diff options
author | Elizabeth Alexander Hunt <me@liz.coffee> | 2025-05-18 22:54:15 -0700 |
---|---|---|
committer | Elizabeth Alexander Hunt <me@liz.coffee> | 2025-05-18 22:55:20 -0700 |
commit | d54e91c6582ed160cf2f2fcf977e48b4439d133b (patch) | |
tree | 5669367c4fa49bc0373b0c581ea3027218fd5e32 /hooks | |
parent | 9cf3fc0259730b7dcf47b3ab4a04369e39fb4614 (diff) | |
download | ci-d54e91c6582ed160cf2f2fcf977e48b4439d133b.tar.gz ci-d54e91c6582ed160cf2f2fcf977e48b4439d133b.zip |
snapshottheBigRefactor
Diffstat (limited to 'hooks')
-rw-r--r-- | hooks/deno.json | 2 | ||||
-rw-r--r-- | hooks/job/queue.ts | 0 | ||||
-rw-r--r-- | hooks/main.ts | 11 | ||||
-rw-r--r-- | hooks/mod.ts | 4 | ||||
-rw-r--r-- | hooks/queuer.ts | 44 | ||||
-rwxr-xr-x | hooks/server.ts | 213 | ||||
-rw-r--r-- | hooks/server/ci.ts | 55 | ||||
-rw-r--r-- | hooks/server/health.ts | 16 | ||||
-rw-r--r-- | hooks/server/job/activity.ts | 100 | ||||
-rw-r--r-- | hooks/server/job/mod.ts | 2 | ||||
-rw-r--r-- | hooks/server/job/queuer.ts | 78 | ||||
-rw-r--r-- | hooks/server/mod.ts | 40 |
12 files changed, 261 insertions, 304 deletions
diff --git a/hooks/deno.json b/hooks/deno.json index c4e8fca..cdaf63f 100644 --- a/hooks/deno.json +++ b/hooks/deno.json @@ -1,4 +1,4 @@ { - "name": "@liz-ci/hooks", + "name": "@emprespresso/ci-hooks", "exports": "./mod.ts" } diff --git a/hooks/job/queue.ts b/hooks/job/queue.ts deleted file mode 100644 index e69de29..0000000 --- a/hooks/job/queue.ts +++ /dev/null diff --git a/hooks/main.ts b/hooks/main.ts index 1348e57..d8e4f7f 100644 --- a/hooks/main.ts +++ b/hooks/main.ts @@ -1 +1,12 @@ #!/usr/bin/env -S deno run --allow-env --allow-net --allow-run + +import { LizCIServer } from "./server/mod.ts"; + +const server = new LizCIServer(); + +const SERVER_CONFIG = { + host: "0.0.0.0", + port: 9000, +}; + +Deno.serve(SERVER_CONFIG, (request: Request) => server.serve(request)); diff --git a/hooks/mod.ts b/hooks/mod.ts index 738003c..cc15112 100644 --- a/hooks/mod.ts +++ b/hooks/mod.ts @@ -1,2 +1,2 @@ -export * from "./queuer.ts"; -export * from "./server.ts"; +export * from "./server/mod.ts"; +export * from "./main.ts"; diff --git a/hooks/queuer.ts b/hooks/queuer.ts deleted file mode 100644 index d2987ca..0000000 --- a/hooks/queuer.ts +++ /dev/null @@ -1,44 +0,0 @@ -import { getStdout, type IEither, type Traceable } from "@liz-ci/utils"; -import type { Job } from "@liz-ci/model"; - -type QueuePosition = string; -export class QueueError extends Error {} -export interface IJobQueuer<TJob> { - queue: ( - job: TJob, - ) => Promise<IEither<QueueError, QueuePosition>>; -} - -export class LaminarJobQueuer implements IJobQueuer<Traceable<Job>> { - constructor( - private readonly queuePositionPrefix: string, - ) {} - - public async queue(j: Traceable<Job>) { - const { item: job, logger: _logger } = j; - const logger = _logger.addTracer(() => `[LaminarJobQueuer.queue.${job}]`); - 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, - ); - - return (await getStdout(j.map(() => laminarCommand))).mapRight( - (stdout) => { - logger.log(stdout); - - const [jobName, jobId] = stdout.split(":"); - const jobUrl = `${this.queuePositionPrefix}/jobs/${jobName}/${jobId}`; - - logger.log(`all queued up and weady to go~ (˘ω˘) => ${jobUrl}\n`); - return jobUrl; - }, - ); - } -} diff --git a/hooks/server.ts b/hooks/server.ts deleted file mode 100755 index 9a3f716..0000000 --- a/hooks/server.ts +++ /dev/null @@ -1,213 +0,0 @@ -import { - Either, - getRequiredEnv, - getStdout, - IEither, - isObject, - Traceable, - validateExecutionEntries, -} from "@liz-ci/utils"; -import { IJobQueuer } from "./mod.ts"; -import type { Job } from "@liz-ci/model"; - -const SERVER_CONFIG = { - host: "0.0.0.0", - port: 9000, -}; -interface IHealthCheckActivity<R> { - healthCheck(req: R): Promise<Response>; -} - -interface IJobHookActivity<R> { - processHook(req: R): Promise<Response>; -} -type GetJobRequest = { jobType: string; args: unknown }; -class JobHookActivityImpl implements IJobHookActivity<Traceable<Request>> { - constructor(private readonly queuer: IJobQueuer<Traceable<Job>>) {} - - private getJob<JsonT>(u: Traceable<JsonT>): Either<string, Job> { - const { logger: _logger, item } = u; - const logger = _logger.addTracer(() => "[getJob]"); - const couldBeJsonJob = isObject(item) && "arguments" in item && - "type" in item && item; - const couldBeArguments = couldBeJsonJob && isObject(item.arguments) && - item.arguments; - if (!couldBeJsonJob) { - const err = "seems like a pwetty mawfomed job \\(-.-)/"; - logger.warn(err); - return Either.left(err); - } - - return validateExecutionEntries({ - type: item.type, - ...couldBeArguments, - }).mapBoth((err) => { - const e = "your reqwest seems invawid (´。﹏。`) can you fix? uwu\n" + - err.toString(); - logger.warn(e); - return e; - }, (_ok) => <Job> item); - } - - public async processHook(r: Traceable<Request>) { - return await r.bimap(Traceable.withClassTrace(this)).map(aPost) - .map(aJson(this.getJob)); - } - // flatMapAsync(aJsonPost(this.getJob)) - // .map(TraceableImpl.promiseify((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(); - } - - // 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(); -}); diff --git a/hooks/server/ci.ts b/hooks/server/ci.ts new file mode 100644 index 0000000..cdb8372 --- /dev/null +++ b/hooks/server/ci.ts @@ -0,0 +1,55 @@ +import { + FourOhFourActivityImpl, + getRequiredEnv, + HealthCheckActivityImpl, + type HealthChecker, + type IFourOhFourActivity, + type IHealthCheckActivity, + type ITraceable, + PenguenoRequest, + type ServerTrace, + TraceUtil, +} from "@emprespresso/pengueno"; +import type { Job } from "@emprespresso/ci-model"; +import { + healthCheck as _healthCheck, + type IJobHookActivity, + type IJobQueuer, + JobHookActivityImpl, + LaminarJobQueuer, +} from "@emprespresso/ci-hooks"; + +export class LizCIServer { + constructor( + healthCheck: HealthChecker = _healthCheck, + jobQueuer: IJobQueuer<ITraceable<Job, ServerTrace>> = new LaminarJobQueuer( + getRequiredEnv("LAMINAR_URL").fold((err, val) => + err ? "https://ci.liz.coffee" : val + ), + ), + private readonly healthCheckActivity: IHealthCheckActivity = + new HealthCheckActivityImpl(healthCheck), + private readonly jobHookActivity: IJobHookActivity = + new JobHookActivityImpl(jobQueuer), + private readonly fourOhFourActivity: IFourOhFourActivity = + new FourOhFourActivityImpl(), + ) {} + + private route(req: ITraceable<PenguenoRequest, ServerTrace>) { + const url = new URL(req.get().url); + if (url.pathname === "/health") { + return this.healthCheckActivity.checkHealth(req); + } + if (url.pathname === "/job") { + return this.jobHookActivity.processHook(req); + } + return this.fourOhFourActivity.fourOhFour(req); + } + + public serve(req: Request): Promise<Response> { + return PenguenoRequest.from(req) + .bimap(TraceUtil.withClassTrace(this)) + .map(this.route) + .get(); + } +} diff --git a/hooks/server/health.ts b/hooks/server/health.ts index 41dfcb4..2f67aa4 100644 --- a/hooks/server/health.ts +++ b/hooks/server/health.ts @@ -1,23 +1,25 @@ import { getRequiredEnv, getStdout, + type HealthChecker, type HealthCheckInput, HealthCheckOutput, type IEither, type ITraceable, + type ServerTrace, TraceUtil, } from "@emprespresso/pengueno"; -export const healthCheck = <Trace>( - input: ITraceable<HealthCheckInput, Trace>, +export const healthCheck: HealthChecker = ( + input: ITraceable<HealthCheckInput, ServerTrace>, ): Promise<IEither<Error, HealthCheckOutput>> => input.bimap(TraceUtil.withFunctionTrace(healthCheck)) .move(getRequiredEnv("LAMINAR_HOST")) - // we need to test LAMINAR_HOST is propagated to getStdout for other procedures - .map(({ item }) => item.moveRight(["laminarc", "show-jobs"])) + // ensure LAMINAR_HOST is propagated to getStdout for other procedures + .map((e) => e.get().moveRight(["laminarc", "show-jobs"])) .map((i) => - i.item.mapRight(i.move.apply) + i.get().mapRight(i.move.apply) .flatMapAsync(getStdout.apply) - .then((gotJobs) => gotJobs.moveRight(HealthCheckOutput.YAASQUEEN)) + .then((gotJobs) => gotJobs.moveRight(HealthCheckOutput.YAASSSLAYQUEEN)) ) - .item; + .get(); diff --git a/hooks/server/job/activity.ts b/hooks/server/job/activity.ts new file mode 100644 index 0000000..14ea459 --- /dev/null +++ b/hooks/server/job/activity.ts @@ -0,0 +1,100 @@ +import { + Either, + ErrorSource, + type IActivity, + type IEither, + type ITraceable, + jsonModel, + JsonResponse, + LogLevel, + Metric, + PenguenoError, + type PenguenoRequest, + type ServerTrace, + TraceUtil, + validateExecutionEntries, +} from "@emprespresso/pengueno"; +import { isJob, type Job } from "@emprespresso/ci-model"; +import type { IJobQueuer } from "@emprespresso/ci-hooks"; + +const wellFormedJobMetric = Metric.fromName("Job.WellFormed"); + +const jobJsonTransformer = ( + j: ITraceable<unknown, ServerTrace>, +): IEither<PenguenoError, Job> => + j.bimap(TraceUtil.withMetricTrace(wellFormedJobMetric)) + .map((tJson) => { + if (!isJob(tJson) || !validateExecutionEntries(tJson)) { + const err = "seems like a pwetty mawfomed job \\(-.-)/"; + tJson.trace.addTrace(LogLevel.WARN).trace(err); + return Either.left<PenguenoError, Job>(new PenguenoError(err, 400)); + } + return Either.right<PenguenoError, Job>(tJson); + }) + .peek((tJob) => + tJob.trace.trace( + tJob.get().fold((err) => + err ? wellFormedJobMetric.failure : wellFormedJobMetric.success + ), + ) + ) + .get(); + +export interface IJobHookActivity { + processHook: IActivity; +} + +const jobHookRequestMetric = Metric.fromName("JobHook.process"); +export class JobHookActivityImpl implements IJobHookActivity { + constructor( + private readonly queuer: IJobQueuer<ITraceable<Job, ServerTrace>>, + ) {} + + private trace(r: ITraceable<PenguenoRequest, ServerTrace>) { + return r.bimap(TraceUtil.withClassTrace(this)) + .bimap( + TraceUtil.withMetricTrace(jobHookRequestMetric), + ); + } + + public processHook(r: ITraceable<PenguenoRequest, ServerTrace>) { + return this.trace(r).map(jsonModel(jobJsonTransformer)) + .map(async (tEitherJobJson) => { + const eitherJob = await tEitherJobJson.get(); + return eitherJob.flatMapAsync(async (job) => { + const eitherQueued = await tEitherJobJson.move(job) + .map(this.queuer.queue) + .get(); + return eitherQueued.mapLeft((e) => new PenguenoError(e.message, 500)); + }); + }) + .peek( + TraceUtil.promiseify((tJob) => + tJob.get().fold( + (err: PenguenoError | undefined, _val: string | undefined) => { + if (!err) { + tJob.trace.trace(jobHookRequestMetric.success); + tJob.trace.trace(`all queued up and weady to go :D !! ${_val}`); + return; + } + tJob.trace.trace( + err.source === ErrorSource.SYSTEM + ? jobHookRequestMetric.failure + : jobHookRequestMetric.warn, + ); + tJob.trace.addTrace(err.source).trace(`${err}`); + }, + ) + ), + ) + .map( + TraceUtil.promiseify((tEitherQueuedJob) => + new JsonResponse(r, tEitherQueuedJob.get(), { + status: tEitherQueuedJob.get() + .fold(({ status }, _val) => _val ? 200 : status), + }) + ), + ) + .get(); + } +} diff --git a/hooks/server/job/mod.ts b/hooks/server/job/mod.ts new file mode 100644 index 0000000..6b4ae85 --- /dev/null +++ b/hooks/server/job/mod.ts @@ -0,0 +1,2 @@ +export * from "./activity.ts"; +export * from "./queuer.ts"; diff --git a/hooks/server/job/queuer.ts b/hooks/server/job/queuer.ts new file mode 100644 index 0000000..6094183 --- /dev/null +++ b/hooks/server/job/queuer.ts @@ -0,0 +1,78 @@ +import { + getStdout, + type IEither, + type ITraceable, + LogLevel, + type Mapper, + memoize, + Metric, + type ServerTrace, + TraceUtil, +} from "@emprespresso/pengueno"; +import type { Job } from "@emprespresso/ci-model"; + +type QueuePosition = string; +export class QueueError extends Error {} +export interface IJobQueuer<TJob> { + queue: Mapper<TJob, Promise<IEither<QueueError, QueuePosition>>>; +} + +export class LaminarJobQueuer + implements IJobQueuer<ITraceable<Job, ServerTrace>> { + constructor( + private readonly queuePositionPrefix: string, + ) {} + + private static GetJobTypeTrace = (jobType: string) => + `LaminarJobQueue.Queue.${jobType}`; + private static JobTypeMetrics = memoize((jobType: string) => + Metric.fromName(LaminarJobQueuer.GetJobTypeTrace(jobType)) + ); + + public queue(j: ITraceable<Job, ServerTrace>) { + const { type: jobType } = j.get(); + const trace = LaminarJobQueuer.GetJobTypeTrace(jobType); + const metric = LaminarJobQueuer.JobTypeMetrics(trace); + + return j + .bimap(TraceUtil.withTrace(trace)) + .bimap(TraceUtil.withMetricTrace(metric)) + .map((j) => { + const { type: jobType, arguments: args } = j.get(); + const laminarCommand = [ + "laminarc", + "queue", + jobType, + ...Object.entries(args).map(([key, val]) => `"${key}"="${val}"`), + ]; + return laminarCommand; + }) + .peek((c) => + c.trace.trace( + `im so excited to see how this queue job will end!! (>ᴗ<): ${c.get().toString()}`, + ) + ) + .map(getStdout) + .peek( + TraceUtil.promiseify((q) => + q.trace.trace( + q.get().fold((err, _val) => err ? metric.failure : metric.success), + ) + ), + ) + .map(TraceUtil.promiseify((q) => + q.get().mapRight((stdout) => { + q.trace.addTrace(LogLevel.DEBUG).trace(`stdout ${stdout}`); + const [jobName, jobId] = stdout.split(":"); + const jobUrl = `${this.queuePositionPrefix}/jobs/${jobName}/${jobId}`; + + q.trace.trace(`all queued up and weady to go~ (˘ω˘) => ${jobUrl}`); + return jobUrl; + }).mapLeft((err) => { + q.trace.addTrace(LogLevel.ERROR).trace(err.toString()); + return err; + }) + )) + .get(); + } +} diff --git a/hooks/server/mod.ts b/hooks/server/mod.ts index b635b05..0a520f9 100644 --- a/hooks/server/mod.ts +++ b/hooks/server/mod.ts @@ -1,37 +1,3 @@ -import { - getRequiredEnv, - getStdout, - type HealthCheckInput, - HealthCheckOutput, - type IEither, - type ITraceable, - LogTraceable, - TraceUtil, -} from "@emprespresso/pengueno"; - -export class LizCIServer { - private constructor( - private readonly healthCheckActivity = HealthCheckActivity(healthCheck), - private readonly jobHookActivity = JobHookActivity(jobQueuer), - private readonly fourOhFourActivity = FourOhFourActivity(), - ) {} - - private async route(req: LogTraceable<Request>) { - return req.flatMap((req) => { - const { item: request } = req; - const url = new URL(request.url); - if (url.pathname === "/health") { - return this.healthCheckActivity.healthCheck(req); - } - if (url.pathname === "/job") { - return this.jobHookActivity.processHook(req); - } - }); - } - - public async serve(req: Request): Promise<Response> { - return LogTraceable(req).bimap(TraceUtil.withClassTrace(this)).map( - this.route, - ); - } -} +export * from "./ci.ts"; +export * from "./health.ts"; +export * from "./job/mod.ts"; |