summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorElizabeth Alexander Hunt <me@liz.coffee>2025-05-18 22:54:15 -0700
committerElizabeth Alexander Hunt <me@liz.coffee>2025-05-18 22:55:20 -0700
commitd54e91c6582ed160cf2f2fcf977e48b4439d133b (patch)
tree5669367c4fa49bc0373b0c581ea3027218fd5e32
parent9cf3fc0259730b7dcf47b3ab4a04369e39fb4614 (diff)
downloadci-theBigRefactor.tar.gz
ci-theBigRefactor.zip
-rw-r--r--deno.json2
-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
-rw-r--r--model/deno.json2
-rw-r--r--model/job.ts19
-rw-r--r--model/pipeline.ts37
-rw-r--r--u/fn/either.ts6
-rw-r--r--u/fn/mod.ts1
-rw-r--r--u/leftpadesque/memoize.ts14
-rw-r--r--u/leftpadesque/mod.ts1
-rw-r--r--u/process/env.ts15
-rw-r--r--u/process/run.ts33
-rw-r--r--u/server/activity/fourohfour.ts27
-rw-r--r--u/server/activity/health.ts77
-rw-r--r--u/server/activity/mod.ts20
-rw-r--r--u/server/filter/json.ts43
-rw-r--r--u/server/filter/method.ts10
-rw-r--r--u/server/filter/mod.ts28
-rw-r--r--u/trace/metrics.ts24
-rw-r--r--u/trace/trace.ts8
-rw-r--r--u/trace/util.ts14
-rw-r--r--utils/deno.json2
-rw-r--r--worker/deno.json2
-rw-r--r--worker/jobs/checkout_ci.run.ts231
-rwxr-xr-xworker/scripts/ansible_playbook12
-rwxr-xr-xworker/scripts/run_pipeline51
36 files changed, 749 insertions, 495 deletions
diff --git a/deno.json b/deno.json
index 6b5c03d..a30d0ec 100644
--- a/deno.json
+++ b/deno.json
@@ -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;
- }
-}