summaryrefslogtreecommitdiff
path: root/worker/jobs/ci_pipeline.run
diff options
context:
space:
mode:
authorElizabeth Alexander Hunt <me@liz.coffee>2025-05-12 09:40:12 -0700
committerElizabeth <me@liz.coffee>2025-05-26 14:15:42 -0700
commitd51c9d74857aca3c2f172609297266968bc7f809 (patch)
tree64327f9cc4219729aa11af32d7d4c70cddfc2292 /worker/jobs/ci_pipeline.run
parent30729a0cf707d9022bae0a7baaba77379dc31fd5 (diff)
downloadci-d51c9d74857aca3c2f172609297266968bc7f809.tar.gz
ci-d51c9d74857aca3c2f172609297266968bc7f809.zip
The big refactor TM
Diffstat (limited to 'worker/jobs/ci_pipeline.run')
-rw-r--r--worker/jobs/ci_pipeline.run166
1 files changed, 166 insertions, 0 deletions
diff --git a/worker/jobs/ci_pipeline.run b/worker/jobs/ci_pipeline.run
new file mode 100644
index 0000000..337bd53
--- /dev/null
+++ b/worker/jobs/ci_pipeline.run
@@ -0,0 +1,166 @@
+import {
+ type Command,
+ Either,
+ getRequiredEnvVars,
+ getStdout,
+ isObject,
+ LogMetricTraceable,
+ Metric,
+ prependWith,
+ TraceUtil,
+} from "@emprespresso/pengueno";
+import {
+ type CheckoutCiJob,
+ type FetchCodeJob,
+ PipelineImpl,
+} from "@emprespresso/ci-model";
+import { executeJob, executePipeline } from "@emprespresso/ci-worker";
+
+const run = Date.now().toString();
+const eitherJob = getRequiredEnvVars(["remote", "refname", "rev"])
+ .mapRight((baseArgs) => (
+ <CheckoutCiJob> {
+ type: "checkout_ci",
+ arguments: {
+ ...baseArgs,
+ run,
+ returnPath: Deno.cwd(),
+ },
+ }
+ ));
+
+const ciRunMetric = Metric.fromName("checkout_ci.run");
+const trace = `checkout_ci.${run}`;
+await LogMetricTraceable.from(eitherJob).bimap(TraceUtil.withTrace(trace))
+ .bimap(TraceUtil.withMetricTrace(ciRunMetric))
+ .map((tEitherJob) =>
+ tEitherJob.get().flatMapAsync((ciJob) => {
+ const wd = getWorkingDirectoryForCiJob(ciJob);
+ const fetchPackageJob = <FetchCodeJob> {
+ type: "fetch_code",
+ arguments: {
+ remoteUrl: ciJob.arguments.remote,
+ checkout: ciJob.arguments.rev,
+ path: getSrcDirectoryForCiJob(ciJob),
+ },
+ };
+ return Either.fromFailableAsync<Error, CheckoutCiJob>(
+ Deno.mkdir(wd).then(() => Deno.chdir(wd))
+ .then(() => tEitherJob.move(fetchPackageJob).map(executeJob).get())
+ .then(() => ciJob),
+ );
+ })
+ )
+ .map((tEitherCiJob) =>
+ tEitherCiJob.get().then((eitherCiJob) =>
+ eitherCiJob.flatMapAsync<{ cmd: Command; job: CheckoutCiJob }>((ciJob) =>
+ Either.fromFailableAsync<Error, string>(
+ Deno.readTextFile(
+ `${getSrcDirectoryForCiJob(ciJob)}/${CI_WORKFLOW_FILE}`,
+ ),
+ ).then((eitherWorkflowJson) =>
+ eitherWorkflowJson.flatMap(
+ (json) => Either.fromFailable<Error, unknown>(JSON.parse(json)),
+ ).flatMap((eitherWorkflowParse) => {
+ if (isCiWorkflow(eitherWorkflowParse)) {
+ return Either.right({
+ cmd: getPipelineGenerationCommand(
+ ciJob,
+ eitherWorkflowParse.workflow,
+ ),
+ job: ciJob,
+ });
+ }
+ return Either.left(
+ new Error(
+ "couldn't find any valid ci configuration (。•́︿•̀。), that's okay~",
+ ),
+ );
+ })
+ )
+ )
+ )
+ )
+ .map(async (tEitherPipelineGenerationCommand) => {
+ const eitherJobCommand = await tEitherPipelineGenerationCommand.get();
+ const eitherPipeline = await eitherJobCommand
+ .flatMapAsync((jobCommand) =>
+ tEitherPipelineGenerationCommand.move(jobCommand.cmd)
+ .map(getStdout)
+ .get()
+ );
+ return eitherPipeline
+ .flatMap(PipelineImpl.from)
+ .flatMap((pipeline) =>
+ eitherJobCommand.mapRight(({ job }) => ({ job, pipeline }))
+ );
+ })
+ .peek(
+ TraceUtil.promiseify((tEitherPipeline) =>
+ tEitherPipeline.get()
+ .mapRight((val) => val.pipeline.serialize())
+ .mapRight((pipeline) =>
+ `built the pipeline~ (◕ᴗ◕✿) let's make something amazing! ${pipeline}`
+ )
+ .mapRight((msg) => tEitherPipeline.trace.trace(msg))
+ ),
+ )
+ .map(
+ async (tEitherPipeline) => {
+ const eitherPipeline = await tEitherPipeline.get();
+ return eitherPipeline.flatMapAsync(({ pipeline, job }) =>
+ tEitherPipeline.move(pipeline)
+ .map((p) =>
+ executePipeline(p, {
+ HOME: getWorkingDirectoryForCiJob(job),
+ })
+ )
+ .get()
+ );
+ },
+ )
+ .get()
+ .then((e) =>
+ e.flatMap(() => eitherJob).fold((err, val) => {
+ if (err) throw err;
+ return Deno.remove(getWorkingDirectoryForCiJob(val), { recursive: true });
+ })
+ );
+
+const getWorkingDirectoryForCiJob = (job: CheckoutCiJob) =>
+ `${job.arguments.returnPath}/${job.arguments.run}`;
+
+const getSrcDirectoryForCiJob = (job: CheckoutCiJob) =>
+ `${job.arguments.returnPath}/${job.arguments.run}/src`;
+
+const _runFlags = ("--rm --network none --cap-drop ALL" +
+ "--security-opt no-new-privileges").split(" ");
+const _image = "oci.liz.coffee/img/ci-worker:release";
+const getPipelineGenerationCommand = (
+ job: CheckoutCiJob,
+ pipelineGeneratorPath: string,
+ image = _image,
+ runFlags = _runFlags,
+) => [
+ "docker",
+ "run",
+ ...runFlags,
+ ...prependWith(
+ Object.entries(job.arguments).map(([key, val]) => `"${key}"="${val}"`),
+ "-e",
+ ),
+ "-v",
+ `${
+ getSrcDirectoryForCiJob(job)
+ }/${pipelineGeneratorPath}:/pipeline_generator`,
+ image,
+ "/pipeline_generator",
+];
+
+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 CI_WORKFLOW_FILE = ".ci/ci.json";