summaryrefslogtreecommitdiff
path: root/hooks/mod.ts
diff options
context:
space:
mode:
Diffstat (limited to 'hooks/mod.ts')
-rwxr-xr-xhooks/mod.ts321
1 files changed, 267 insertions, 54 deletions
diff --git a/hooks/mod.ts b/hooks/mod.ts
index f858ad0..63a05fc 100755
--- a/hooks/mod.ts
+++ b/hooks/mod.ts
@@ -3,76 +3,289 @@
import {
getRequiredEnv,
getStdout,
- loggerWithPrefix,
- validateIdentifier,
+ invalidExecutionEntriesOf,
+ type Traceable,
+ TraceableImpl,
} from "@liz-ci/utils";
+import type { Job } from "@liz-ci/model";
-const getRequestLogger = (req: Request) => {
- const url = new URL(req.url);
- const traceId = crypto.randomUUID();
- const getPrefix = () =>
- `[${
- new Date().toISOString()
- }] RequestTrace=[${traceId}] @ [${url.pathname}] -X [${req.method}] |`;
- return loggerWithPrefix(getPrefix);
+const SERVER_CONFIG = {
+ host: "0.0.0.0",
+ port: 9000,
};
-const addr = { port: 9000, hostname: "0.0.0.0" };
-Deno.serve(addr, async (req) => {
- const logger = getRequestLogger(req);
- logger.log("start");
+type QueuePosition = string;
+interface IJobQueuer {
+ queue: (
+ job: Traceable<Job>,
+ ) => Promise<{ ok?: QueuePosition; err?: unknown }>;
+}
- try {
- const { pathname } = new URL(req.url);
- if (pathname === "/health") {
+class LaminarJobQueuer implements IJobQueuer {
+ constructor(
+ private readonly queuePositionPrefix: string,
+ ) {}
+
+ public async queue({ item: job, logger }: Traceable<Job>) {
+ try {
+ const laminarCommand = [
+ "laminarc",
+ "queue",
+ job.type,
+ ...Object.entries(job.arguments).map(([key, val]) =>
+ `"${key}"="${val}"`
+ ),
+ ];
+
+ logger.log(
+ `im so excited to see how this queue job will end!! (>ᴗ<)`,
+ laminarCommand,
+ );
+
+ const output = await getStdout(laminarCommand);
+ logger.log(output);
+
+ const [jobName, jobId] = output.split(":");
+ const jobUrl = `${this.queuePositionPrefix}/jobs/${jobName}/${jobId}`;
+
+ logger.log(`all queued up and weady to go~ (˘ω˘) => ${jobUrl}\n`);
+ return { ok: jobUrl, err: undefined };
+ } catch (e) {
+ return { ok: undefined, err: e };
+ }
+ }
+}
+
+interface IHealthCheckActivity {
+ healthCheck(req: Traceable<Request>): Traceable<Promise<Response>>;
+}
+
+class HealthCheckActivity implements IHealthCheckActivity {
+ public healthCheck(
+ req: Traceable<Request>,
+ ) {
+ return req.map(async ({ logger }) => {
try {
getRequiredEnv("LAMINAR_HOST");
await getStdout(["laminarc", "show-jobs"]);
- return new Response("think im healthy!! lets get to work.\n", {
- status: 200,
- });
- } catch (e) {
- logger.error(e);
- return new Response("i need to eat more vegetables -.-\n", {
- status: 500,
- });
+ const msg = `think im healthy!! (✿˘◡˘) ready to do work~\n`;
+ logger.log(msg);
+ return new Response(
+ msg,
+ { status: 200 },
+ );
+ } catch (error) {
+ logger.error(error);
+ return new Response(
+ "oh no, i need to eat more vegetables (。•́︿•̀。)...\n",
+ { status: 500 },
+ );
}
+ });
+ }
+}
+
+interface IJobHookActivity {
+ processHook(req: Traceable<Request>): Traceable<Promise<Response>>;
+}
+type GetJobRequest = { jobType: string; args: unknown };
+class JobHookActivityImpl implements IJobHookActivity {
+ constructor(private readonly queuer: IJobQueuer) {}
+
+ private getJob(
+ { item: { args, jobType }, logger }: Traceable<GetJobRequest>,
+ ): { ok?: Job; err?: string } {
+ if (Array.isArray(args) || typeof args !== "object" || args === null) {
+ return { err: "your reqwest seems compwetewy mawfomed (-.-)/\n" };
}
- if (req.method !== "POST") {
- return new Response("invalid method", {
- status: 405,
- });
+ const invalidArgEntries = invalidExecutionEntriesOf({
+ ...args,
+ jobType,
+ });
+ if (invalidArgEntries.length > 0) {
+ const err = "your reqwest seems invawid (´。﹏。`) can you fix? uwu";
+ logger.error(err, jobType, args);
+ return { err };
}
- if (pathname === "/checkout_ci") {
- const { remote, rev, refname } = await req.json();
- if (![remote, rev, refname].every(validateIdentifier)) {
- logger.log("invalid reqwest");
- return new Response("invalid reqwest >:D\n", {
- status: 400,
- });
+ return {
+ ok: <Job> {
+ type: jobType,
+ arguments: args,
+ },
+ };
+ }
+
+ public processHook(r: Traceable<Request>) {
+ return r.map(async ({ item: request, logger }) => {
+ const { method } = request;
+ if (method !== "POST") {
+ const msg = "that's not how you pet me (⋟﹏⋞) try post instead~";
+ logger.log(msg);
+ const r405 = new Response(msg, { status: 405 });
+ return { err: r405 };
}
- const laminar = await getStdout([
- "laminarc",
- "queue",
- "checkout_ci",
- `remote="${remote}"`,
- `rev="${rev}"`,
- `refname="${refname}"`,
- ]);
- logger.log(`successful queue :D`, laminar);
- return new Response(laminar + "\n", {
- status: 200,
- });
+ const jobType = new URL(request.url).pathname.split("/")[1];
+ const args = await request.json();
+ return { ok: <GetJobRequest> { jobType, args } };
+ })
+ .map(TraceableImpl.promiseify((g) => {
+ const { item: { ok: jobRequest, err } } = 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();
}
- return new Response("idk what that is bro :((\n", { status: 404 });
- } catch (e) {
- logger.error("uncaught exception", e);
- return new Response("womp womp D:\n", { status: 500 });
- } finally {
- logger.log("finish");
+ // 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();
});