feat(pipelines): add pipelines service methodsenjoy pipelinesService.* !
@@ -20,7 +20,7 @@ export const Chip = styled.div<WithThemeSchemeProp & { color?: string }>`
text-decoration: none !important;
color: ${color
- ? Color(color).alpha(1).lightness(0.3).toString()
+ ? Color(color).alpha(1).toString()
: NamedColors.TEXT_DEFAULT[themeScheme]};
background-color: ${color
? Color(color).alpha(0.2).toString()
@@ -0,0 +1,15 @@
+// 1st-party
+import type { ServiceMethodFactory } from "@ethicdevs/react-monolith";
+import type { PipelineServiceDeps } from "./types";
+
+const makeCancelRunner: ServiceMethodFactory<
+ PipelineServiceDeps,
+ [string],
+ Promise<any>
+> = ({ runner }) => {
+ return async (pipelineId: string) => {
+ return runner.cancelRun(pipelineId);
+ };
+};
+
+export default makeCancelRunner;
@@ -0,0 +1,22 @@
+// 1st-party
+import type { ServiceMethodFactory } from "@ethicdevs/react-monolith";
+// auto-generated via script [prisma:generate]
+import type { Pipeline } from "@prisma/client";
+// app
+import type { PipelineServiceDeps } from "./types";
+
+const makeGetPipeline: ServiceMethodFactory<
+ PipelineServiceDeps,
+ [string],
+ Promise<Pipeline | null>
+> = ({ request }) => {
+ return async (pipelineId: string) => {
+ return request.prisma.pipeline.findUnique({
+ where: {
+ id: pipelineId,
+ },
+ });
+ };
+};
+
+export default makeGetPipeline;
@@ -0,0 +1,15 @@
+// 1st-party
+import type { ServiceMethodFactory } from "@ethicdevs/react-monolith";
+import type { PipelineServiceDeps } from "./types";
+
+const makeGetPipelineArtefacts: ServiceMethodFactory<
+ PipelineServiceDeps,
+ [string],
+ Promise<any>
+> = ({ runner }) => {
+ return async (pipelineId: string) => {
+ return runner.getPipelineArtefacts(pipelineId);
+ };
+};
+
+export default makeGetPipelineArtefacts;
@@ -0,0 +1,18 @@
+// 1st-party
+import type { ServiceMethodFactory } from "@ethicdevs/react-monolith";
+import type { PipelineServiceDeps } from "./types";
+
+const makeGetPipelineManifest: ServiceMethodFactory<
+ PipelineServiceDeps,
+ [string],
+ Promise<string | null>
+> = ({ request }) => {
+ return async (pipelineId: string) => {
+ const p = await request.prisma.pipeline.findUnique({
+ where: { id: pipelineId },
+ });
+ return p?.manifest ?? null;
+ };
+};
+
+export default makeGetPipelineManifest;
@@ -0,0 +1,18 @@
+// 1st-party
+import type { ServiceMethodFactory } from "@ethicdevs/react-monolith";
+import type { PipelineServiceDeps } from "./types";
+
+const makeGetPipelineStageLogs: ServiceMethodFactory<
+ PipelineServiceDeps,
+ [string, string],
+ Promise<string>
+> = ({ request }) => {
+ return async (pipelineId: string, stageId: string) => {
+ const stage = await request.prisma.stage.findFirst({
+ where: { id: stageId, pipelineId },
+ });
+ return stage?.logs ?? "";
+ };
+};
+
+export default makeGetPipelineStageLogs;
@@ -0,0 +1,18 @@
+// 1st-party
+import type { ServiceMethodFactory } from "@ethicdevs/react-monolith";
+import type { PipelineServiceDeps } from "./types";
+
+const makeGetPipelineStages: ServiceMethodFactory<
+ PipelineServiceDeps,
+ [string],
+ Promise<any>
+> = ({ request }) => {
+ return async (pipelineId: string) => {
+ return request.prisma.stage.findMany({
+ where: { pipelineId },
+ orderBy: { order: "asc" },
+ });
+ };
+};
+
+export default makeGetPipelineStages;
@@ -0,0 +1,25 @@
+// 1st-party
+import type { ServiceMethodFactory } from "@ethicdevs/react-monolith";
+import type { PipelineServiceDeps } from "./types";
+
+const makeGetRepoPipelineManifest: ServiceMethodFactory<
+ PipelineServiceDeps,
+ [string, string],
+ Promise<string | null>
+> = ({ request }) => {
+ return async (orgSlug: string, repoSlug: string) => {
+ const p = await request.prisma.pipeline.findFirst({
+ where: {
+ repo: {
+ organization: { slug: orgSlug },
+ slug: repoSlug,
+ },
+ },
+ orderBy: { createdAt: "desc" },
+ });
+ // todo: return a deserialized PipelineManifest
+ return p?.manifest ?? null;
+ };
+};
+
+export default makeGetRepoPipelineManifest;
@@ -0,0 +1,41 @@
+// 1st-party
+import { makeService } from "@ethicdevs/react-monolith";
+// app
+// app service pipelines
+import type { PipelineServiceAPI, PipelineServiceDeps } from "./types";
+import { default as makeListByRepo } from "./listByRepo";
+import { default as makeListByRepoId } from "./listByRepoId";
+import { default as makeGetPipeline } from "./getPipeline";
+import { default as makeSetPipeline } from "./setPipeline";
+import { default as makeRmPipeline } from "./rmPipeline";
+import { default as makeParsePipelineManifest } from "./parsePipelineManifest";
+import { default as makeGetRepoPipelineManifest } from "./getRepoPipelineManifest";
+import { default as makeGetPipelineManifest } from "./getPipelineManifest";
+import { default as makeGetPipelineStages } from "./getPipelineStages";
+import { default as makeGetPipelineStageLogs } from "./getPipelineStageLogs";
+import { default as makeGetPipelineArtefacts } from "./getPipelineArtefacts";
+import { default as makeResetRunnerCache } from "./resetRunnerCache";
+import { default as makeInitRunnerForRepo } from "./initRunnerForRepo";
+import { default as makeTriggerRunner } from "./triggerRunner";
+import { default as makeCancelRunner } from "./cancelRunner";
+
+export const makePipelineService = makeService<
+ PipelineServiceAPI,
+ PipelineServiceDeps
+>({
+ listByRepo: makeListByRepo,
+ listByRepoId: makeListByRepoId,
+ getPipeline: makeGetPipeline,
+ setPipeline: makeSetPipeline,
+ rmPipeline: makeRmPipeline,
+ parsePipelineManifest: makeParsePipelineManifest,
+ getRepoPipelineManifest: makeGetRepoPipelineManifest,
+ getPipelineManifest: makeGetPipelineManifest,
+ getPipelineStages: makeGetPipelineStages,
+ getPipelineArtefacts: makeGetPipelineArtefacts,
+ getPipelineStageLogs: makeGetPipelineStageLogs,
+ resetRunnerCache: makeResetRunnerCache,
+ initRunnerForRepo: makeInitRunnerForRepo,
+ triggerRunner: makeTriggerRunner,
+ cancelRunner: makeCancelRunner,
+});
@@ -0,0 +1,15 @@
+// 1st-party
+import type { ServiceMethodFactory } from "@ethicdevs/react-monolith";
+import type { PipelineServiceDeps } from "./types";
+
+const makeInitRunnerForRepo: ServiceMethodFactory<
+ PipelineServiceDeps,
+ [string, string, string],
+ Promise<any>
+> = ({ runner }) => {
+ return async (orgSlug: string, repoSlug: string, manifest: string) => {
+ return runner.initRepo(orgSlug, repoSlug, manifest);
+ };
+};
+
+export default makeInitRunnerForRepo;
@@ -0,0 +1,26 @@
+// 1st-party
+import type { ServiceMethodFactory } from "@ethicdevs/react-monolith";
+import type { Pipeline } from "@prisma/client";
+import type { PipelineServiceDeps } from "./types";
+
+const makeListByRepo: ServiceMethodFactory<
+ PipelineServiceDeps,
+ [string, string],
+ Promise<Pipeline[]>
+> = ({ request }) => {
+ return async (orgSlug: string, repoSlug: string) => {
+ return request.prisma.pipeline.findMany({
+ where: {
+ repo: {
+ organization: {
+ slug: orgSlug,
+ },
+ slug: repoSlug,
+ },
+ },
+ orderBy: { createdAt: "desc" },
+ });
+ };
+};
+
+export default makeListByRepo;
@@ -0,0 +1,19 @@
+// 1st-party
+import type { ServiceMethodFactory } from "@ethicdevs/react-monolith";
+import type { Pipeline } from "@prisma/client";
+import type { PipelineServiceDeps } from "./types";
+
+const makeListByRepoId: ServiceMethodFactory<
+ PipelineServiceDeps,
+ [string],
+ Promise<Pipeline[]>
+> = ({ request }) => {
+ return async (repoId: string) => {
+ return request.prisma.pipeline.findMany({
+ where: { repoId },
+ orderBy: { createdAt: "desc" },
+ });
+ };
+};
+
+export default makeListByRepoId;
@@ -0,0 +1,20 @@
+// 1st-party
+import type { ServiceMethodFactory } from "@ethicdevs/react-monolith";
+import type { Manifest, PipelineServiceDeps } from "./types";
+
+const makeParsePipelineManifest: ServiceMethodFactory<
+ PipelineServiceDeps,
+ [string],
+ Promise<Manifest>
+> = () => {
+ return async (manifestJsonOrYml: string) => {
+ try {
+ const m = JSON.parse(manifestJsonOrYml);
+ return { manifest: manifestJsonOrYml, version: m.version ?? "1.0" };
+ } catch {
+ throw new Error("Unsupported manifest format. Provide JSON.");
+ }
+ };
+};
+
+export default makeParsePipelineManifest;
@@ -0,0 +1,15 @@
+// 1st-party
+import type { ServiceMethodFactory } from "@ethicdevs/react-monolith";
+import type { PipelineServiceDeps } from "./types";
+
+const makeResetRunnerCache: ServiceMethodFactory<
+ PipelineServiceDeps,
+ [string, string],
+ Promise<any>
+> = ({ runner }) => {
+ return async (orgSlug: string, repoSlug: string) => {
+ return runner.resetCache(orgSlug, repoSlug);
+ };
+};
+
+export default makeResetRunnerCache;
@@ -0,0 +1,20 @@
+// 1st-party
+import type { ServiceMethodFactory } from "@ethicdevs/react-monolith";
+import type { PipelineServiceDeps } from "./types";
+
+const makeRmPipeline: ServiceMethodFactory<
+ PipelineServiceDeps,
+ [string, string],
+ Promise<any>
+> = ({ request, runner }) => {
+ return async (pipelineId: string, reason: string) => {
+ await runner.rmPipeline(pipelineId, reason);
+
+ return request.prisma.pipeline.update({
+ where: { id: pipelineId },
+ data: { status: "CANCELED" as any },
+ });
+ };
+};
+
+export default makeRmPipeline;
@@ -0,0 +1,19 @@
+// 1st-party
+import type { ServiceMethodFactory } from "@ethicdevs/react-monolith";
+import type { PipelineServiceDeps } from "./types";
+import type { Pipeline } from "@prisma/client";
+
+const makeSetPipeline: ServiceMethodFactory<
+ PipelineServiceDeps,
+ [string, Partial<Pipeline>],
+ Promise<Pipeline>
+> = ({ request }) => {
+ return async (pipelineId: string, data: Partial<Pipeline>) => {
+ return request.prisma.pipeline.update({
+ where: { id: pipelineId },
+ data: data as any,
+ });
+ };
+};
+
+export default makeSetPipeline;
@@ -0,0 +1,15 @@
+// 1st-party
+import type { ServiceMethodFactory } from "@ethicdevs/react-monolith";
+import type { PipelineServiceDeps } from "./types";
+
+const makeTriggerRunner: ServiceMethodFactory<
+ PipelineServiceDeps,
+ [string, string, string],
+ Promise<any>
+> = ({ runner }) => {
+ return async (orgSlug: string, repoSlug: string, pipelineId: string) => {
+ return runner.triggerRun(orgSlug, repoSlug, pipelineId);
+ };
+};
+
+export default makeTriggerRunner;
@@ -0,0 +1,45 @@
+// 3rd-party
+import type { FastifyRequest } from "fastify";
+// generated via script [prisma:generate]
+import type { Artefact, Pipeline, Stage } from "@prisma/client";
+
+// Lightweight shared types for pipeline domain to minimize coupling in MVP.
+export type Manifest = { manifest: string; version?: string };
+
+export type PipelineEntity = Pipeline;
+export type StageEntity = Stage;
+export type ArtefactEntity = Artefact;
+
+export interface PipelineServiceDeps {
+ request: FastifyRequest;
+ runner: any; // GitfossCIRunnerClient
+}
+
+export type PipelineServiceAPI = {
+ listByRepo(orgSlug: string, repoSlug: string): Promise<Pipeline[]>;
+ listByRepoId(repoId: string): Promise<Pipeline[]>;
+ getRepoPipelineManifest(orgSlug: string, repoSlug: string): Promise<Manifest>;
+ parsePipelineManifest(manifestJsonOrYml: string): Manifest;
+ getPipelineManifest(pipelineId: string): Promise<Manifest>;
+ getPipelineStages(pipelineId: string): Promise<Stage[]>;
+ getPipelineStageLogs(
+ pipelineId: string,
+ stageId: string,
+ ): Promise<Stage["logs"][]>;
+ getPipelineArtefacts(pipelineId: string): Promise<Artefact[]>;
+ getPipeline(pipelineId: string): Promise<Pipeline>;
+ setPipeline(pipelineId: string, data: Partial<Pipeline>): Promise<Pipeline>;
+ rmPipeline(pipelineId: string, reason: string): Promise<Pipeline>;
+ initRunnerForRepo(
+ orgSlug: string,
+ repoSlug: string,
+ manifest: string,
+ ): Promise<void>;
+ triggerRunner(
+ orgSlug: string,
+ repoSlug: string,
+ pipelineId: string,
+ ): Promise<Pipeline>;
+ cancelRunner(pipelineId: string): Promise<void>;
+ resetRunnerCache(orgSlug: string, repoSlug: string): Promise<void>;
+};
@@ -0,0 +1,53 @@
+-- CreateEnum
+CREATE TYPE "PipelineStatus" AS ENUM ('PENDING', 'RUNNING', 'PASSED', 'FAILED', 'CANCELED');
+
+-- CreateEnum
+CREATE TYPE "StageStatus" AS ENUM ('PENDING', 'RUNNING', 'PASSED', 'FAILED', 'CANCELED');
+
+-- CreateTable
+CREATE TABLE "Pipeline" (
+ "id" TEXT NOT NULL,
+ "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ "updatedAt" TIMESTAMP(3) NOT NULL,
+ "name" TEXT NOT NULL,
+ "status" "PipelineStatus" NOT NULL DEFAULT 'PENDING',
+ "manifest" TEXT,
+ "repoId" TEXT NOT NULL,
+
+ CONSTRAINT "Pipeline_pkey" PRIMARY KEY ("id")
+);
+
+-- CreateTable
+CREATE TABLE "Stage" (
+ "id" TEXT NOT NULL,
+ "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ "updatedAt" TIMESTAMP(3) NOT NULL,
+ "name" TEXT NOT NULL,
+ "order" INTEGER NOT NULL,
+ "status" "StageStatus" NOT NULL DEFAULT 'PENDING',
+ "logs" TEXT,
+ "pipelineId" TEXT NOT NULL,
+
+ CONSTRAINT "Stage_pkey" PRIMARY KEY ("id")
+);
+
+-- CreateTable
+CREATE TABLE "Artefact" (
+ "id" TEXT NOT NULL,
+ "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ "name" TEXT NOT NULL,
+ "path" TEXT NOT NULL,
+ "size" INTEGER,
+ "pipelineId" TEXT NOT NULL,
+
+ CONSTRAINT "Artefact_pkey" PRIMARY KEY ("id")
+);
+
+-- AddForeignKey
+ALTER TABLE "Pipeline" ADD CONSTRAINT "Pipeline_repoId_fkey" FOREIGN KEY ("repoId") REFERENCES "Repository"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
+
+-- AddForeignKey
+ALTER TABLE "Stage" ADD CONSTRAINT "Stage_pipelineId_fkey" FOREIGN KEY ("pipelineId") REFERENCES "Pipeline"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
+
+-- AddForeignKey
+ALTER TABLE "Artefact" ADD CONSTRAINT "Artefact_pipelineId_fkey" FOREIGN KEY ("pipelineId") REFERENCES "Pipeline"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
@@ -11,7 +11,7 @@ datasource db {
model Organization {
id String @id @default(cuid())
-
+
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@ -105,11 +105,12 @@ model Repository {
shortDescription String?
websiteUrl String?
+ organization Organization @relation("ManyRepositoriesToOneOrganization", fields: [organizationId], references: [id])
forkedFromRepo Repository? @relation("OneParentRepositoryToManyForkRepository", fields: [forkedFromRepoId], references: [id])
forks Repository[] @relation("OneParentRepositoryToManyForkRepository")
- organization Organization @relation("ManyRepositoriesToOneOrganization", fields: [organizationId], references: [id])
pullRequestsWhereSource PullRequest[] @relation("OneSourceRepositoryToManyPullRequests")
pullRequestsWhereTarget PullRequest[] @relation("OneTargetRepositoryToManyPullRequests")
+ pipelines Pipeline[] @relation("RepositoryPipelines")
@@unique([slug, organizationId])
}
@@ -163,6 +164,48 @@ model UserSSHKey {
revoked Boolean @default(false)
}
+model Pipeline {
+ id String @id @default(cuid())
+ createdAt DateTime @default(now())
+ updatedAt DateTime @updatedAt
+
+ name String
+ status PipelineStatus @default(PENDING)
+ manifest String?
+
+ repoId String
+ repo Repository @relation("RepositoryPipelines",fields: [repoId], references: [id])
+
+ stages Stage[] @relation("PipelineStages")
+ artefacts Artefact[] @relation("PipelineArtefacts")
+}
+
+model Stage {
+ id String @id @default(cuid())
+ createdAt DateTime @default(now())
+ updatedAt DateTime @updatedAt
+
+ name String
+ order Int
+ status StageStatus @default(PENDING)
+ logs String?
+
+ pipelineId String
+ pipeline Pipeline @relation("PipelineStages", fields: [pipelineId], references: [id])
+}
+
+model Artefact {
+ id String @id @default(cuid())
+ createdAt DateTime @default(now())
+
+ name String
+ path String
+ size Int?
+
+ pipelineId String
+ pipeline Pipeline @relation("PipelineArtefacts", fields: [pipelineId], references: [id])
+}
+
enum GlobalRole {
GUEST
CUSTOMER
@@ -193,3 +236,19 @@ enum ResourceVisibility {
UNLISTED
PRIVATE
}
+
+enum PipelineStatus {
+ PENDING
+ RUNNING
+ PASSED
+ FAILED
+ CANCELED
+}
+
+enum StageStatus {
+ PENDING
+ RUNNING
+ PASSED
+ FAILED
+ CANCELED
+}