diff options
author | Elizabeth Alexander Hunt <me@liz.coffee> | 2025-05-12 23:05:27 -0700 |
---|---|---|
committer | Elizabeth Alexander Hunt <me@liz.coffee> | 2025-05-12 23:33:52 -0700 |
commit | e49fda41176d025a671802be76c219d66167276f (patch) | |
tree | 6791f0ffe26b2405e13436a7e0c8d05d60e2b3bc | |
parent | bbaea13ee7125a9d289a74f0c173e7e75177e53c (diff) | |
download | ci-e49fda41176d025a671802be76c219d66167276f.tar.gz ci-e49fda41176d025a671802be76c219d66167276f.zip |
snapshot
-rwxr-xr-x | hooks/mod.ts | 107 | ||||
-rw-r--r-- | hooks/queuer.ts | 47 | ||||
-rw-r--r-- | utils/either.ts | 25 | ||||
-rw-r--r-- | utils/mod.ts | 1 | ||||
-rw-r--r-- | utils/run.ts | 28 | ||||
-rw-r--r-- | utils/trace.ts | 47 |
6 files changed, 154 insertions, 101 deletions
diff --git a/hooks/mod.ts b/hooks/mod.ts index 9fc4501..9df7d67 100755 --- a/hooks/mod.ts +++ b/hooks/mod.ts @@ -4,8 +4,11 @@ import { getRequiredEnv, getStdout, invalidExecutionEntriesOf, - type Traceable, - TraceableImpl, + Traceable, + ITraceable, + ITraceableLogger, + IEither, + Either, } from "@liz-ci/utils"; import type { Job } from "@liz-ci/model"; @@ -13,49 +16,6 @@ const SERVER_CONFIG = { host: "0.0.0.0", port: 9000, }; - -type QueuePosition = string; -interface IJobQueuer { - queue: ( - job: Traceable<Job>, - ) => Promise<{ ok?: QueuePosition; err?: unknown }>; -} - -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>>; } @@ -85,39 +45,36 @@ class HealthCheckActivity implements IHealthCheckActivity { } } -const aPost = (r: Traceable<Request>): Traceable<{ ok?: Request, err?: Response }> => - r.map((req) => { - const {item: request, logger} = req; - const {method} = request; - if (method !== "POST") { - const msg = "that's not how you pet me (⋟﹏⋞) try post instead~"; - logger.warn(msg); - const r405 = new Response(msg + "\n", {status: 405}); - return {err: r405}; - } - return {ok: request}; - }); +const aPost = (req: Traceable<Request>): IEither<Response, Request> => { + const {item: request, logger} = req; + const {method} = request; + if (method !== "POST") { + const msg = "that's not how you pet me (⋟﹏⋞) try post instead~"; + logger.warn(msg); + return { err: new Response(msg + "\n", {status: 405}) }; + } + return {ok: request}; +}; -type JsonTransformer<JsonT, R> = (json: Traceable<JsonT>) => Traceable<{ ok?: R; err?: Response }>; +type JsonTransformer<JsonT, R> = (json: Traceable<JsonT>) => Either<string, R>; const aJson = <BodyT, JsonT = unknown>(jsonTransformer: JsonTransformer<JsonT, BodyT>) => (r: Traceable<Request>) => r - .map(async ({item: request, logger}): Promise<{ ok?: JsonT, err?: Response }> => { - + .map(async ({item: request, logger}): Promise<Either<string, JsonT>> => { try { return {ok: <JsonT>(await request.json())}; } catch (e) { const err = "seems to be invalid JSON (>//<) can you fix?"; logger.warn(err); - const r400 = new Response(msg + "\n", {status: 400}); - return {err: r400}; + return { err }; } }) - .flatMapAsync(TraceableImpl.promiseify((t): Traceable<{ ok?: BodyT, err?: Response }> => { + .flatMapAsync(TraceableImpl.promiseify((t): Traceable<Either<string, BodyT>> => { const {item: {err, ok: json}} = t; - if (err) return <Traceable<{ err: Response }>>t; - return t.map(() => json!).flatMap(jsonTransformer); + if (err) return t.map(() => ({ err })); + return t.map(() => json!).map(jsonTransformer); })) + interface IJobHookActivity { processHook(req: Traceable<Request>): Traceable<Promise<Response>>; } @@ -126,31 +83,31 @@ class JobHookActivityImpl implements IJobHookActivity { constructor(private readonly queuer: IJobQueuer) {} private getJob( - j: Traceable<unknown>, - ): Traceable<{ ok?: Job; err?: Response }> { - return j.map(({ logger, item }) => { + { logger, item }: Traceable<unknown>, + ): Either<string, Job> { const isObject = (o: unknown): o is Record<string, unknown> => typeof o === "object" && !Array.isArray(o) && !!o; if (!isObject(item) || !isObject(item.arguments)|| typeof item.type !== "string") { - const err = "your reqwest seems compwetewy mawfomed \\(-.-)/\n"; - logger.warn(err); - return { err: new Response(err, { status: 400 }) }; + const err = "seems like a pwetty mawfomed job \\(-.-)/"; + logger.warn(err) + return { err }; } const ok = { type: item.type, arguments: item.arguments }; const invalidArgEntries = invalidExecutionEntriesOf({type: ok.type, ...ok.arguments}); if (invalidArgEntries.length > 0) { - const err = "your reqwest seems invawid (´。﹏。`) can you fix? uwu"; + const err = "your reqwest seems invawid (´。﹏。`) can you fix? uwu\n" + invalidArgEntries; logger.warn(err); - return { err: new Response(err, { status: 400 }) }; + return { err }; } return { ok: <Job>ok }; - }); } public processHook(r: Traceable<Request>) { - return r.flatMap(aPost).flatMap((t) => { + return r.map(aPost) + .map((t) => { + either((err) => ({ err }), (request) => ) const {item: {ok: request, err}} = t; if (err) return <Traceable<{ err: Response }>> t; return t.map(() => request!).map(aJson(this.getJob)); diff --git a/hooks/queuer.ts b/hooks/queuer.ts new file mode 100644 index 0000000..1461809 --- /dev/null +++ b/hooks/queuer.ts @@ -0,0 +1,47 @@ +import type { IEither, ITraceable, ITraceableLogger } from "@liz-ci/utils"; +import type { Job } from "@liz-ci/model"; + +type QueuePosition = string; +export class QueueError extends Error {} +export interface IJobQueuer<TJob> { + queue: <L extends ITraceableLogger<L>>( + job: ITraceable<TJob, L>, + ) => Promise<IEither<QueueError, QueuePosition>>; +} + +export class LaminarJobQueuer implements IJobQueuer<Job> { + constructor( + private readonly queuePositionPrefix: string, + ) {} + + public async queue({ item: job, logger }: Traceable<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(laminarCommand)).mapBoth( + (e) => { + const err = `we bwoke oh noesss D:`; + logger.error(err, e); + return Either.left<QueueError, QueuePosition>(e); + }, + (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 Either.right<QueueError, QueuePosition>(jobUrl); + }, + ); + } +} diff --git a/utils/either.ts b/utils/either.ts new file mode 100644 index 0000000..d21c796 --- /dev/null +++ b/utils/either.ts @@ -0,0 +1,25 @@ +export interface IEither<E, T> { + ok?: T; + err?: E; + mapBoth: <Ee, Tt>( + errBranch: (e: E) => Ee, + okBranch: (o: T) => Tt, + ) => IEither<Ee, Tt>; +} + +export class Either<E, T> implements IEither<E, T> { + private constructor(readonly err?: E, readonly ok?: T) {} + + public mapBoth<Ee, Tt>(errBranch: (e: E) => Ee, okBranch: (t: T) => Tt) { + if (this.err) return new Either<Ee, Tt>(errBranch(this.err)); + return new Either<Ee, Tt>(undefined, okBranch(this.ok!)); + } + + static left<E, T>(e: E) { + return new Either<E, T>(e); + } + + static right<E, T>(t: T) { + return new Either<E, T>(undefined, t); + } +} diff --git a/utils/mod.ts b/utils/mod.ts index a8a0751..53ea173 100644 --- a/utils/mod.ts +++ b/utils/mod.ts @@ -1,4 +1,5 @@ export * from "./trace.ts"; +export * from "./either.ts"; export * from "./env.ts"; export * from "./run.ts"; export * from "./secret.ts"; diff --git a/utils/run.ts b/utils/run.ts index 60ae1e6..06e7d9f 100644 --- a/utils/run.ts +++ b/utils/run.ts @@ -1,7 +1,10 @@ +import { Either } from "./mod.ts"; + +export class ProcessError extends Error {} export const getStdout = async ( cmd: string[] | string, options: Deno.CommandOptions = {}, -): Promise<string> => { +): Promise<Either<ProcessError, string>> => { const [exec, ...args] = (typeof cmd === "string") ? cmd.split(" ") : cmd; const command = new Deno.Command(exec, { args, @@ -10,12 +13,21 @@ export const getStdout = async ( ...options, }); - const { code, stdout, stderr } = await command.output(); - - const stdoutText = new TextDecoder().decode(stdout); - const stderrText = new TextDecoder().decode(stderr); - - if (code !== 0) throw new Error(`command failed\n${stderrText}`); + try { + const { code, stdout, stderr } = await command.output(); + const stdoutText = new TextDecoder().decode(stdout); + const stderrText = new TextDecoder().decode(stderr); - return stdoutText; + if (code !== 0) { + return Either.left<ProcessError, string>( + new ProcessError(`command failed\n${stderrText}`), + ); + } + return Either.right<ProcessError, string>(stdoutText); + } catch (e) { + if (e instanceof Error) { + return Either.left<ProcessError, string>(e); + } + throw new Error("unknown error " + e); + } }; diff --git a/utils/trace.ts b/utils/trace.ts index eb4ac2f..373f37e 100644 --- a/utils/trace.ts +++ b/utils/trace.ts @@ -25,12 +25,15 @@ export interface ITraceable<T, L extends ITraceableLogger<L>> { mapper: ITraceableMapper<T, L, ITraceableTuple<U>>, ) => ITraceable<U, L>; peek: (peek: ITraceableMapper<T, L, void>) => ITraceable<T, L>; - flatMap: <U>(mapper: ITraceableMapper<T, L, ITraceable<U, L>>) => ITraceable<U, L>; - flatMapAsync<U>(mapper: ITraceableMapper<T, L, Promise<ITraceable<U, L>>>): ITraceable<Promise<U>, L>; + flatMap: <U>( + mapper: ITraceableMapper<T, L, ITraceable<U, L>>, + ) => ITraceable<U, L>; + flatMapAsync<U>( + mapper: ITraceableMapper<T, L, Promise<ITraceable<U, L>>>, + ): ITraceable<Promise<U>, L>; } -export class TraceableLogger - implements ITraceableLogger<TraceableLogger> { +export class TraceableLogger implements ITraceableLogger<TraceableLogger> { private readonly logger: Logger = console; constructor( private readonly traces = [() => `[${new Date().toISOString()}]`], @@ -62,23 +65,30 @@ export class TraceableLogger } } -export class TraceableImpl< +class TraceableImpl< T, L extends ITraceableLogger<L>, > implements ITraceable<T, L> { - private constructor(readonly item: T, readonly logger: L) {} + protected constructor(readonly item: T, readonly logger: L) {} public map<U>(mapper: ITraceableMapper<T, L, U>) { const result = mapper(this); return new TraceableImpl(result, this.logger); } - public flatMap<U>(mapper: ITraceableMapper<T, L, ITraceable<U, L>>): ITraceable<U, L> { + public flatMap<U>( + mapper: ITraceableMapper<T, L, ITraceable<U, L>>, + ): ITraceable<U, L> { return mapper(this); } - public flatMapAsync<U>(mapper: ITraceableMapper<T, L, Promise<ITraceable<U, L>>>): ITraceable<Promise<U>, L> { - return new TraceableImpl(mapper(this).then(({ item }) => item), this.logger); + public flatMapAsync<U>( + mapper: ITraceableMapper<T, L, Promise<ITraceable<U, L>>>, + ): ITraceable<Promise<U>, L> { + return new TraceableImpl( + mapper(this).then(({ item }) => item), + this.logger, + ); } public peek(peek: ITraceableMapper<T, L, void>) { @@ -95,20 +105,21 @@ export class TraceableImpl< mapper: ITraceableMapper<T, L, U>, ): ITraceableMapper<Promise<T>, L, Promise<U>> { return (traceablePromise) => - traceablePromise.flatMapAsync(async (t) => { - const item = await t.item; - return t.map(() => item).map(mapper); - }).item; + traceablePromise.flatMapAsync(async (t) => { + const item = await t.item; + return t.map(() => item).map(mapper); + }).item; } +} - static withClassTrace<C extends Object, T, L extends ITraceableLogger<L>>(c: C): ITraceableMapper<T, L, ITraceableTuple<T>> { +export class Traceable<T> extends TraceableImpl<T, TraceableLogger> { + static withClassTrace<C extends object, T>( + c: C, + ): ITraceableMapper<T, TraceableLogger, ITraceableTuple<T>> { return (t) => [t.item, () => c.constructor.name]; } static from<T>(t: T) { - return new TraceableImpl(t, new TraceableLogger()); + return new Traceable(t, new TraceableLogger()); } } - -export interface Traceable<T, L extends ITraceableLogger<L> = TraceableLogger> extends ITraceable<T, L> { -} |