diff options
Diffstat (limited to 'server')
-rw-r--r-- | server/Dockerfile | 2 | ||||
-rw-r--r-- | server/ci.ts | 89 | ||||
-rw-r--r-- | server/deno.json | 4 | ||||
-rw-r--r-- | server/health.ts | 48 | ||||
-rw-r--r-- | server/index.ts | 31 | ||||
-rw-r--r-- | server/job.ts | 185 | ||||
-rw-r--r-- | server/job/index.ts | 2 | ||||
-rw-r--r-- | server/job/queue.ts | 74 | ||||
-rw-r--r-- | server/job/run_activity.ts | 94 | ||||
-rw-r--r-- | server/mod.ts | 22 | ||||
-rw-r--r-- | server/package.json | 33 | ||||
-rw-r--r-- | server/tsconfig.json | 15 |
12 files changed, 310 insertions, 289 deletions
diff --git a/server/Dockerfile b/server/Dockerfile index b610907..1b9b0ed 100644 --- a/server/Dockerfile +++ b/server/Dockerfile @@ -1,5 +1,5 @@ # -- <ci_server> -- FROM oci.liz.coffee/emprespresso/ci_base:release AS server -CMD [ "/app/mod.ts --run-server --port=9000" ] +CMD [ "node", "/app/dist/index.js", "--run-server", "--port=9000" ] # -- </ci_server> -- diff --git a/server/ci.ts b/server/ci.ts index f8d4a17..f57c426 100644 --- a/server/ci.ts +++ b/server/ci.ts @@ -1,56 +1,47 @@ 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_server"; + 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 { type IJobHookActivity, type IJobQueuer, JobHookActivityImpl, LaminarJobQueuer } from './job'; +import { healthCheck as _healthCheck } from '.'; + +export const DEFAULT_CI_SERVER = 'https://ci.liz.coffee'; export class CiHookServer { - constructor( - healthCheck: HealthChecker = _healthCheck, - jobQueuer: IJobQueuer<ITraceable<Job, ServerTrace>> = new LaminarJobQueuer( - getRequiredEnv("LAMINAR_URL").fold(({ isLeft, value }) => - isLeft ? "https://ci.liz.coffee" : value, - ), - ), - private readonly healthCheckActivity: IHealthCheckActivity = new HealthCheckActivityImpl( - healthCheck, - ), - private readonly jobHookActivity: IJobHookActivity = new JobHookActivityImpl( - jobQueuer, - ), - private readonly fourOhFourActivity: IFourOhFourActivity = new FourOhFourActivityImpl(), - ) {} + constructor( + healthCheck: HealthChecker = _healthCheck, + jobQueuer: IJobQueuer<ITraceable<Job, ServerTrace>> = new LaminarJobQueuer( + getRequiredEnv('LAMINAR_URL').fold(({ isLeft, value }) => (isLeft ? DEFAULT_CI_SERVER : value)), + ), + 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); + 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); } - 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((req) => this.route(req)) - .get(); - } + public serve(req: Request): Promise<Response> { + return PenguenoRequest.from(req) + .bimap(TraceUtil.withClassTrace(this)) + .map((req) => this.route(req)) + .get(); + } } diff --git a/server/deno.json b/server/deno.json deleted file mode 100644 index c86c9a7..0000000 --- a/server/deno.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "name": "@emprespresso/ci_server", - "exports": "./mod.ts" -} diff --git a/server/health.ts b/server/health.ts index e69077b..8435865 100644 --- a/server/health.ts +++ b/server/health.ts @@ -1,32 +1,24 @@ import { - getRequiredEnv, - getStdout, - type HealthChecker, - type HealthCheckInput, - HealthCheckOutput, - type IEither, - type ITraceable, - type ServerTrace, - TraceUtil, -} from "@emprespresso/pengueno"; + getRequiredEnv, + getStdout, + type HealthChecker, + type HealthCheckInput, + HealthCheckOutput, + type IEither, + type ITraceable, + type ServerTrace, + TraceUtil, +} from '@emprespresso/pengueno'; export const healthCheck: HealthChecker = ( - input: ITraceable<HealthCheckInput, ServerTrace>, + input: ITraceable<HealthCheckInput, ServerTrace>, ): Promise<IEither<Error, HealthCheckOutput>> => - input - .bimap(TraceUtil.withFunctionTrace(healthCheck)) - .move(getRequiredEnv("LAMINAR_HOST")) - // ensure LAMINAR_HOST is propagated to getStdout for other procedures - .map((tEitherEnv) => - tEitherEnv - .get() - .flatMapAsync((_hasEnv) => - getStdout(tEitherEnv.move(["laminarc", "show-jobs"])), - ), - ) - .map( - TraceUtil.promiseify((stdout) => - stdout.get().moveRight(HealthCheckOutput.YAASSSLAYQUEEN), - ), - ) - .get(); + input + .bimap(TraceUtil.withFunctionTrace(healthCheck)) + .move(getRequiredEnv('LAMINAR_HOST')) + // ensure LAMINAR_HOST is propagated to getStdout for other procedures + .map((tEitherEnv) => + tEitherEnv.get().flatMapAsync((_hasEnv) => getStdout(tEitherEnv.move(['laminarc', 'show-jobs']))), + ) + .map(TraceUtil.promiseify((stdout) => stdout.get().moveRight(HealthCheckOutput.YAASSSLAYQUEEN))) + .get(); diff --git a/server/index.ts b/server/index.ts new file mode 100644 index 0000000..c33b43e --- /dev/null +++ b/server/index.ts @@ -0,0 +1,31 @@ +#!/usr/bin/env node + +export * from './job'; +export * from './ci'; +export * from './health'; + +import { CiHookServer } from '.'; +import { Either, type IEither } from '@emprespresso/pengueno'; +import { serve } from '@hono/node-server'; +import { Hono } from 'hono'; + +const server = new CiHookServer(); + +const neverEndingPromise = new Promise<IEither<Error, 0>>(() => {}); +export const runServer = (port: number, host: string): Promise<IEither<Error, 0>> => + Either.fromFailable<Error, void>(() => { + const app = new Hono(); + + app.all('*', async (c) => { + const response = await server.serve(c.req.raw); + return response; + }); + + serve({ + fetch: app.fetch, + port, + hostname: host, + }); + + console.log(`server running on http://${host}:${port} :D`); + }).flatMapAsync(() => neverEndingPromise); diff --git a/server/job.ts b/server/job.ts deleted file mode 100644 index 620a083..0000000 --- a/server/job.ts +++ /dev/null @@ -1,185 +0,0 @@ -import { - getStdout, - type Mapper, - memoize, - 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"; - -// -- <job.hook> -- -const wellFormedJobMetric = Metric.fromName("Job.WellFormed"); - -const jobJsonTransformer = ( - j: ITraceable<unknown, ServerTrace>, -): IEither<PenguenoError, Job> => - j - .bimap(TraceUtil.withMetricTrace(wellFormedJobMetric)) - .map((tJson): IEither<PenguenoError, Job> => { - const tJob = tJson.get(); - if (!isJob(tJob) || !validateExecutionEntries(tJob)) { - const err = "seems like a pwetty mawfomed job (-.-)"; - tJson.trace.addTrace(LogLevel.WARN).trace(err); - return Either.left(new PenguenoError(err, 400)); - } - return Either.right(tJob); - }) - .peek((tJob) => - tJob.trace.trace( - tJob - .get() - .fold(({ isLeft }) => - isLeft ? 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((job) => this.queuer.queue(job)) - .get(); - return eitherQueued.mapLeft((e) => new PenguenoError(e.message, 500)); - }); - }) - .peek( - TraceUtil.promiseify((tJob) => - tJob.get().fold(({ isRight, value }) => { - if (isRight) { - tJob.trace.trace(jobHookRequestMetric.success); - tJob.trace.trace(`all queued up and weady to go :D !! ${value}`); - return; - } - - tJob.trace.trace( - value.source === ErrorSource.SYSTEM - ? jobHookRequestMetric.failure - : jobHookRequestMetric.warn, - ); - tJob.trace.addTrace(value.source).trace(`${value}`); - }), - ), - ) - .map( - TraceUtil.promiseify( - (tEitherQueuedJob) => - new JsonResponse(r, tEitherQueuedJob.get(), { - status: tEitherQueuedJob - .get() - .fold(({ isRight, value }) => (isRight ? 200 : value.status)), - }), - ), - ) - .get(); - } -} - -// -- </job.hook> -- - -// -- <job.queuer> -- -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(jobType); - - 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(({ isLeft }) => (isLeft ? metric.failure : metric.success)), - ), - ), - ) - .map( - TraceUtil.promiseify((q) => - q.get().fold(({ isLeft, value }) => { - if (isLeft) { - q.trace.addTrace(LogLevel.ERROR).trace(value.toString()); - return Either.left<Error, string>(value); - } - q.trace.addTrace(LogLevel.DEBUG).trace(`stdout ${value}`); - const [jobName, jobId] = value.split(":"); - const jobUrl = `${this.queuePositionPrefix}/jobs/${jobName}/${jobId}`; - - q.trace.trace(`all queued up and weady to go~ (˘ω˘) => ${jobUrl}`); - return Either.right<Error, string>(jobUrl); - }), - ), - ) - .get(); - } -} -// -- </job.queuer> -- diff --git a/server/job/index.ts b/server/job/index.ts new file mode 100644 index 0000000..ecf0984 --- /dev/null +++ b/server/job/index.ts @@ -0,0 +1,2 @@ +export * from './queue'; +export * from './run_activity'; diff --git a/server/job/queue.ts b/server/job/queue.ts new file mode 100644 index 0000000..2392222 --- /dev/null +++ b/server/job/queue.ts @@ -0,0 +1,74 @@ +import { + getStdout, + type Mapper, + memoize, + Either, + type IEither, + type ITraceable, + LogLevel, + 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(jobType); + + 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(({ isLeft }) => (isLeft ? metric.failure : metric.success))), + ), + ) + .map( + TraceUtil.promiseify((q) => + q.get().fold(({ isLeft, value }) => { + if (isLeft) { + q.trace.addTrace(LogLevel.ERROR).trace(value.toString()); + return Either.left<Error, string>(value); + } + q.trace.addTrace(LogLevel.DEBUG).trace(`stdout ${value}`); + const [jobName, jobId] = value.split(':'); + const jobUrl = `${this.queuePositionPrefix}/jobs/${jobName}/${jobId}`; + + q.trace.trace(`all queued up and weady to go~ (˘ω˘) => ${jobUrl}`); + return Either.right<Error, string>(jobUrl); + }), + ), + ) + .get(); + } +} diff --git a/server/job/run_activity.ts b/server/job/run_activity.ts new file mode 100644 index 0000000..9f25cf8 --- /dev/null +++ b/server/job/run_activity.ts @@ -0,0 +1,94 @@ +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 { IJobQueuer } from './queue'; + +const wellFormedJobMetric = Metric.fromName('Job.WellFormed'); + +const jobJsonTransformer = (j: ITraceable<unknown, ServerTrace>): IEither<PenguenoError, Job> => + j + .bimap(TraceUtil.withMetricTrace(wellFormedJobMetric)) + .map((tJson): IEither<PenguenoError, Job> => { + const tJob = tJson.get(); + if (!isJob(tJob) || !validateExecutionEntries(tJob)) { + const err = 'seems like a pwetty mawfomed job (-.-)'; + tJson.trace.addTrace(LogLevel.WARN).trace(err); + return Either.left(new PenguenoError(err, 400)); + } + return Either.right(tJob); + }) + .peek((tJob) => + tJob.trace.trace( + tJob.get().fold(({ isLeft }) => (isLeft ? 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((job) => this.queuer.queue(job)) + .get(); + return eitherQueued.mapLeft((e) => new PenguenoError(e.message, 500)); + }); + }) + .peek( + TraceUtil.promiseify((tJob) => + tJob.get().fold(({ isRight, value }) => { + if (isRight) { + tJob.trace.trace(jobHookRequestMetric.success); + tJob.trace.trace(`all queued up and weady to go :D !! ${value}`); + return; + } + + tJob.trace.trace( + value.source === ErrorSource.SYSTEM + ? jobHookRequestMetric.failure + : jobHookRequestMetric.warn, + ); + tJob.trace.addTrace(value.source).trace(`${value}`); + }), + ), + ) + .map( + TraceUtil.promiseify( + (tEitherQueuedJob) => + new JsonResponse(r, tEitherQueuedJob.get(), { + status: tEitherQueuedJob.get().fold(({ isRight, value }) => (isRight ? 200 : value.status)), + }), + ), + ) + .get(); + } +} diff --git a/server/mod.ts b/server/mod.ts deleted file mode 100644 index 1d168d2..0000000 --- a/server/mod.ts +++ /dev/null @@ -1,22 +0,0 @@ -#!/usr/bin/env -S deno run --allow-env --allow-net --allow-run - -export * from "./ci.ts"; -export * from "./health.ts"; -export * from "./job.ts"; - -import { CiHookServer } from "./mod.ts"; -import { Either, type IEither } from "@emprespresso/pengueno"; -const server = new CiHookServer(); - -export const runServer = ( - port: number, - host: string, -): Promise<IEither<Error, 0>> => { - const serverConfig = { - host, - port, - }; - return Either.fromFailable<Error, Deno.HttpServer>(() => - Deno.serve(serverConfig, (req) => server.serve(req)), - ).flatMapAsync((server) => Either.fromFailableAsync(() => server.finished.then(() => 0))); -}; diff --git a/server/package.json b/server/package.json new file mode 100644 index 0000000..644f267 --- /dev/null +++ b/server/package.json @@ -0,0 +1,33 @@ +{ + "name": "@emprespresso/ci_server", + "version": "0.1.0", + "type": "module", + "main": "./dist/index.js", + "types": "./dist/index.d.ts", + "exports": { + ".": { + "types": "./dist/index.d.ts", + "import": "./dist/index.js" + } + }, + "scripts": { + "build": "tsc", + "dev": "tsc --watch", + "clean": "rm -rf dist", + "type-check": "tsc --noEmit", + "start": "node dist/index.js", + "format": "prettier --write .", + "format:check": "prettier --check ." + }, + "dependencies": { + "@emprespresso/pengueno": "*", + "@emprespresso/ci_model": "*", + "hono": "^4.8.0", + "@hono/node-server": "^1.14.0" + }, + "files": [ + "dist/**/*", + "package.json", + "README.md" + ] +} diff --git a/server/tsconfig.json b/server/tsconfig.json new file mode 100644 index 0000000..58e9147 --- /dev/null +++ b/server/tsconfig.json @@ -0,0 +1,15 @@ +{ + "extends": "../tsconfig.json", + "compilerOptions": { + "outDir": "./dist", + "rootDir": "./", + "composite": true, + "declaration": true, + "declarationMap": true, + "sourceMap": true, + "noEmit": false + }, + "include": ["**/*.ts"], + "exclude": ["node_modules", "dist", "**/*.test.ts", "**/*.spec.ts"], + "references": [{ "path": "../u" }, { "path": "../model" }] +} |