summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorElizabeth Alexander Hunt <me@liz.coffee>2025-05-12 23:05:27 -0700
committerElizabeth Alexander Hunt <me@liz.coffee>2025-05-12 23:33:52 -0700
commite49fda41176d025a671802be76c219d66167276f (patch)
tree6791f0ffe26b2405e13436a7e0c8d05d60e2b3bc
parentbbaea13ee7125a9d289a74f0c173e7e75177e53c (diff)
downloadci-e49fda41176d025a671802be76c219d66167276f.tar.gz
ci-e49fda41176d025a671802be76c219d66167276f.zip
snapshot
-rwxr-xr-xhooks/mod.ts107
-rw-r--r--hooks/queuer.ts47
-rw-r--r--utils/either.ts25
-rw-r--r--utils/mod.ts1
-rw-r--r--utils/run.ts28
-rw-r--r--utils/trace.ts47
6 files changed, 154 insertions, 101 deletions
diff --git a/hooks/mod.ts b/hooks/mod.ts
index 9fc4501..9df7d67 100755
--- a/hooks/mod.ts
+++ b/hooks/mod.ts
@@ -4,8 +4,11 @@ import {
getRequiredEnv,
getStdout,
invalidExecutionEntriesOf,
- type Traceable,
- TraceableImpl,
+ Traceable,
+ ITraceable,
+ ITraceableLogger,
+ IEither,
+ Either,
} from "@liz-ci/utils";
import type { Job } from "@liz-ci/model";
@@ -13,49 +16,6 @@ const SERVER_CONFIG = {
host: "0.0.0.0",
port: 9000,
};
-
-type QueuePosition = string;
-interface IJobQueuer {
- queue: (
- job: Traceable<Job>,
- ) => Promise<{ ok?: QueuePosition; err?: unknown }>;
-}
-
-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>>;
}
@@ -85,39 +45,36 @@ class HealthCheckActivity implements IHealthCheckActivity {
}
}
-const aPost = (r: Traceable<Request>): Traceable<{ ok?: Request, err?: Response }> =>
- r.map((req) => {
- const {item: request, logger} = req;
- const {method} = request;
- if (method !== "POST") {
- const msg = "that's not how you pet me (⋟﹏⋞) try post instead~";
- logger.warn(msg);
- const r405 = new Response(msg + "\n", {status: 405});
- return {err: r405};
- }
- return {ok: request};
- });
+const aPost = (req: Traceable<Request>): IEither<Response, Request> => {
+ const {item: request, logger} = req;
+ const {method} = request;
+ if (method !== "POST") {
+ const msg = "that's not how you pet me (⋟﹏⋞) try post instead~";
+ logger.warn(msg);
+ return { err: new Response(msg + "\n", {status: 405}) };
+ }
+ return {ok: request};
+};
-type JsonTransformer<JsonT, R> = (json: Traceable<JsonT>) => Traceable<{ ok?: R; err?: Response }>;
+type JsonTransformer<JsonT, R> = (json: Traceable<JsonT>) => Either<string, R>;
const aJson = <BodyT, JsonT = unknown>(jsonTransformer: JsonTransformer<JsonT, BodyT>) =>
(r: Traceable<Request>) => r
- .map(async ({item: request, logger}): Promise<{ ok?: JsonT, err?: Response }> => {
-
+ .map(async ({item: request, logger}): Promise<Either<string, JsonT>> => {
try {
return {ok: <JsonT>(await request.json())};
} catch (e) {
const err = "seems to be invalid JSON (>//<) can you fix?";
logger.warn(err);
- const r400 = new Response(msg + "\n", {status: 400});
- return {err: r400};
+ return { err };
}
})
- .flatMapAsync(TraceableImpl.promiseify((t): Traceable<{ ok?: BodyT, err?: Response }> => {
+ .flatMapAsync(TraceableImpl.promiseify((t): Traceable<Either<string, BodyT>> => {
const {item: {err, ok: json}} = t;
- if (err) return <Traceable<{ err: Response }>>t;
- return t.map(() => json!).flatMap(jsonTransformer);
+ if (err) return t.map(() => ({ err }));
+ return t.map(() => json!).map(jsonTransformer);
}))
+
interface IJobHookActivity {
processHook(req: Traceable<Request>): Traceable<Promise<Response>>;
}
@@ -126,31 +83,31 @@ class JobHookActivityImpl implements IJobHookActivity {
constructor(private readonly queuer: IJobQueuer) {}
private getJob(
- j: Traceable<unknown>,
- ): Traceable<{ ok?: Job; err?: Response }> {
- return j.map(({ logger, item }) => {
+ { logger, item }: Traceable<unknown>,
+ ): Either<string, Job> {
const isObject = (o: unknown): o is Record<string, unknown> =>
typeof o === "object" && !Array.isArray(o) && !!o;
if (!isObject(item) || !isObject(item.arguments)|| typeof item.type !== "string") {
- const err = "your reqwest seems compwetewy mawfomed \\(-.-)/\n";
- logger.warn(err);
- return { err: new Response(err, { status: 400 }) };
+ const err = "seems like a pwetty mawfomed job \\(-.-)/";
+ logger.warn(err)
+ return { err };
}
const ok = { type: item.type, arguments: item.arguments };
const invalidArgEntries = invalidExecutionEntriesOf({type: ok.type, ...ok.arguments});
if (invalidArgEntries.length > 0) {
- const err = "your reqwest seems invawid (´。﹏。`) can you fix? uwu";
+ const err = "your reqwest seems invawid (´。﹏。`) can you fix? uwu\n" + invalidArgEntries;
logger.warn(err);
- return { err: new Response(err, { status: 400 }) };
+ return { err };
}
return { ok: <Job>ok };
- });
}
public processHook(r: Traceable<Request>) {
- return r.flatMap(aPost).flatMap((t) => {
+ return r.map(aPost)
+ .map((t) => {
+ either((err) => ({ err }), (request) => )
const {item: {ok: request, err}} = t;
if (err) return <Traceable<{ err: Response }>> t;
return t.map(() => request!).map(aJson(this.getJob));
diff --git a/hooks/queuer.ts b/hooks/queuer.ts
new file mode 100644
index 0000000..1461809
--- /dev/null
+++ b/hooks/queuer.ts
@@ -0,0 +1,47 @@
+import type { IEither, ITraceable, ITraceableLogger } from "@liz-ci/utils";
+import type { Job } from "@liz-ci/model";
+
+type QueuePosition = string;
+export class QueueError extends Error {}
+export interface IJobQueuer<TJob> {
+ queue: <L extends ITraceableLogger<L>>(
+ job: ITraceable<TJob, L>,
+ ) => Promise<IEither<QueueError, QueuePosition>>;
+}
+
+export class LaminarJobQueuer implements IJobQueuer<Job> {
+ constructor(
+ private readonly queuePositionPrefix: string,
+ ) {}
+
+ public async queue({ item: job, logger }: Traceable<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(laminarCommand)).mapBoth(
+ (e) => {
+ const err = `we bwoke oh noesss D:`;
+ logger.error(err, e);
+ return Either.left<QueueError, QueuePosition>(e);
+ },
+ (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 Either.right<QueueError, QueuePosition>(jobUrl);
+ },
+ );
+ }
+}
diff --git a/utils/either.ts b/utils/either.ts
new file mode 100644
index 0000000..d21c796
--- /dev/null
+++ b/utils/either.ts
@@ -0,0 +1,25 @@
+export interface IEither<E, T> {
+ ok?: T;
+ err?: E;
+ mapBoth: <Ee, Tt>(
+ errBranch: (e: E) => Ee,
+ okBranch: (o: T) => Tt,
+ ) => IEither<Ee, Tt>;
+}
+
+export class Either<E, T> implements IEither<E, T> {
+ private constructor(readonly err?: E, readonly ok?: T) {}
+
+ public mapBoth<Ee, Tt>(errBranch: (e: E) => Ee, okBranch: (t: T) => Tt) {
+ if (this.err) return new Either<Ee, Tt>(errBranch(this.err));
+ return new Either<Ee, Tt>(undefined, okBranch(this.ok!));
+ }
+
+ static left<E, T>(e: E) {
+ return new Either<E, T>(e);
+ }
+
+ static right<E, T>(t: T) {
+ return new Either<E, T>(undefined, t);
+ }
+}
diff --git a/utils/mod.ts b/utils/mod.ts
index a8a0751..53ea173 100644
--- a/utils/mod.ts
+++ b/utils/mod.ts
@@ -1,4 +1,5 @@
export * from "./trace.ts";
+export * from "./either.ts";
export * from "./env.ts";
export * from "./run.ts";
export * from "./secret.ts";
diff --git a/utils/run.ts b/utils/run.ts
index 60ae1e6..06e7d9f 100644
--- a/utils/run.ts
+++ b/utils/run.ts
@@ -1,7 +1,10 @@
+import { Either } from "./mod.ts";
+
+export class ProcessError extends Error {}
export const getStdout = async (
cmd: string[] | string,
options: Deno.CommandOptions = {},
-): Promise<string> => {
+): Promise<Either<ProcessError, string>> => {
const [exec, ...args] = (typeof cmd === "string") ? cmd.split(" ") : cmd;
const command = new Deno.Command(exec, {
args,
@@ -10,12 +13,21 @@ export const getStdout = async (
...options,
});
- const { code, stdout, stderr } = await command.output();
-
- const stdoutText = new TextDecoder().decode(stdout);
- const stderrText = new TextDecoder().decode(stderr);
-
- if (code !== 0) throw new Error(`command failed\n${stderrText}`);
+ try {
+ const { code, stdout, stderr } = await command.output();
+ const stdoutText = new TextDecoder().decode(stdout);
+ const stderrText = new TextDecoder().decode(stderr);
- return stdoutText;
+ if (code !== 0) {
+ return Either.left<ProcessError, string>(
+ new ProcessError(`command failed\n${stderrText}`),
+ );
+ }
+ return Either.right<ProcessError, string>(stdoutText);
+ } catch (e) {
+ if (e instanceof Error) {
+ return Either.left<ProcessError, string>(e);
+ }
+ throw new Error("unknown error " + e);
+ }
};
diff --git a/utils/trace.ts b/utils/trace.ts
index eb4ac2f..373f37e 100644
--- a/utils/trace.ts
+++ b/utils/trace.ts
@@ -25,12 +25,15 @@ export interface ITraceable<T, L extends ITraceableLogger<L>> {
mapper: ITraceableMapper<T, L, ITraceableTuple<U>>,
) => ITraceable<U, L>;
peek: (peek: ITraceableMapper<T, L, void>) => ITraceable<T, L>;
- flatMap: <U>(mapper: ITraceableMapper<T, L, ITraceable<U, L>>) => ITraceable<U, L>;
- flatMapAsync<U>(mapper: ITraceableMapper<T, L, Promise<ITraceable<U, L>>>): ITraceable<Promise<U>, L>;
+ flatMap: <U>(
+ mapper: ITraceableMapper<T, L, ITraceable<U, L>>,
+ ) => ITraceable<U, L>;
+ flatMapAsync<U>(
+ mapper: ITraceableMapper<T, L, Promise<ITraceable<U, L>>>,
+ ): ITraceable<Promise<U>, L>;
}
-export class TraceableLogger
- implements ITraceableLogger<TraceableLogger> {
+export class TraceableLogger implements ITraceableLogger<TraceableLogger> {
private readonly logger: Logger = console;
constructor(
private readonly traces = [() => `[${new Date().toISOString()}]`],
@@ -62,23 +65,30 @@ export class TraceableLogger
}
}
-export class TraceableImpl<
+class TraceableImpl<
T,
L extends ITraceableLogger<L>,
> implements ITraceable<T, L> {
- private constructor(readonly item: T, readonly logger: L) {}
+ protected constructor(readonly item: T, readonly logger: L) {}
public map<U>(mapper: ITraceableMapper<T, L, U>) {
const result = mapper(this);
return new TraceableImpl(result, this.logger);
}
- public flatMap<U>(mapper: ITraceableMapper<T, L, ITraceable<U, L>>): ITraceable<U, L> {
+ public flatMap<U>(
+ mapper: ITraceableMapper<T, L, ITraceable<U, L>>,
+ ): ITraceable<U, L> {
return mapper(this);
}
- public flatMapAsync<U>(mapper: ITraceableMapper<T, L, Promise<ITraceable<U, L>>>): ITraceable<Promise<U>, L> {
- return new TraceableImpl(mapper(this).then(({ item }) => item), this.logger);
+ public flatMapAsync<U>(
+ mapper: ITraceableMapper<T, L, Promise<ITraceable<U, L>>>,
+ ): ITraceable<Promise<U>, L> {
+ return new TraceableImpl(
+ mapper(this).then(({ item }) => item),
+ this.logger,
+ );
}
public peek(peek: ITraceableMapper<T, L, void>) {
@@ -95,20 +105,21 @@ export class TraceableImpl<
mapper: ITraceableMapper<T, L, U>,
): ITraceableMapper<Promise<T>, L, Promise<U>> {
return (traceablePromise) =>
- traceablePromise.flatMapAsync(async (t) => {
- const item = await t.item;
- return t.map(() => item).map(mapper);
- }).item;
+ traceablePromise.flatMapAsync(async (t) => {
+ const item = await t.item;
+ return t.map(() => item).map(mapper);
+ }).item;
}
+}
- static withClassTrace<C extends Object, T, L extends ITraceableLogger<L>>(c: C): ITraceableMapper<T, L, ITraceableTuple<T>> {
+export class Traceable<T> extends TraceableImpl<T, TraceableLogger> {
+ static withClassTrace<C extends object, T>(
+ c: C,
+ ): ITraceableMapper<T, TraceableLogger, ITraceableTuple<T>> {
return (t) => [t.item, () => c.constructor.name];
}
static from<T>(t: T) {
- return new TraceableImpl(t, new TraceableLogger());
+ return new Traceable(t, new TraceableLogger());
}
}
-
-export interface Traceable<T, L extends ITraceableLogger<L> = TraceableLogger> extends ITraceable<T, L> {
-}