summaryrefslogtreecommitdiff
path: root/server
diff options
context:
space:
mode:
Diffstat (limited to 'server')
-rw-r--r--server/Dockerfile2
-rw-r--r--server/ci.ts89
-rw-r--r--server/deno.json4
-rw-r--r--server/health.ts48
-rw-r--r--server/index.ts31
-rw-r--r--server/job.ts185
-rw-r--r--server/job/index.ts2
-rw-r--r--server/job/queue.ts74
-rw-r--r--server/job/run_activity.ts94
-rw-r--r--server/mod.ts22
-rw-r--r--server/package.json33
-rw-r--r--server/tsconfig.json15
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" }]
+}