summaryrefslogtreecommitdiff
path: root/hooks
diff options
context:
space:
mode:
Diffstat (limited to 'hooks')
-rw-r--r--hooks/deno.json2
-rw-r--r--hooks/job/queue.ts0
-rw-r--r--hooks/main.ts11
-rw-r--r--hooks/mod.ts4
-rw-r--r--hooks/queuer.ts44
-rwxr-xr-xhooks/server.ts213
-rw-r--r--hooks/server/ci.ts55
-rw-r--r--hooks/server/health.ts16
-rw-r--r--hooks/server/job/activity.ts100
-rw-r--r--hooks/server/job/mod.ts2
-rw-r--r--hooks/server/job/queuer.ts78
-rw-r--r--hooks/server/mod.ts40
12 files changed, 261 insertions, 304 deletions
diff --git a/hooks/deno.json b/hooks/deno.json
index c4e8fca..cdaf63f 100644
--- a/hooks/deno.json
+++ b/hooks/deno.json
@@ -1,4 +1,4 @@
{
- "name": "@liz-ci/hooks",
+ "name": "@emprespresso/ci-hooks",
"exports": "./mod.ts"
}
diff --git a/hooks/job/queue.ts b/hooks/job/queue.ts
deleted file mode 100644
index e69de29..0000000
--- a/hooks/job/queue.ts
+++ /dev/null
diff --git a/hooks/main.ts b/hooks/main.ts
index 1348e57..d8e4f7f 100644
--- a/hooks/main.ts
+++ b/hooks/main.ts
@@ -1 +1,12 @@
#!/usr/bin/env -S deno run --allow-env --allow-net --allow-run
+
+import { LizCIServer } from "./server/mod.ts";
+
+const server = new LizCIServer();
+
+const SERVER_CONFIG = {
+ host: "0.0.0.0",
+ port: 9000,
+};
+
+Deno.serve(SERVER_CONFIG, (request: Request) => server.serve(request));
diff --git a/hooks/mod.ts b/hooks/mod.ts
index 738003c..cc15112 100644
--- a/hooks/mod.ts
+++ b/hooks/mod.ts
@@ -1,2 +1,2 @@
-export * from "./queuer.ts";
-export * from "./server.ts";
+export * from "./server/mod.ts";
+export * from "./main.ts";
diff --git a/hooks/queuer.ts b/hooks/queuer.ts
deleted file mode 100644
index d2987ca..0000000
--- a/hooks/queuer.ts
+++ /dev/null
@@ -1,44 +0,0 @@
-import { getStdout, type IEither, type Traceable } from "@liz-ci/utils";
-import type { Job } from "@liz-ci/model";
-
-type QueuePosition = string;
-export class QueueError extends Error {}
-export interface IJobQueuer<TJob> {
- queue: (
- job: TJob,
- ) => Promise<IEither<QueueError, QueuePosition>>;
-}
-
-export class LaminarJobQueuer implements IJobQueuer<Traceable<Job>> {
- constructor(
- private readonly queuePositionPrefix: string,
- ) {}
-
- public async queue(j: Traceable<Job>) {
- const { item: job, logger: _logger } = j;
- const logger = _logger.addTracer(() => `[LaminarJobQueuer.queue.${job}]`);
- const laminarCommand = [
- "laminarc",
- "queue",
- job.type,
- ...Object.entries(job.arguments).map(([key, val]) => `"${key}"="${val}"`),
- ];
-
- logger.log(
- `im so excited to see how this queue job will end!! (>ᴗ<)`,
- laminarCommand,
- );
-
- return (await getStdout(j.map(() => laminarCommand))).mapRight(
- (stdout) => {
- logger.log(stdout);
-
- const [jobName, jobId] = stdout.split(":");
- const jobUrl = `${this.queuePositionPrefix}/jobs/${jobName}/${jobId}`;
-
- logger.log(`all queued up and weady to go~ (˘ω˘) => ${jobUrl}\n`);
- return jobUrl;
- },
- );
- }
-}
diff --git a/hooks/server.ts b/hooks/server.ts
deleted file mode 100755
index 9a3f716..0000000
--- a/hooks/server.ts
+++ /dev/null
@@ -1,213 +0,0 @@
-import {
- Either,
- getRequiredEnv,
- getStdout,
- IEither,
- isObject,
- Traceable,
- validateExecutionEntries,
-} from "@liz-ci/utils";
-import { IJobQueuer } from "./mod.ts";
-import type { Job } from "@liz-ci/model";
-
-const SERVER_CONFIG = {
- host: "0.0.0.0",
- port: 9000,
-};
-interface IHealthCheckActivity<R> {
- healthCheck(req: R): Promise<Response>;
-}
-
-interface IJobHookActivity<R> {
- processHook(req: R): Promise<Response>;
-}
-type GetJobRequest = { jobType: string; args: unknown };
-class JobHookActivityImpl implements IJobHookActivity<Traceable<Request>> {
- constructor(private readonly queuer: IJobQueuer<Traceable<Job>>) {}
-
- private getJob<JsonT>(u: Traceable<JsonT>): Either<string, Job> {
- const { logger: _logger, item } = u;
- const logger = _logger.addTracer(() => "[getJob]");
- const couldBeJsonJob = isObject(item) && "arguments" in item &&
- "type" in item && item;
- const couldBeArguments = couldBeJsonJob && isObject(item.arguments) &&
- item.arguments;
- if (!couldBeJsonJob) {
- const err = "seems like a pwetty mawfomed job \\(-.-)/";
- logger.warn(err);
- return Either.left(err);
- }
-
- return validateExecutionEntries({
- type: item.type,
- ...couldBeArguments,
- }).mapBoth((err) => {
- const e = "your reqwest seems invawid (´。﹏。`) can you fix? uwu\n" +
- err.toString();
- logger.warn(e);
- return e;
- }, (_ok) => <Job> item);
- }
-
- public async processHook(r: Traceable<Request>) {
- return await r.bimap(Traceable.withClassTrace(this)).map(aPost)
- .map(aJson(this.getJob));
- }
- // flatMapAsync(aJsonPost(this.getJob))
- // .map(TraceableImpl.promiseify((g) => {
- // if (jobRequest) {
- // return g.map(() => jobRequest)
- // .map(this.getJob)
- // .map(
- // ({ item: { ok: jobRequest, err } }) => {
- // if (err) return { err: new Response(err, { status: 400 }) };
- // return { ok: jobRequest };
- // },
- // );
- // }
- // return g.map(() => ({ ok: undefined, err }));
- // }))
- // .map(TraceableImpl.promiseify(({ item: t }) => {
- // const { item: { ok: job, err } } = t;
- // if (err) return t.map(() => Promise.resolve(err));
- //
- // return t.map(() => job!)
- // .map(this.queuer.queue)
- // .map(TraceableImpl.promiseify(({ item, logger }) => {
- // if (item.ok) {
- // return new Response(item.ok, { status: 200 });
- // }
- // logger.error(item.err);
- // return new Response("i messed up D:\n", { status: 500 });
- // }));
- // }));
- // }
-}
-
-class LizCIServerImpl implements ILizCIServer {
- constructor(
- private readonly healthCheckActivity: IHealthCheckActivity,
- private readonly jobHookActivity: IJobHookActivity,
- ) {}
-
- private route(
- req: Traceable<Request & { pathname: string }>,
- ): Traceable<Promise<Response>> {
- return req.flatMap((req) => {
- const { logger, item: { method, pathname } } = req;
- if (pathname === "/health") {
- return this.healthCheckActivity.healthCheck(req);
- }
- return this.jobHookActivity.processHook(req);
- });
- }
-
- public async serve(req: Request): Promise<Response> {
- const traceId = crypto.randomUUID();
- const { pathname } = new URL(req.url);
- const traceSupplier = () => `[${traceId} <- ${req.method}'d @ ${pathname}]`;
- return TraceableImpl.from(req)
- .bimap(({ item: req }) => [{ req, pathname }, traceSupplier])
- .flatMap(this.route)
- .map(({ item, logger }) =>
- item.catch((e) => {
- const errorMessage = `oh noes! something went wrong (ಥ_ಥ) so sowwy!`;
- logger.error(errorMessage, e);
- return new Response(`${errorMessage}\n`, { status: 500 });
- })
- )
- .item;
- }
-}
-
-class JobQueue {
- private readonly logger: PrefixLogger;
- private readonly url: URL;
- private readonly pathname: string;
-
- constructor(private readonly request: Request, private readonly) {
- this.url = new URL(request.url);
- this.pathname = this.url.pathname;
- this.logger = this.createLogger();
- }
-
- /**
- * Creates a logger with request-specific context
- */
-
- /**
- * Performs health checks on dependent services
- */
- private async performHealthCheck(): Promise<void> {
- }
-
- /**
- * Handles health check requests
- */
- private async handleHealthCheck(): Promise<Response> {
- try {
- await this.performHealthCheck();
- } catch (error) {
- }
- }
-
- /**
- * Queues a job in the laminar system
- */
- private async queueJob(jobName: string, args: JobRequest): Promise<Response> {
- }
-
- /**
- * Validates job request parameters
- */
- private validateJobRequest(
- jobName: string,
- args: unknown,
- ): { valid: boolean; response?: Response } {
- }
-
- /**
- * Main method to handle the request
- */
- public async handle(): Promise<Response> {
- this.logger.log("go! :DDD");
-
- // Handle health check requests
- if (this.pathname === "/health") {
- return this.handleHealthCheck();
- }
-
- // Validate HTTP method
- if (this.request.method !== "POST") {
- }
-
- // Extract job name from path
-
- if (!validation.valid) {
- return validation.response!;
- }
-
- // Queue the job
- return this.queueJob(jobName, requestBody as JobRequest);
- }
-
- /**
- * Handles the entire request lifecycle, including error handling
- */
- public async processRequest(): Promise<Response> {
- try {
- return await this.handle();
- } catch (error) {
- } finally {
- this.logger.log("allll done!");
- }
- }
-}
-
-/**
- * Entry point - starts the server
- */
-Deno.serve(SERVER_CONFIG, async (request: Request) => {
- const handler = new RequestHandler(request);
- return handler.processRequest();
-});
diff --git a/hooks/server/ci.ts b/hooks/server/ci.ts
new file mode 100644
index 0000000..cdb8372
--- /dev/null
+++ b/hooks/server/ci.ts
@@ -0,0 +1,55 @@
+import {
+ FourOhFourActivityImpl,
+ getRequiredEnv,
+ HealthCheckActivityImpl,
+ type HealthChecker,
+ type IFourOhFourActivity,
+ type IHealthCheckActivity,
+ type ITraceable,
+ PenguenoRequest,
+ type ServerTrace,
+ TraceUtil,
+} from "@emprespresso/pengueno";
+import type { Job } from "@emprespresso/ci-model";
+import {
+ healthCheck as _healthCheck,
+ type IJobHookActivity,
+ type IJobQueuer,
+ JobHookActivityImpl,
+ LaminarJobQueuer,
+} from "@emprespresso/ci-hooks";
+
+export class LizCIServer {
+ constructor(
+ healthCheck: HealthChecker = _healthCheck,
+ jobQueuer: IJobQueuer<ITraceable<Job, ServerTrace>> = new LaminarJobQueuer(
+ getRequiredEnv("LAMINAR_URL").fold((err, val) =>
+ err ? "https://ci.liz.coffee" : val
+ ),
+ ),
+ private readonly healthCheckActivity: IHealthCheckActivity =
+ new HealthCheckActivityImpl(healthCheck),
+ private readonly jobHookActivity: IJobHookActivity =
+ new JobHookActivityImpl(jobQueuer),
+ private readonly fourOhFourActivity: IFourOhFourActivity =
+ new FourOhFourActivityImpl(),
+ ) {}
+
+ private route(req: ITraceable<PenguenoRequest, ServerTrace>) {
+ const url = new URL(req.get().url);
+ if (url.pathname === "/health") {
+ return this.healthCheckActivity.checkHealth(req);
+ }
+ if (url.pathname === "/job") {
+ return this.jobHookActivity.processHook(req);
+ }
+ return this.fourOhFourActivity.fourOhFour(req);
+ }
+
+ public serve(req: Request): Promise<Response> {
+ return PenguenoRequest.from(req)
+ .bimap(TraceUtil.withClassTrace(this))
+ .map(this.route)
+ .get();
+ }
+}
diff --git a/hooks/server/health.ts b/hooks/server/health.ts
index 41dfcb4..2f67aa4 100644
--- a/hooks/server/health.ts
+++ b/hooks/server/health.ts
@@ -1,23 +1,25 @@
import {
getRequiredEnv,
getStdout,
+ type HealthChecker,
type HealthCheckInput,
HealthCheckOutput,
type IEither,
type ITraceable,
+ type ServerTrace,
TraceUtil,
} from "@emprespresso/pengueno";
-export const healthCheck = <Trace>(
- input: ITraceable<HealthCheckInput, Trace>,
+export const healthCheck: HealthChecker = (
+ input: ITraceable<HealthCheckInput, ServerTrace>,
): Promise<IEither<Error, HealthCheckOutput>> =>
input.bimap(TraceUtil.withFunctionTrace(healthCheck))
.move(getRequiredEnv("LAMINAR_HOST"))
- // we need to test LAMINAR_HOST is propagated to getStdout for other procedures
- .map(({ item }) => item.moveRight(["laminarc", "show-jobs"]))
+ // ensure LAMINAR_HOST is propagated to getStdout for other procedures
+ .map((e) => e.get().moveRight(["laminarc", "show-jobs"]))
.map((i) =>
- i.item.mapRight(i.move.apply)
+ i.get().mapRight(i.move.apply)
.flatMapAsync(getStdout.apply)
- .then((gotJobs) => gotJobs.moveRight(HealthCheckOutput.YAASQUEEN))
+ .then((gotJobs) => gotJobs.moveRight(HealthCheckOutput.YAASSSLAYQUEEN))
)
- .item;
+ .get();
diff --git a/hooks/server/job/activity.ts b/hooks/server/job/activity.ts
new file mode 100644
index 0000000..14ea459
--- /dev/null
+++ b/hooks/server/job/activity.ts
@@ -0,0 +1,100 @@
+import {
+ Either,
+ ErrorSource,
+ type IActivity,
+ type IEither,
+ type ITraceable,
+ jsonModel,
+ JsonResponse,
+ LogLevel,
+ Metric,
+ PenguenoError,
+ type PenguenoRequest,
+ type ServerTrace,
+ TraceUtil,
+ validateExecutionEntries,
+} from "@emprespresso/pengueno";
+import { isJob, type Job } from "@emprespresso/ci-model";
+import type { IJobQueuer } from "@emprespresso/ci-hooks";
+
+const wellFormedJobMetric = Metric.fromName("Job.WellFormed");
+
+const jobJsonTransformer = (
+ j: ITraceable<unknown, ServerTrace>,
+): IEither<PenguenoError, Job> =>
+ j.bimap(TraceUtil.withMetricTrace(wellFormedJobMetric))
+ .map((tJson) => {
+ if (!isJob(tJson) || !validateExecutionEntries(tJson)) {
+ const err = "seems like a pwetty mawfomed job \\(-.-)/";
+ tJson.trace.addTrace(LogLevel.WARN).trace(err);
+ return Either.left<PenguenoError, Job>(new PenguenoError(err, 400));
+ }
+ return Either.right<PenguenoError, Job>(tJson);
+ })
+ .peek((tJob) =>
+ tJob.trace.trace(
+ tJob.get().fold((err) =>
+ err ? wellFormedJobMetric.failure : wellFormedJobMetric.success
+ ),
+ )
+ )
+ .get();
+
+export interface IJobHookActivity {
+ processHook: IActivity;
+}
+
+const jobHookRequestMetric = Metric.fromName("JobHook.process");
+export class JobHookActivityImpl implements IJobHookActivity {
+ constructor(
+ private readonly queuer: IJobQueuer<ITraceable<Job, ServerTrace>>,
+ ) {}
+
+ private trace(r: ITraceable<PenguenoRequest, ServerTrace>) {
+ return r.bimap(TraceUtil.withClassTrace(this))
+ .bimap(
+ TraceUtil.withMetricTrace(jobHookRequestMetric),
+ );
+ }
+
+ public processHook(r: ITraceable<PenguenoRequest, ServerTrace>) {
+ return this.trace(r).map(jsonModel(jobJsonTransformer))
+ .map(async (tEitherJobJson) => {
+ const eitherJob = await tEitherJobJson.get();
+ return eitherJob.flatMapAsync(async (job) => {
+ const eitherQueued = await tEitherJobJson.move(job)
+ .map(this.queuer.queue)
+ .get();
+ return eitherQueued.mapLeft((e) => new PenguenoError(e.message, 500));
+ });
+ })
+ .peek(
+ TraceUtil.promiseify((tJob) =>
+ tJob.get().fold(
+ (err: PenguenoError | undefined, _val: string | undefined) => {
+ if (!err) {
+ tJob.trace.trace(jobHookRequestMetric.success);
+ tJob.trace.trace(`all queued up and weady to go :D !! ${_val}`);
+ return;
+ }
+ tJob.trace.trace(
+ err.source === ErrorSource.SYSTEM
+ ? jobHookRequestMetric.failure
+ : jobHookRequestMetric.warn,
+ );
+ tJob.trace.addTrace(err.source).trace(`${err}`);
+ },
+ )
+ ),
+ )
+ .map(
+ TraceUtil.promiseify((tEitherQueuedJob) =>
+ new JsonResponse(r, tEitherQueuedJob.get(), {
+ status: tEitherQueuedJob.get()
+ .fold(({ status }, _val) => _val ? 200 : status),
+ })
+ ),
+ )
+ .get();
+ }
+}
diff --git a/hooks/server/job/mod.ts b/hooks/server/job/mod.ts
new file mode 100644
index 0000000..6b4ae85
--- /dev/null
+++ b/hooks/server/job/mod.ts
@@ -0,0 +1,2 @@
+export * from "./activity.ts";
+export * from "./queuer.ts";
diff --git a/hooks/server/job/queuer.ts b/hooks/server/job/queuer.ts
new file mode 100644
index 0000000..6094183
--- /dev/null
+++ b/hooks/server/job/queuer.ts
@@ -0,0 +1,78 @@
+import {
+ getStdout,
+ type IEither,
+ type ITraceable,
+ LogLevel,
+ type Mapper,
+ memoize,
+ Metric,
+ type ServerTrace,
+ TraceUtil,
+} from "@emprespresso/pengueno";
+import type { Job } from "@emprespresso/ci-model";
+
+type QueuePosition = string;
+export class QueueError extends Error {}
+export interface IJobQueuer<TJob> {
+ queue: Mapper<TJob, Promise<IEither<QueueError, QueuePosition>>>;
+}
+
+export class LaminarJobQueuer
+ implements IJobQueuer<ITraceable<Job, ServerTrace>> {
+ constructor(
+ private readonly queuePositionPrefix: string,
+ ) {}
+
+ private static GetJobTypeTrace = (jobType: string) =>
+ `LaminarJobQueue.Queue.${jobType}`;
+ private static JobTypeMetrics = memoize((jobType: string) =>
+ Metric.fromName(LaminarJobQueuer.GetJobTypeTrace(jobType))
+ );
+
+ public queue(j: ITraceable<Job, ServerTrace>) {
+ const { type: jobType } = j.get();
+ const trace = LaminarJobQueuer.GetJobTypeTrace(jobType);
+ const metric = LaminarJobQueuer.JobTypeMetrics(trace);
+
+ return j
+ .bimap(TraceUtil.withTrace(trace))
+ .bimap(TraceUtil.withMetricTrace(metric))
+ .map((j) => {
+ const { type: jobType, arguments: args } = j.get();
+ const laminarCommand = [
+ "laminarc",
+ "queue",
+ jobType,
+ ...Object.entries(args).map(([key, val]) => `"${key}"="${val}"`),
+ ];
+ return laminarCommand;
+ })
+ .peek((c) =>
+ c.trace.trace(
+ `im so excited to see how this queue job will end!! (>ᴗ<): ${c.get().toString()}`,
+ )
+ )
+ .map(getStdout)
+ .peek(
+ TraceUtil.promiseify((q) =>
+ q.trace.trace(
+ q.get().fold((err, _val) => err ? metric.failure : metric.success),
+ )
+ ),
+ )
+ .map(TraceUtil.promiseify((q) =>
+ q.get().mapRight((stdout) => {
+ q.trace.addTrace(LogLevel.DEBUG).trace(`stdout ${stdout}`);
+ const [jobName, jobId] = stdout.split(":");
+ const jobUrl = `${this.queuePositionPrefix}/jobs/${jobName}/${jobId}`;
+
+ q.trace.trace(`all queued up and weady to go~ (˘ω˘) => ${jobUrl}`);
+ return jobUrl;
+ }).mapLeft((err) => {
+ q.trace.addTrace(LogLevel.ERROR).trace(err.toString());
+ return err;
+ })
+ ))
+ .get();
+ }
+}
diff --git a/hooks/server/mod.ts b/hooks/server/mod.ts
index b635b05..0a520f9 100644
--- a/hooks/server/mod.ts
+++ b/hooks/server/mod.ts
@@ -1,37 +1,3 @@
-import {
- getRequiredEnv,
- getStdout,
- type HealthCheckInput,
- HealthCheckOutput,
- type IEither,
- type ITraceable,
- LogTraceable,
- TraceUtil,
-} from "@emprespresso/pengueno";
-
-export class LizCIServer {
- private constructor(
- private readonly healthCheckActivity = HealthCheckActivity(healthCheck),
- private readonly jobHookActivity = JobHookActivity(jobQueuer),
- private readonly fourOhFourActivity = FourOhFourActivity(),
- ) {}
-
- private async route(req: LogTraceable<Request>) {
- return req.flatMap((req) => {
- const { item: request } = req;
- const url = new URL(request.url);
- if (url.pathname === "/health") {
- return this.healthCheckActivity.healthCheck(req);
- }
- if (url.pathname === "/job") {
- return this.jobHookActivity.processHook(req);
- }
- });
- }
-
- public async serve(req: Request): Promise<Response> {
- return LogTraceable(req).bimap(TraceUtil.withClassTrace(this)).map(
- this.route,
- );
- }
-}
+export * from "./ci.ts";
+export * from "./health.ts";
+export * from "./job/mod.ts";