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 | |
parent | 9cf3fc0259730b7dcf47b3ab4a04369e39fb4614 (diff) | |
download | ci-theBigRefactor.tar.gz ci-theBigRefactor.zip |
snapshottheBigRefactor
36 files changed, 749 insertions, 495 deletions
@@ -1,3 +1,3 @@ { - "workspace": ["./model", "./worker", "./hooks", "./utils", "./u"] + "workspace": ["./model", "./u", "./utils", "./worker", "./hooks"] } 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"; diff --git a/model/deno.json b/model/deno.json index d22242e..afd9f22 100644 --- a/model/deno.json +++ b/model/deno.json @@ -1,5 +1,5 @@ { - "name": "@liz-ci/model", + "name": "@emprespresso/ci-model", "version": "0.1.0", "exports": "./mod.ts" } diff --git a/model/job.ts b/model/job.ts index 53d548f..a3b52dd 100644 --- a/model/job.ts +++ b/model/job.ts @@ -1,8 +1,13 @@ +import { isObject } from "@emprespresso/pengueno"; + export type JobArgT = Record<string, string>; export interface Job { readonly type: string; readonly arguments: JobArgT; } +export const isJob = (j: unknown): j is Job => + !!(isObject(j) && "arguments" in j && isObject(j.arguments) && + "type" in j && typeof j.type === "string" && j); export interface FetchCodeJobProps extends JobArgT { readonly remoteUrl: string; @@ -40,3 +45,17 @@ export interface AnsiblePlaybookJob extends Job { readonly type: "ansible_playbook"; readonly arguments: AnsiblePlaybookJobProps; } + +export interface CheckoutCiJobProps extends JobArgT { + readonly remote: string; + readonly refname: string; + readonly rev: string; + + readonly run: string; + readonly returnPath: string; +} + +export interface CheckoutCiJob extends Job { + readonly type: "checkout_ci"; + readonly arguments: CheckoutCiJobProps; +} diff --git a/model/pipeline.ts b/model/pipeline.ts index 06361a4..b0db1b7 100644 --- a/model/pipeline.ts +++ b/model/pipeline.ts @@ -1,13 +1,20 @@ -import type { FetchCodeJob, Job } from "./mod.ts"; - -export interface Pipeline { - getStages(): ReadonlyArray<PipelineStage>; - serialize(): string; -} +import { Either, type IEither, isObject } from "@emprespresso/pengueno"; +import { type FetchCodeJob, isJob, type Job } from "@emprespresso/ci-model"; export interface PipelineStage { readonly parallelJobs: Array<Job>; } +export const isPipelineStage = (t: unknown): t is PipelineStage => + isObject(t) && "parallelJobs" in t && Array.isArray(t.parallelJobs) && + t.parallelJobs.every((j) => isJob(j)); + +export interface Pipeline { + readonly serialJobs: Array<PipelineStage>; + serialize(): string; +} +export const isPipeline = (t: unknown): t is Pipeline => + isObject(t) && "serialJobs" in t && Array.isArray(t.serialJobs) && + t.serialJobs.every((p) => isPipelineStage(p)); export interface PipelineBuilder { addStage(stage: PipelineStage): PipelineBuilder; @@ -15,18 +22,22 @@ export interface PipelineBuilder { } export class PipelineImpl implements Pipeline { - constructor(private readonly serialJobs: ReadonlyArray<PipelineStage>) {} - - public getStages() { - return this.serialJobs; - } + constructor(public readonly serialJobs: Array<PipelineStage>) {} public serialize() { return JSON.stringify(this.serialJobs); } - public static from(s: string): Pipeline { - return new PipelineImpl(JSON.parse(s)); + public static from(s: string): IEither<Error, Pipeline> { + return Either.fromFailable<Error, unknown>(() => JSON.parse(s)).flatMap< + Pipeline + >(( + eitherPipelineJson, + ) => + isPipeline(eitherPipelineJson) + ? Either.right(eitherPipelineJson) + : Either.left(new Error("oh noes D: its a bad pipewine :((")) + ).mapRight((pipeline) => new PipelineImpl(pipeline.serialJobs)); } } diff --git a/u/fn/either.ts b/u/fn/either.ts index 9dc1027..8b233bf 100644 --- a/u/fn/either.ts +++ b/u/fn/either.ts @@ -10,7 +10,7 @@ export interface IEither<E, T> { errBranch: Mapper<E, Ee>, okBranch: Mapper<T, Tt>, ) => IEither<Ee, Tt>; - fold: <Tt>(folder: BiMapper<E | null, T | null, Tt>) => Tt; + fold: <Tt>(folder: BiMapper<E | undefined, T | undefined, Tt>) => Tt; moveRight: <Tt>(t: Tt) => IEither<E, Tt>; mapRight: <Tt>(mapper: Mapper<T, Tt>) => IEither<E, Tt>; mapLeft: <Ee>(mapper: Mapper<E, Ee>) => IEither<Ee, T>; @@ -31,8 +31,8 @@ export class Either<E, T> implements IEither<E, T> { return this.mapRight(() => t); } - public fold<R>(folder: BiMapper<E | null, T | null, R>): R { - return folder(this.err ?? null, this.ok ?? null); + public fold<R>(folder: BiMapper<E | undefined, T | undefined, R>): R { + return folder(this.err ?? undefined, this.ok ?? undefined); } public mapBoth<Ee, Tt>( diff --git a/u/fn/mod.ts b/u/fn/mod.ts index f0fbe88..265c6db 100644 --- a/u/fn/mod.ts +++ b/u/fn/mod.ts @@ -1,2 +1,3 @@ export * from "./callable.ts"; export * from "./either.ts"; +export * from "./memoize.ts"; diff --git a/u/leftpadesque/memoize.ts b/u/leftpadesque/memoize.ts new file mode 100644 index 0000000..95e6019 --- /dev/null +++ b/u/leftpadesque/memoize.ts @@ -0,0 +1,14 @@ +import type { Callable } from "@emprespresso/pengueno"; + +export const memoize = <R, F extends Callable<R>>(fn: F): F => { + const cache = new Map<string, R>(); + return ((...args: unknown[]): R => { + const key = JSON.stringify(args); + if (cache.has(key)) { + return cache.get(key)!; + } + const res = fn.apply(args); + cache.set(key, res); + return res; + }) as F; +}; diff --git a/u/leftpadesque/mod.ts b/u/leftpadesque/mod.ts index 801846a..63d8d7a 100644 --- a/u/leftpadesque/mod.ts +++ b/u/leftpadesque/mod.ts @@ -1,3 +1,4 @@ export * from "./object.ts"; export * from "./prepend.ts"; export * from "./debug.ts"; +export * from "./memoize.ts"; diff --git a/u/process/env.ts b/u/process/env.ts index 26e1158..65f4e63 100644 --- a/u/process/env.ts +++ b/u/process/env.ts @@ -10,3 +10,18 @@ export const getRequiredEnv = (name: string): IEither<Error, string> => new Error(`environment variable "${name}" is required D:`), ) ); + +export const getRequiredEnvVars = (vars: Array<string>) => + vars + .map((envVar) => + [envVar, getRequiredEnv(envVar)] as [string, IEither<Error, string>] + ) + .reduce((acc, x: [string, IEither<Error, string>]) => { + const [envVar, eitherVal] = x; + return acc.flatMap((args) => { + return eitherVal.mapRight((envValue) => ({ + ...args, + [envVar]: envValue, + })); + }); + }, Either.right<Error, Record<string, string>>({})); diff --git a/u/process/run.ts b/u/process/run.ts index cbf8c65..4954438 100644 --- a/u/process/run.ts +++ b/u/process/run.ts @@ -6,21 +6,21 @@ import { TraceUtil, } from "@emprespresso/pengueno"; -type Command = string[] | string; +export type Command = string[] | string; type CommandOutputDecoded = { code: number; stdoutText: string; stderrText: string; }; -export class ProcessError extends Error {} export const getStdout = <Trace>( c: ITraceable<Command, Trace>, options: Deno.CommandOptions = {}, -): Promise<IEither<ProcessError, string>> => +): Promise<IEither<Error, string>> => c.bimap(TraceUtil.withFunctionTrace(getStdout)) - .map(({ item: cmd, trace }) => { - trace.trace(`:> im gonna run this command! ${cmd}`); + .map((tCmd) => { + const cmd = tCmd.get(); + tCmd.trace.trace(`:> im gonna run this command! ${cmd}`); const [exec, ...args] = (typeof cmd === "string") ? cmd.split(" ") : cmd; return new Deno.Command(exec, { args, @@ -29,12 +29,12 @@ export const getStdout = <Trace>( ...options, }).output(); }) - .map(({ item: p }) => - Either.fromFailableAsync<Error, Deno.CommandOutput>(p) + .map((tOut) => + Either.fromFailableAsync<Error, Deno.CommandOutput>(tOut.get()) ) .map( - TraceUtil.promiseify(({ item: eitherOutput, trace }) => - eitherOutput.flatMap(({ code, stderr, stdout }) => + TraceUtil.promiseify((tEitherOut) => + tEitherOut.get().flatMap(({ code, stderr, stdout }) => Either .fromFailable<Error, CommandOutputDecoded>(() => { const stdoutText = new TextDecoder().decode(stdout); @@ -42,22 +42,23 @@ export const getStdout = <Trace>( return { code, stdoutText, stderrText }; }) .mapLeft((e) => { - trace.addTrace(LogLevel.ERROR).trace(`o.o wat ${e}`); - return new ProcessError(`${e}`); + tEitherOut.trace.addTrace(LogLevel.ERROR).trace(`o.o wat ${e}`); + return new Error(`${e}`); }) - .flatMap((decodedOutput): Either<ProcessError, string> => { + .flatMap((decodedOutput): Either<Error, string> => { const { code, stdoutText, stderrText } = decodedOutput; - trace.addTrace(LogLevel.DEBUG).trace( + tEitherOut.trace.addTrace(LogLevel.DEBUG).trace( `stderr hehehe ${stderrText}`, ); if (code !== 0) { const msg = `i weceived an exit code of ${code} i wanna zewoooo :<`; - trace.addTrace(LogLevel.ERROR).trace(msg); - return Either.left(new ProcessError(msg)); + tEitherOut.trace.addTrace(LogLevel.ERROR).trace(msg); + return Either.left(new Error(msg)); } return Either.right(stdoutText); }) ) ), - ).item; + ) + .get(); diff --git a/u/server/activity/fourohfour.ts b/u/server/activity/fourohfour.ts index 48740df..6449abd 100644 --- a/u/server/activity/fourohfour.ts +++ b/u/server/activity/fourohfour.ts @@ -1,4 +1,5 @@ import { + type IActivity, type ITraceable, JsonResponse, type PenguenoRequest, @@ -16,12 +17,20 @@ const messages = [ "ヽ(;▽;)ノ Eep! This route has ghosted you~", ]; const randomFourOhFour = () => messages[Math.random() * messages.length]; -export const FourOhFourActivity = ( - req: ITraceable<PenguenoRequest, ServerTrace>, -) => - req - .move( - new JsonResponse(req, randomFourOhFour(), { - status: 404, - }), - ); + +export interface IFourOhFourActivity { + fourOhFour: IActivity; +} + +export class FourOhFourActivityImpl implements IFourOhFourActivity { + public fourOhFour( + req: ITraceable<PenguenoRequest, ServerTrace>, + ) { + return req + .move( + new JsonResponse(req, randomFourOhFour(), { status: 404 }), + ) + .map((resp) => Promise.resolve(resp.get())) + .get(); + } +} diff --git a/u/server/activity/health.ts b/u/server/activity/health.ts index b9efa3a..0f54a99 100644 --- a/u/server/activity/health.ts +++ b/u/server/activity/health.ts @@ -1,4 +1,5 @@ import { + type IActivity, type IEither, type ITraceable, JsonResponse, @@ -14,39 +15,53 @@ export enum HealthCheckInput { CHECK, } export enum HealthCheckOutput { - YAASQUEEN, + YAASSSLAYQUEEN, +} + +export interface IHealthCheckActivity { + checkHealth: IActivity; } const healthCheckMetric = Metric.fromName("Health"); -export const HealthCheckActivity = ( - check: Mapper< +export interface HealthChecker extends + Mapper< ITraceable<HealthCheckInput, ServerTrace>, Promise<IEither<Error, HealthCheckOutput>> - >, -) => -(req: ITraceable<PenguenoRequest, ServerTrace>) => - req - .bimap(TraceUtil.withFunctionTrace(HealthCheckActivity)) - .bimap(TraceUtil.withMetricTrace(healthCheckMetric)) - .flatMap((r) => r.move(HealthCheckInput.CHECK).map(check)) - .map(TraceUtil.promiseify((h) => { - const health = h.get(); - health.mapBoth((e) => { - h.trace.trace(healthCheckMetric.failure); - h.trace.addTrace(LogLevel.ERROR).trace(`${e}`); - return new JsonResponse( - req, - "oh no, i need to eat more vegetables (。•́︿•̀。)...", - { status: 500 }, - ); - }, (_healthy) => { - h.trace.trace(healthCheckMetric.success); - const msg = `think im healthy!! (✿˘◡˘) ready to do work~`; - h.trace.trace(msg); - return new JsonResponse( - req, - msg, - { status: 200 }, - ); - }); - })); + > {} +export class HealthCheckActivityImpl implements IHealthCheckActivity { + constructor( + private readonly check: HealthChecker, + ) {} + + public checkHealth(req: ITraceable<PenguenoRequest, ServerTrace>) { + return req + .bimap(TraceUtil.withFunctionTrace(this.checkHealth)) + .bimap(TraceUtil.withMetricTrace(healthCheckMetric)) + .flatMap((r) => r.move(HealthCheckInput.CHECK).map(this.check)) + .peek(TraceUtil.promiseify((h) => + h.get().fold((err) => { + if (err) { + h.trace.trace(healthCheckMetric.failure); + h.trace.addTrace(LogLevel.ERROR).trace(`${err}`); + return; + } + h.trace.trace(healthCheckMetric.success); + }) + )) + .map(TraceUtil.promiseify((h) => + h.get() + .mapBoth( + () => "oh no, i need to eat more vegetables (。•́︿•̀。)...", + () => "think im healthy!! (✿˘◡˘) ready to do work~", + ) + .fold((errMsg, okMsg) => + new JsonResponse( + req, + errMsg ?? okMsg, + { status: errMsg ? 500 : 200 }, + ) + ) + )) + .get(); + } +} diff --git a/u/server/activity/mod.ts b/u/server/activity/mod.ts index 9bd512f..82d8ec4 100644 --- a/u/server/activity/mod.ts +++ b/u/server/activity/mod.ts @@ -1,15 +1,13 @@ -import type { PenguenoResponse, RequestFilter } from "@emprespresso/pengueno"; +import type { + ITraceable, + PenguenoRequest, + PenguenoResponse, + ServerTrace, +} from "@emprespresso/pengueno"; -export enum StatusOK { - FOLLOW = 300, - OK = 200, -} -export interface ActivityOk { - readonly status: StatusOK; -} - -export interface IActivity<Trace> - extends RequestFilter<ActivityOk, Trace, PenguenoResponse> { +export interface IActivity { + (req: ITraceable<PenguenoRequest, ServerTrace>): Promise<PenguenoResponse>; } export * from "./health.ts"; +export * from "./fourohfour.ts"; diff --git a/u/server/filter/json.ts b/u/server/filter/json.ts index c839707..4a2961e 100644 --- a/u/server/filter/json.ts +++ b/u/server/filter/json.ts @@ -3,46 +3,49 @@ import { type IEither, type ITraceable, LogLevel, + Metric, + PenguenoError, type PenguenoRequest, type RequestFilter, type ServerTrace, TraceUtil, } from "@emprespresso/pengueno"; -import { Metric } from "../../trace/mod.ts"; -type JsonTransformer<R, ParsedJson = unknown> = ( - json: ITraceable<ParsedJson, ServerTrace>, -) => IEither<Error, R>; +export interface JsonTransformer<R, ParsedJson = unknown> { + (json: ITraceable<ParsedJson, ServerTrace>): IEither<PenguenoError, R>; +} const ParseJsonMetric = Metric.fromName("JsonParse"); export const jsonModel = <MessageT>( jsonTransformer: JsonTransformer<MessageT>, -): RequestFilter<MessageT, Error> => +): RequestFilter<MessageT> => (r: ITraceable<PenguenoRequest, ServerTrace>) => - r - .bimap(TraceUtil.withMetricTrace(ParseJsonMetric)) + r.bimap(TraceUtil.withMetricTrace(ParseJsonMetric)) .map((j) => Either.fromFailableAsync<Error, MessageT>(j.get().json()) .then((either) => either.mapLeft((errReason) => { j.trace.addTrace(LogLevel.WARN).trace(`${errReason}`); - return new Error("seems to be invalid JSON (>//<) can you fix?"); + return new PenguenoError( + "seems to be invalid JSON (>//<) can you fix?", + 400, + ); }) ) ) - .flatMapAsync( - TraceUtil.promiseify((traceableEitherJson) => - traceableEitherJson.map((t) => - t.get().mapRight(traceableEitherJson.move).flatMap( - jsonTransformer, - ) + .peek( + TraceUtil.promiseify((traceableEither) => + traceableEither.get().mapBoth( + () => traceableEither.trace.trace(ParseJsonMetric.failure), + () => traceableEither.trace.trace(ParseJsonMetric.success), ) ), ) - .peek(TraceUtil.promiseify((traceableEither) => - traceableEither.get().mapBoth( - () => traceableEither.trace.trace(ParseJsonMetric.failure), - () => traceableEither.trace.trace(ParseJsonMetric.success), - ) - )) + .map( + TraceUtil.promiseify((traceableEitherJson) => + traceableEitherJson.get() + .mapRight(traceableEitherJson.move) + .flatMap(jsonTransformer) + ), + ) .get(); diff --git a/u/server/filter/method.ts b/u/server/filter/method.ts index 350f04c..6b0419d 100644 --- a/u/server/filter/method.ts +++ b/u/server/filter/method.ts @@ -1,8 +1,8 @@ import { Either, type ITraceable, - JsonResponse, LogLevel, + PenguenoError, type PenguenoRequest, type RequestFilter, type ServerTrace, @@ -22,7 +22,7 @@ type HttpMethod = export const requireMethod = ( methods: Array<HttpMethod>, -): RequestFilter<HttpMethod, JsonResponse> => +): RequestFilter<HttpMethod> => (req: ITraceable<PenguenoRequest, ServerTrace>) => req.bimap(TraceUtil.withFunctionTrace(requireMethod)) .move(Promise.resolve(req.get())) @@ -32,10 +32,10 @@ export const requireMethod = ( if (!methods.includes(method)) { const msg = "that's not how you pet me (⋟﹏⋞)~"; t.trace.addTrace(LogLevel.WARN).trace(msg); - return Either.left<JsonResponse, HttpMethod>( - new JsonResponse(req, msg, { status: 405 }), + return Either.left<PenguenoError, HttpMethod>( + new PenguenoError(msg, 405), ); } - return Either.right<JsonResponse, HttpMethod>(method); + return Either.right<PenguenoError, HttpMethod>(method); })) .get(); diff --git a/u/server/filter/mod.ts b/u/server/filter/mod.ts index 22ddad5..bbf37df 100644 --- a/u/server/filter/mod.ts +++ b/u/server/filter/mod.ts @@ -1,13 +1,29 @@ -import type { - IEither, - ITraceable, - PenguenoRequest, - ServerTrace, +import { + type IEither, + type ITraceable, + LogLevel, + type PenguenoRequest, + type ServerTrace, } from "@emprespresso/pengueno"; +export enum ErrorSource { + USER = LogLevel.WARN, + SYSTEM = LogLevel.ERROR, +} + +export class PenguenoError extends Error { + public readonly source: ErrorSource; + constructor(message: string, public readonly status: number) { + super(message); + this.source = Math.floor(status / 100) === 4 + ? ErrorSource.USER + : ErrorSource.SYSTEM; + } +} + export interface RequestFilter< T, - Err, + Err extends PenguenoError = PenguenoError, RIn = ITraceable<PenguenoRequest, ServerTrace>, > { (req: RIn): Promise<IEither<Err, T>>; diff --git a/u/trace/metrics.ts b/u/trace/metrics.ts index a26ee5d..4ddde06 100644 --- a/u/trace/metrics.ts +++ b/u/trace/metrics.ts @@ -4,6 +4,7 @@ import { type ITraceWith, type Mapper, type SideEffect, + type Supplier, } from "@emprespresso/pengueno"; export enum Unit { @@ -16,10 +17,13 @@ export interface IMetric { readonly time: IEmittableMetric; readonly failure: IMetric; readonly success: IMetric; - readonly _isIMetric: true; + readonly warn: IMetric; + readonly children: Supplier<Array<IMetric>>; + + readonly _tag: "IMetric"; } export const isIMetric = (t: unknown): t is IMetric => - isObject(t) && "_isIMetric" in t; + isObject(t) && "_tag" in t && t._tag === "IMetric"; export interface IEmittableMetric { readonly name: string; @@ -35,9 +39,9 @@ export class EmittableMetric implements IEmittableMetric { return { name: this.name, unit: this.unit, - _isMetricValue: true as true, emissionTimestamp: Date.now(), value, + _tag: "MetricValue", }; } } @@ -48,15 +52,21 @@ export class Metric implements IMetric { public readonly time: IEmittableMetric, public readonly failure: Metric, public readonly success: Metric, - public readonly _isIMetric: true = true, + public readonly warn: Metric, + public readonly _tag: "IMetric" = "IMetric", ) {} + public children() { + return [this.failure, this.success, this.warn]; + } + static fromName(name: string): Metric { return new Metric( new EmittableMetric(`${name}.count`, Unit.COUNT), new EmittableMetric(`${name}.elapsed`, Unit.MILLISECONDS), Metric.fromName(`${name}.failure`), Metric.fromName(`${name}.success`), + Metric.fromName(`${name}.warn`), ); } } @@ -66,10 +76,10 @@ export interface MetricValue { readonly unit: Unit; readonly value: number; readonly emissionTimestamp: number; - readonly _isMetricValue: true; + readonly _tag: "MetricValue"; } export const isMetricValue = (t: unknown): t is MetricValue => - isObject(t) && "_isMetricValue" in t; + isObject(t) && "_tag" in t && t._tag === "MetricValue"; export const isMetricsTraceSupplier = (t: unknown): t is MetricsTraceSupplier => isMetricValue(t) || isIMetric(t); @@ -98,7 +108,7 @@ export class MetricsTrace implements ITrace<MetricsTraceSupplier> { const foundMetricValues = this.tracing.flatMap(( [tracing, startedTracing], ) => - [tracing, tracing.success, tracing.failure] + [tracing, ...tracing.children()] .filter((_tracing) => metric === _tracing) .flatMap((metric) => [ this.addMetric(metric, startedTracing), diff --git a/u/trace/trace.ts b/u/trace/trace.ts index 72d4eef..e942066 100644 --- a/u/trace/trace.ts +++ b/u/trace/trace.ts @@ -41,18 +41,18 @@ export type LogMetricTraceSupplier = ITraceWith< >; export class LogMetricTrace implements ITrace<LogMetricTraceSupplier> { constructor( - private readonly logTrace: ITrace<LogTraceSupplier>, - private readonly metricsTrace: ITrace<MetricsTraceSupplier>, + private logTrace: ITrace<LogTraceSupplier>, + private metricsTrace: ITrace<MetricsTraceSupplier>, ) {} public addTrace( trace: LogTraceSupplier | MetricsTraceSupplier, ): LogMetricTrace { if (isMetricsTraceSupplier(trace)) { - this.metricsTrace.addTrace(trace); + this.metricsTrace = this.metricsTrace.addTrace(trace); return this; } - this.logTrace.addTrace(trace); + this.logTrace = this.logTrace.addTrace(trace); return this; } diff --git a/u/trace/util.ts b/u/trace/util.ts index dd8fb0d..302c8e4 100644 --- a/u/trace/util.ts +++ b/u/trace/util.ts @@ -7,6 +7,16 @@ import type { } from "@emprespresso/pengueno"; export class TraceUtil { + static withTrace<T, Trace>( + trace: string, + ): ITraceableMapper< + T, + ITraceableTuple<T, Trace | Array<Trace>>, + Trace + > { + return (t) => [t.get(), `[${trace}]`]; + } + static withMetricTrace<T, Trace extends MetricsTraceSupplier>( metric: IMetric, ): ITraceableMapper< @@ -24,7 +34,7 @@ export class TraceUtil { ITraceableTuple<T, Trace | Array<Trace>>, Trace > { - return (t) => [t.get(), `[${f.name}]`]; + return TraceUtil.withTrace(f.name); } static withClassTrace<C extends object, T, Trace>( @@ -34,7 +44,7 @@ export class TraceUtil { ITraceableTuple<T, Trace | Array<Trace>>, Trace > { - return (t) => [t.get(), `[${c.constructor.name}]`]; + return TraceUtil.withTrace(c.constructor.name); } static promiseify<T, U, Trace>( diff --git a/utils/deno.json b/utils/deno.json index b85c47f..16536f4 100644 --- a/utils/deno.json +++ b/utils/deno.json @@ -1,5 +1,5 @@ { - "name": "@liz-ci/utils", + "name": "@emprespresso/ci-utils", "version": "0.1.0", "exports": "./mod.ts" } diff --git a/worker/deno.json b/worker/deno.json index 5636d0a..90f50c9 100644 --- a/worker/deno.json +++ b/worker/deno.json @@ -1,4 +1,4 @@ { - "name": "@liz-ci/worker", + "name": "@emprespresso/ci-worker", "exports": "./mod.ts" } diff --git a/worker/jobs/checkout_ci.run.ts b/worker/jobs/checkout_ci.run.ts new file mode 100644 index 0000000..948a4eb --- /dev/null +++ b/worker/jobs/checkout_ci.run.ts @@ -0,0 +1,231 @@ +import { + type Command, + Either, + getRequiredEnvVars, + getStdout, + type IEither, + isObject, + type ITraceable, + LogLevel, + LogMetricTraceable, + type LogMetricTraceSupplier, + memoize, + Metric, + prependWith, + TraceUtil, + validateExecutionEntries, +} from "@emprespresso/pengueno"; +import { + type CheckoutCiJob, + type Job, + type Pipeline, + PipelineImpl, + type PipelineStage, +} from "@emprespresso/ci-model"; + +export interface CiWorkflow { + workflow: string; +} +export const isCiWorkflow = (t: unknown): t is CiWorkflow => + isObject(t) && "workflow" in t && typeof t.workflow === "string" && + !t.workflow.includes(".."); + +const run = Date.now().toString(); +const trace = `checkout_ci.${run}`; +const eitherJob = getRequiredEnvVars(["remote", "refname", "rev"]).mapRight(( + baseArgs, +) => ({ + type: "checkout_ci", + arguments: { + ...baseArgs, + run, + return: Deno.cwd(), + }, +} as unknown as CheckoutCiJob)); + +const ciRunMetric = Metric.fromName("checkout_ci.run"); +// TODO: FIGURE OUT HOW TO SETUP CLEANUP JOB! Maybe a .onfinally()? +await LogMetricTraceable.from(eitherJob) + .bimap(TraceUtil.withTrace(trace)) + .bimap(TraceUtil.withMetricTrace(ciRunMetric)) + .peek((tEitherJob) => + tEitherJob.trace.trace( + `hewwo~ starting checkout job for ${tEitherJob.get()}`, + ) + ) + .map((tEitherJob) => + tEitherJob.get().flatMapAsync((job) => { + const wd = getWorkingDirectoryForCheckoutJob(job); + return Either.fromFailableAsync<Error, CheckoutCiJob>( + Deno.mkdir(wd).then(() => Deno.chdir(wd)).then(() => job), + ); + }) + ) + .map((tEitherJob) => + tEitherJob.get().then((eitherJob) => + eitherJob.flatMapAsync((job) => + getStdout(tEitherJob.move("fetch_code"), { + env: { + remote: job.arguments.remote, + checkout: job.arguments.rev, + path: getSrcDirectoryForCheckoutJob(job), + }, + }).then((e) => e.moveRight(job)) + ) + ) + ) + .map((tEitherJob) => + tEitherJob.get().then((eitherJob) => + eitherJob.flatMapAsync<Command>((job) => + Either.fromFailableAsync<Error, string>( + Deno.readTextFile( + `${getSrcDirectoryForCheckoutJob(job)}/.ci/ci.json`, + ), + ).then((eitherWorkflowJson) => + eitherWorkflowJson.flatMap( + (json) => Either.fromFailable<Error, unknown>(JSON.parse(json)), + ).flatMap((eitherWorkflowParse) => { + if (isCiWorkflow(eitherWorkflowParse)) { + return Either.right( + getPipelineGenerationCommand(job, eitherWorkflowParse.workflow), + ); + } + return Either.left( + new Error( + "couldn't find any valid ci configuration (。•́︿•̀。), that's okay~", + ), + ); + }) + ) + ) + ) + ) + .map((tEitherPipelineGenerationCommand) => + tEitherPipelineGenerationCommand.get().then(( + eitherPipelineGenerationCommand, + ) => + eitherPipelineGenerationCommand.flatMapAsync((command) => + tEitherPipelineGenerationCommand.move(command).map(getStdout).get() + ) + ) + ) + .map( + TraceUtil.promiseify((tEitherPipelineString) => + tEitherPipelineString.get().flatMap(PipelineImpl.from) + ), + ) + .peek( + TraceUtil.promiseify((tEitherPipeline) => + tEitherPipeline.get().mapRight((val) => + tEitherPipeline.trace.trace( + "built the pipeline~ (◕ᴗ◕✿) let's make something amazing! " + + val.serialize(), + ) + ) + ), + ) + .map( + TraceUtil.promiseify((tEitherPipeline) => + tEitherPipeline.get() + .mapRight((pipeline) => tEitherPipeline.move(pipeline)) + .mapRight(executePipeline) + ), + ) + .get(); + +const jobTypeMetric = memoize((type: string) => + Metric.fromName(`checkout_ci.run.${type}`) +); +const executeJob = (tJob: ITraceable<Job, LogMetricTraceSupplier>) => { + const jobType = tJob.get().type; + const metric = jobTypeMetric(jobType); + return tJob.bimap(TraceUtil.withMetricTrace(metric)) + .peek((tJob) => + tJob.trace.trace( + `let's do this little job ok!! ${tJob.get()}`, + ) + ) + .map((tJob) => + validateExecutionEntries(tJob.get().arguments) + .mapLeft((badEntries) => { + tJob.trace.addTrace(LogLevel.ERROR).trace( + badEntries.toString(), + ); + return new Error("invalid job arguments"); + }) + .flatMapAsync((args) => + getStdout(tJob.move(tJob.get().type), { env: args }) + ) + ) + .peek( + TraceUtil.promiseify((q) => + q.trace.trace( + q.get().fold((err, _val) => + err + ? jobTypeMetric(tJob.get().type).failure + : jobTypeMetric(tJob.get().type).success + ), + ) + ), + ) + .get(); +}; + +const pipelinesMetric = Metric.fromName("checkout_ci.pipelines"); +const executePipeline = ( + tPipeline: ITraceable<Pipeline, LogMetricTraceSupplier>, +): Promise<IEither<Error, Array<PipelineStage>>> => + tPipeline.bimap(TraceUtil.withFunctionTrace(executePipeline)) + .bimap(TraceUtil.withMetricTrace(pipelinesMetric)) + .map((pipeline) => pipeline.get().serialJobs) + .map(async (tJobs) => { + for (const stage of tJobs.get()) { + tJobs.trace.trace( + `executing stage. do your best little stage :> ${stage}`, + ); + const results = await Promise.all( + stage.parallelJobs.map((job) => + tJobs.move(job).map(executeJob).get() + ), + ); + const failures = results.filter((e) => e.fold((_err, val) => !!val)); + if (failures.length > 0) { + tJobs.trace.trace(pipelinesMetric.failure); + return Either.left<Error, Array<PipelineStage>>( + new Error(failures.join(",")), + ); + } + } + tJobs.trace.trace(pipelinesMetric.success); + return Either.right<Error, Array<PipelineStage>>(tJobs.get()); + }) + .get(); + +const getWorkingDirectoryForCheckoutJob = (job: CheckoutCiJob) => + `${job.arguments.returnPath}/${job.arguments.run}`; + +const getSrcDirectoryForCheckoutJob = (job: CheckoutCiJob) => + `${job.arguments.returnPath}/${job.arguments.run}`; + +const getPipelineGenerationCommand = ( + job: CheckoutCiJob, + pipelineGeneratorPath: string, + runFlags = + "--rm --network none --cap-drop ALL --security-opt no-new-privileges".split( + " ", + ), +) => [ + "docker", + "run", + ...runFlags, + ...prependWith( + Object.entries(job.arguments).map(([key, val]) => `"${key}"="${val}"`), + "-e", + ), + "-v", + `${ + getSrcDirectoryForCheckoutJob(job) + }/${pipelineGeneratorPath}:/pipeline_generator`, + "oci.liz.coffee/img/liz-ci:release", + "/pipeline_generator", +]; diff --git a/worker/scripts/ansible_playbook b/worker/scripts/ansible_playbook index d24cbb6..096bb7b 100755 --- a/worker/scripts/ansible_playbook +++ b/worker/scripts/ansible_playbook @@ -1,14 +1,8 @@ #!/usr/bin/env -S deno run --allow-env --allow-net --allow-run --allow-read --allow-write -import { - BitwardenSession, - getRequiredEnv, - getStdout, - loggerWithPrefix, - prependWith, - type SecureNote, -} from "@liz-ci/utils"; -import type { AnsiblePlaybookJobProps } from "@liz-ci/model"; +import { getRequiredEnv, getStdout, prependWith } from "@emprespresso/pengueno"; +import { BitwardenSession, type SecureNote } from "@emprespresso/ci-utils"; +import type { AnsiblePlaybookJobProps } from "@emprespresso/ci-model"; const args: AnsiblePlaybookJobProps = { path: getRequiredEnv("path"), diff --git a/worker/scripts/run_pipeline b/worker/scripts/run_pipeline deleted file mode 100755 index abb13b3..0000000 --- a/worker/scripts/run_pipeline +++ /dev/null @@ -1,51 +0,0 @@ -#!/usr/bin/env -S deno run --allow-env --allow-net --allow-run --allow-read --allow-write - -import { PipelineImpl } from "@liz-ci/model"; -import { - getRequiredEnv, - getStdout, - invalidExecutionEntriesOf, - loggerWithPrefix, -} from "@liz-ci/utils"; - -const pipelinePath = getRequiredEnv("pipeline"); -const logger = loggerWithPrefix(() => - `[${new Date().toISOString()}] [run_pipeline.${pipelinePath}]` -); - -const run = async () => { - logger.log("starting pipeline execution~ time to work hard!"); - - const stages = await (Deno.readTextFile(pipelinePath)) - .then(PipelineImpl.from) - .then((pipeline) => pipeline.getStages()); - - for (const stage of stages) { - logger.log("executing stage. do your best little stage :>", stage); - - await Promise.all( - stage.parallelJobs.map(async (job, jobIdx) => { - logger.log(`let's do this little job ok!! ${jobIdx}`, job); - const invalidArgs = invalidExecutionEntriesOf(job.arguments); - if (invalidArgs.length) { - logger.error(`oh nooes`, invalidArgs); - throw new Error("invalid job arguments"); - } - - const result = await getStdout(job.type, { env: job.arguments }); - logger.log(jobIdx, "brought something to you! look :D", { result }); - }), - ); - } - - logger.log("all done! everything worked! yay~ (⑅˘꒳˘)"); -}; - -if (import.meta.main) { - try { - await run(); - } catch (e) { - logger.error("womp womp D:", e); - throw e; - } -} |