diff --git a/apps/docs/pages/getting-started/deployment.mdx b/apps/docs/pages/getting-started/deployment.mdx index 1f406b6e..2304120b 100644 --- a/apps/docs/pages/getting-started/deployment.mdx +++ b/apps/docs/pages/getting-started/deployment.mdx @@ -10,7 +10,7 @@ import { Steps } from "nextra/components"; ### Create Github Workflow -### Connect Github to CtrlPlane +### Connect Github to Ctrlplane ### Run the deploy job diff --git a/apps/nextjs/src/app/[workspaceSlug]/(job)/job-agents/add/GithubConfig.tsx b/apps/nextjs/src/app/[workspaceSlug]/(job)/job-agents/add/GithubConfig.tsx index f555bee7..53657aa9 100644 --- a/apps/nextjs/src/app/[workspaceSlug]/(job)/job-agents/add/GithubConfig.tsx +++ b/apps/nextjs/src/app/[workspaceSlug]/(job)/job-agents/add/GithubConfig.tsx @@ -87,7 +87,9 @@ export const GithubJobAgentConfig: React.FC<{ ?.filter( (org) => !githubOrgsInstalled.data?.some( - (o) => o.organizationName === org.data.login, + (o) => + o.github_organization.organizationName === + org.data.login, ), ) .map(({ data: { id, login, avatar_url } }) => ( diff --git a/apps/nextjs/src/app/[workspaceSlug]/(job)/job-agents/add/JobAgentSelectCard.tsx b/apps/nextjs/src/app/[workspaceSlug]/(job)/job-agents/add/JobAgentSelectCard.tsx deleted file mode 100644 index cbf1aeb2..00000000 --- a/apps/nextjs/src/app/[workspaceSlug]/(job)/job-agents/add/JobAgentSelectCard.tsx +++ /dev/null @@ -1,100 +0,0 @@ -import { - SiCircleci, - SiGithub, - SiGooglecloud, - SiKubernetes, -} from "react-icons/si"; -import { TbCaretDownFilled, TbWebhook } from "react-icons/tb"; - -import { Button } from "@ctrlplane/ui/button"; -import { - Card, - CardContent, - CardDescription, - CardHeader, - CardTitle, -} from "@ctrlplane/ui/card"; -import { - DropdownMenu, - DropdownMenuContent, - DropdownMenuItem, - DropdownMenuTrigger, -} from "@ctrlplane/ui/dropdown-menu"; - -export const JobAgentSelectCard: React.FC<{ - setValue: (v: string) => void; -}> = ({ setValue }) => { - return ( - - - Choose a job agent. - - Choose the pipeline agent you would like to connect. - - - - - - - - - setValue("github-app")}> - Github App - - Github OAuth - - - - - - - - - setValue("kubernetes-job")}> - Job - - - - - - - - - - Workflows - Cloud Function - - - - - - - - ); -}; diff --git a/apps/nextjs/src/app/[workspaceSlug]/(job)/job-agents/add/KubernetesConfig.tsx b/apps/nextjs/src/app/[workspaceSlug]/(job)/job-agents/add/KubernetesConfig.tsx deleted file mode 100644 index 03c595e6..00000000 --- a/apps/nextjs/src/app/[workspaceSlug]/(job)/job-agents/add/KubernetesConfig.tsx +++ /dev/null @@ -1,53 +0,0 @@ -"use client"; - -import React, { useState } from "react"; -import { useParams } from "next/navigation"; - -import { - Card, - CardContent, - CardDescription, - CardHeader, - CardTitle, -} from "@ctrlplane/ui/card"; - -export const KubernetesJobDeploy: React.FC = () => { - const { workspaceSlug } = useParams<{ workspaceSlug: string }>(); - const [name, setName] = useState(""); - return ( - - - Kubernetes job agent config - - Configure a kubernetes job agent to dispatch your runs. - - - -
helm repo add ctrlplane https://charts.ctrlplane.dev
-
-          

helm upgrade --install

-

--set workspace={workspaceSlug}

-

- --set name= - setName(e.target.value)} - className="max-w-[200px] bg-transparent placeholder:italic placeholder:text-red-400" - placeholder="JOB AGENT NAME" - /> -

-

- --set apiKey= - setName(e.target.value)} - className="bg-transparent placeholder:italic placeholder:text-red-400" - placeholder="API KEY" - /> -

-

job agent ctrlplane/agent

-
-
-
- ); -}; diff --git a/apps/nextjs/src/app/[workspaceSlug]/(job)/job-agents/add/page.tsx b/apps/nextjs/src/app/[workspaceSlug]/(job)/job-agents/add/page.tsx deleted file mode 100644 index 52baf557..00000000 --- a/apps/nextjs/src/app/[workspaceSlug]/(job)/job-agents/add/page.tsx +++ /dev/null @@ -1,106 +0,0 @@ -"use client"; - -import { useEffect, useState } from "react"; -import { redirect } from "next/navigation"; -import { useSession } from "next-auth/react"; -import { TbBolt, TbCheck, TbNumber1, TbNumber2 } from "react-icons/tb"; - -import { cn } from "@ctrlplane/ui"; - -import { env } from "~/env"; -import { api } from "~/trpc/react"; -import { GithubJobAgentConfig } from "./GithubConfig"; -import { JobAgentSelectCard } from "./JobAgentSelectCard"; -import { KubernetesJobDeploy } from "./KubernetesConfig"; - -const githubAuthUrl = (userId?: string) => - `${env.GITHUB_URL}/login/oauth/authorize?response_type=code&client_id=${env.NEXT_PUBLIC_GITHUB_BOT_CLIENT_ID}&redirect_uri=${env.BASE_URL}/api/github/${userId}&state=sLtHqpxQ6FiUtBWJ&scope=repo%2Cread%3Auser`; - -export default function AddJobAgentPage({ - params, -}: { - params: { workspaceSlug: string }; -}) { - const [jobAgentType, setJobAgentType] = useState(null); - const session = useSession(); - - const githubUser = api.github.user.byUserId.useQuery( - session.data?.user.id ?? "", - { enabled: session.status === "authenticated" }, - ); - - const { workspaceSlug } = params; - const workspace = api.workspace.bySlug.useQuery(workspaceSlug); - - useEffect(() => { - const isGithubDisptacher = jobAgentType === "github-app"; - const isUserUnauthenticatedForGit = - !githubUser.isLoading && githubUser.data == null; - - if (isGithubDisptacher && isUserUnauthenticatedForGit) - redirect(githubAuthUrl(session.data?.user.id)); - }, [jobAgentType, githubUser, workspace, session]); - - return ( -
-

- - Add Job Agent -

-
-
setJobAgentType(null)} - className={cn( - "flex items-center gap-4", - jobAgentType != null && "cursor-pointer text-muted-foreground", - )} - > -
- {jobAgentType == null || - (jobAgentType === "github-app" && githubUser.data == null) ? ( - - ) : ( - - )} -
-
Select job agent
-
- -
-
- -
-
Configure job agent
-
-
- {jobAgentType == null && ( - - )} - {jobAgentType === "kubernetes-job" && } - {jobAgentType === "github-app" && - githubUser.data != null && - workspace.data != null && ( - - )} -
- ); -} diff --git a/apps/nextjs/src/app/[workspaceSlug]/settings/(settings)/workspace/integrations/(integration)/github/GithubConfigFile.tsx b/apps/nextjs/src/app/[workspaceSlug]/settings/(settings)/workspace/integrations/(integration)/github/GithubConfigFile.tsx new file mode 100644 index 00000000..3cd076bc --- /dev/null +++ b/apps/nextjs/src/app/[workspaceSlug]/settings/(settings)/workspace/integrations/(integration)/github/GithubConfigFile.tsx @@ -0,0 +1,40 @@ +import { + Card, + CardContent, + CardDescription, + CardHeader, + CardTitle, +} from "@ctrlplane/ui/card"; +import { Separator } from "@ctrlplane/ui/separator"; + +import { api } from "~/trpc/react"; + +export const GithubConfigFileSync: React.FC<{ + workspaceId?: string; +}> = ({ workspaceId }) => { + const configFiles = api.github.configFile.list.useQuery(workspaceId ?? "", { + enabled: workspaceId != null, + }); + + return ( + + + Sync Github Config File + + A{" "} + ctrlplane.yaml{" "} + configuration file allows you to manage your Ctrlplane resources from + github. + + + + + + + {configFiles.data?.map((configFile) => ( +
{configFile.name}
+ ))} +
+
+ ); +}; diff --git a/apps/nextjs/src/app/[workspaceSlug]/settings/(settings)/workspace/integrations/(integration)/github/GithubOrgConfig.tsx b/apps/nextjs/src/app/[workspaceSlug]/settings/(settings)/workspace/integrations/(integration)/github/GithubOrgConfig.tsx new file mode 100644 index 00000000..18b1aafe --- /dev/null +++ b/apps/nextjs/src/app/[workspaceSlug]/settings/(settings)/workspace/integrations/(integration)/github/GithubOrgConfig.tsx @@ -0,0 +1,295 @@ +import type { GithubUser } from "@ctrlplane/db/schema"; +import { useState } from "react"; +import _ from "lodash"; +import { SiGithub } from "react-icons/si"; +import { TbChevronDown, TbPlus } from "react-icons/tb"; + +import { cn } from "@ctrlplane/ui"; +import { Avatar, AvatarFallback, AvatarImage } from "@ctrlplane/ui/avatar"; +import { Button } from "@ctrlplane/ui/button"; +import { + Card, + CardContent, + CardDescription, + CardHeader, + CardTitle, +} from "@ctrlplane/ui/card"; +import { + Command, + CommandGroup, + CommandInput, + CommandItem, + CommandList, +} from "@ctrlplane/ui/command"; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuTrigger, +} from "@ctrlplane/ui/dropdown-menu"; +import { Popover, PopoverContent, PopoverTrigger } from "@ctrlplane/ui/popover"; +import { Separator } from "@ctrlplane/ui/separator"; +import { Skeleton } from "@ctrlplane/ui/skeleton"; + +import { env } from "~/env"; +import { api } from "~/trpc/react"; + +export const GithubOrgConfig: React.FC<{ + githubUser?: GithubUser | null; + workspaceSlug?: string; + workspaceId?: string; + loading: boolean; +}> = ({ githubUser, workspaceSlug, workspaceId, loading }) => { + const githubOrgs = api.github.organizations.byGithubUserId.useQuery( + githubUser?.githubUserId ?? 0, + { enabled: !loading && githubUser != null }, + ); + const githubOrgCreate = api.github.organizations.create.useMutation(); + const githubOrgsInstalled = api.github.organizations.list.useQuery( + workspaceId ?? "", + { enabled: !loading && workspaceId != null }, + ); + const githubOrgUpdate = api.github.organizations.update.useMutation(); + const jobAgentCreate = api.job.agent.create.useMutation(); + + const utils = api.useUtils(); + + const [open, setOpen] = useState(false); + const [value, setValue] = useState(null); + const [image, setImage] = useState(null); + + return ( + + + + + Connect an organization + + + Select an organization to integrate with Ctrlplane. + + +
+
+ + + + + + + + + + {githubOrgs.data + ?.filter( + (org) => + !githubOrgsInstalled.data?.some( + (o) => + o.github_organization.organizationName === + org.data.login && + o.github_organization.connected === true, + ), + ) + .map(({ data: { id, login, avatar_url } }) => ( + { + setValue(currentValue); + setImage(avatar_url); + setOpen(false); + }} + > +
+ + + + {login.slice(0, 2)} + + + {login} +
+
+ ))} + + + + + Add new organization + + +
+
+
+
+
+ + +
+
+
+ + + {(loading || githubOrgsInstalled.isLoading) && ( +
+ {_.range(3).map((i) => ( + + ))} +
+ )} + {!loading && !githubOrgsInstalled.isLoading && ( + + {githubOrgsInstalled.data?.map( + ({ github_organization, github_user }) => ( +
+
+ + + + + + +
+

+ {github_organization.organizationName} +

+ {github_user != null && ( +

+ Enabled by {github_user.githubUsername} on{" "} + {github_organization.createdAt.toLocaleDateString()} +

+ )} +
+
+ + + + + + + { + e.preventDefault(); + window.open( + e.currentTarget.href, + "_blank", + "noopener,noreferrer", + ); + }} + > + Configure + + + { + githubOrgUpdate.mutateAsync({ + id: github_organization.id, + data: { + connected: false, + }, + }); + }} + > + Disconnect + + + +
+ ), + )} +
+ )} +
+ ); +}; diff --git a/apps/nextjs/src/app/[workspaceSlug]/settings/(settings)/workspace/integrations/(integration)/github/page.tsx b/apps/nextjs/src/app/[workspaceSlug]/settings/(settings)/workspace/integrations/(integration)/github/page.tsx index a2df3996..78037835 100644 --- a/apps/nextjs/src/app/[workspaceSlug]/settings/(settings)/workspace/integrations/(integration)/github/page.tsx +++ b/apps/nextjs/src/app/[workspaceSlug]/settings/(settings)/workspace/integrations/(integration)/github/page.tsx @@ -9,7 +9,8 @@ import { Card } from "@ctrlplane/ui/card"; import { env } from "~/env"; import { api } from "~/trpc/react"; -import { GithubJobAgentConfig } from "../../../../../../(job)/job-agents/add/GithubConfig"; +import { GithubConfigFileSync } from "./GithubConfigFile"; +import { GithubOrgConfig } from "./GithubOrgConfig"; const githubAuthUrl = (userId?: string, workspaceSlug?: string) => `${env.GITHUB_URL}/login/oauth/authorize?response_type=code&client_id=${env.NEXT_PUBLIC_GITHUB_BOT_CLIENT_ID}&redirect_uri=${env.BASE_URL}/api/github/${userId}/${workspaceSlug}&state=sLtHqpxQ6FiUtBWJ&scope=repo%2Cread%3Auser`; @@ -35,7 +36,7 @@ export default function GitHubIntegrationPage({

GitHub

- Connect a Github organization to CtrlPlane to configure job agents + Connect a Github organization to Ctrlplane to configure job agents and sync config files.

@@ -50,8 +51,8 @@ export default function GitHubIntegrationPage({

{githubUser.data != null - ? "Your GitHub account is connected to CtrlPlane" - : "Connect your GitHub account to CtrlPlane"} + ? "Your GitHub account is connected to Ctrlplane" + : "Connect your GitHub account to Ctrlplane"}

{githubUser.data == null && ( @@ -69,13 +70,14 @@ export default function GitHubIntegrationPage({ )} - {workspace.data != null && githubUser.data != null && ( - - )} + + + ); } diff --git a/apps/nextjs/src/app/api/github/[userId]/[workspaceSlug]/route.ts b/apps/nextjs/src/app/api/github/[userId]/[workspaceSlug]/route.ts index 6dd6d6a0..f006637f 100644 --- a/apps/nextjs/src/app/api/github/[userId]/[workspaceSlug]/route.ts +++ b/apps/nextjs/src/app/api/github/[userId]/[workspaceSlug]/route.ts @@ -14,8 +14,6 @@ export const GET = async ( const code = searchParams.get("code"); const { userId, workspaceSlug } = params; - console.log({ userId, workspaceSlug }); - const tokenResponse = await fetch( `${env.GITHUB_URL}/login/oauth/access_token`, { diff --git a/packages/api/src/router/github.ts b/packages/api/src/router/github.ts index 2af98e34..257e257a 100644 --- a/packages/api/src/router/github.ts +++ b/packages/api/src/router/github.ts @@ -1,16 +1,22 @@ import { createAppAuth } from "@octokit/auth-app"; import { Octokit } from "@octokit/rest"; import { TRPCError } from "@trpc/server"; +import * as yaml from "js-yaml"; import _ from "lodash"; import { isPresent } from "ts-is-present"; import { z } from "zod"; -import { eq, takeFirstOrNull } from "@ctrlplane/db"; +import { and, eq, inArray, takeFirst, takeFirstOrNull } from "@ctrlplane/db"; import { + deployment, + githubConfigFile, githubOrganization, githubOrganizationInsert, githubUser, + system, + workspace, } from "@ctrlplane/db/schema"; +import { configFile } from "@ctrlplane/validators"; import { env } from "../config"; import { createTRPCRouter, protectedProcedure } from "../trpc"; @@ -133,9 +139,25 @@ const reposRouter = createTRPCRouter({ }), }); +const configFileRouter = createTRPCRouter({ + list: protectedProcedure + .meta({ + access: ({ ctx, input }) => ctx.accessQuery().workspace.id(input), + }) + .input(z.string().uuid()) + .query(({ ctx, input }) => + ctx.db + .select() + .from(githubConfigFile) + .where(eq(githubConfigFile.workspaceId, input)), + ), +}); + export const githubRouter = createTRPCRouter({ user: userRouter, + configFile: configFileRouter, + organizations: createTRPCRouter({ byGithubUserId: protectedProcedure.input(z.number()).query(({ input }) => getOctokit() @@ -180,19 +202,232 @@ export const githubRouter = createTRPCRouter({ ), ), + byWorkspaceId: protectedProcedure + .input(z.string().uuid()) + .query(async ({ ctx, input }) => { + const internalOrgs = await ctx.db + .select() + .from(githubOrganization) + .where(eq(githubOrganization.workspaceId, input)); + + return getOctokit() + .apps.listInstallations({ + headers: { + "X-GitHub-Api-Version": "2022-11-28", + }, + }) + .then(({ data: installations }) => + Promise.all( + installations.filter( + (i) => + i.target_type === "Organization" && + internalOrgs.find((org) => org.installationId === i.id) != + null, + ), + ), + ); + }), + list: protectedProcedure .input(z.string().uuid()) .query(({ ctx, input }) => ctx.db .select() .from(githubOrganization) + .leftJoin( + githubUser, + eq(githubOrganization.addedByUserId, githubUser.userId), + ) .where(eq(githubOrganization.workspaceId, input)), ), create: protectedProcedure .input(githubOrganizationInsert) .mutation(({ ctx, input }) => - ctx.db.insert(githubOrganization).values(input).returning(), + ctx.db.transaction((db) => + db + .insert(githubOrganization) + .values(input) + .returning() + .then(takeFirst) + .then((org) => + getOctokit() + .apps.getInstallation({ + installation_id: org.installationId, + }) + .then(async ({ data: installation }) => { + const installationOctokit = new Octokit({ + authStrategy: createAppAuth, + auth: { + appId: env.GITHUB_BOT_APP_ID, + privateKey: env.GITHUB_BOT_PRIVATE_KEY, + clientId: env.GITHUB_BOT_CLIENT_ID, + clientSecret: env.GITHUB_BOT_CLIENT_SECRET, + installationId: installation.id, + }, + }); + + const installationToken = (await installationOctokit.auth({ + type: "installation", + installationId: installation.id, + })) as { token: string }; + + const configFiles = await Promise.all([ + installationOctokit.search.code({ + q: `org:${org.organizationName} filename:ctrlplane.yaml`, + per_page: 100, + headers: { + "X-GitHub-Api-Version": "2022-11-28", + authorization: `Bearer ${installationToken.token}`, + }, + }), + installationOctokit.search.code({ + q: `org:${org.organizationName} filename:ctrlplane.yaml`, + per_page: 100, + headers: { + "X-GitHub-Api-Version": "2022-11-28", + authorization: `Bearer ${installationToken.token}`, + }, + }), + ]).then((responses) => { + return [ + ...responses[0].data.items, + ...responses[1].data.items, + ]; + }); + + if (configFiles.length === 0) return []; + + const parsedConfigFiles = await Promise.allSettled( + configFiles.map(async (cf) => { + const content = await installationOctokit.repos + .getContent({ + owner: org.organizationName, + repo: cf.repository.name, + path: cf.path, + ref: org.branch, + }) + .then(({ data }) => { + if (!("content" in data)) + throw new Error("Invalid response data"); + return Buffer.from(data.content, "base64").toString( + "utf-8", + ); + }); + + const yamlContent = yaml.load(content); + const parsed = configFile.safeParse(yamlContent); + if (!parsed.success) + throw new TRPCError({ + code: "BAD_REQUEST", + message: "Invalid config file", + }); + + return { + ...cf, + content: parsed.data, + }; + }), + ).then((results) => + results + .map((r) => (r.status === "fulfilled" ? r.value : null)) + .filter(isPresent), + ); + + const deploymentInfo = await db + .select() + .from(system) + .innerJoin(workspace, eq(system.workspaceId, workspace.id)) + .where( + and( + inArray( + system.slug, + parsedConfigFiles + .map((d) => + d.content.deployments.map((d) => d.system), + ) + .flat(), + ), + inArray( + workspace.slug, + parsedConfigFiles + .map((d) => + d.content.deployments.map((d) => d.workspace), + ) + .flat(), + ), + ), + ); + + const insertedConfigFiles = await db + .insert(githubConfigFile) + .values( + parsedConfigFiles.map((d) => ({ + ...d, + workspaceId: org.workspaceId, + organizationId: org.id, + repositoryName: d.repository.name, + branch: org.branch, + })), + ) + .returning(); + + const deployments = parsedConfigFiles + .map((cf) => + cf.content.deployments.map((d) => { + const info = deploymentInfo.find( + (i) => + i.system.slug === d.system && + i.workspace.slug === d.workspace, + ); + if (info == null) + throw new TRPCError({ + code: "NOT_FOUND", + message: "Deployment info not found", + }); + const { system, workspace } = info; + + return { + ...d, + systemId: system.id, + workspaceId: workspace.id, + description: d.description ?? "", + githubConfigFileId: insertedConfigFiles.find( + (icf) => + icf.name === cf.name && + icf.path === cf.path && + icf.repositoryName === cf.repository.name, + )?.id, + }; + }), + ) + .flat(); + + return db.insert(deployment).values(deployments); + }), + ), + ), + ), + + update: protectedProcedure + .input( + z.object({ + id: z.string().uuid(), + data: z.object({ + connected: z.boolean().optional(), + installationId: z.number().optional(), + organizationName: z.string().optional(), + organizationId: z.string().optional(), + addedByUserId: z.string().optional(), + workspaceId: z.string().optional(), + }), + }), + ) + .mutation(({ ctx, input }) => + ctx.db + .update(githubOrganization) + .set(input.data) + .where(eq(githubOrganization.id, input.id)), ), repos: reposRouter, diff --git a/packages/db/src/schema/deployment.ts b/packages/db/src/schema/deployment.ts index 06014150..5bffd25c 100644 --- a/packages/db/src/schema/deployment.ts +++ b/packages/db/src/schema/deployment.ts @@ -4,6 +4,7 @@ import { jsonb, pgTable, text, uniqueIndex, uuid } from "drizzle-orm/pg-core"; import { createInsertSchema } from "drizzle-zod"; import { z } from "zod"; +import { githubConfigFile } from "./github"; import { jobAgent } from "./job-execution"; import { release } from "./release"; import { system } from "./system"; @@ -23,6 +24,10 @@ export const deployment = pgTable( .default("{}") .$type>() .notNull(), + githubConfigFileId: uuid("github_config_file_id").references( + () => githubConfigFile.id, + { onDelete: "cascade" }, + ), }, (t) => ({ uniq: uniqueIndex().on(t.systemId, t.slug) }), ); diff --git a/packages/db/src/schema/github.ts b/packages/db/src/schema/github.ts index 8aec1f50..c157f50b 100644 --- a/packages/db/src/schema/github.ts +++ b/packages/db/src/schema/github.ts @@ -1,5 +1,12 @@ import type { InferSelectModel } from "drizzle-orm"; -import { integer, pgTable, text, timestamp, uuid } from "drizzle-orm/pg-core"; +import { + boolean, + integer, + pgTable, + text, + timestamp, + uuid, +} from "drizzle-orm/pg-core"; import { createInsertSchema } from "drizzle-zod"; import { user } from "./auth"; @@ -26,9 +33,31 @@ export const githubOrganization = pgTable("github_organization", { workspaceId: uuid("workspace_id") .notNull() .references(() => workspace.id, { onDelete: "cascade" }), - createdAt: timestamp("created_at", { withTimezone: true }).defaultNow(), + avatarUrl: text("avatar_url"), + createdAt: timestamp("created_at", { withTimezone: true }) + .notNull() + .defaultNow(), + connected: boolean("connected").notNull().default(true), + branch: text("branch").notNull().default("main"), }); export type GithubOrganization = InferSelectModel; export const githubOrganizationInsert = createInsertSchema(githubOrganization); + +export const githubConfigFile = pgTable("github_config_file", { + id: uuid("id").primaryKey().defaultRandom(), + organizationId: uuid("organization_id") + .notNull() + .references(() => githubOrganization.id, { onDelete: "cascade" }), + repositoryName: text("repository_name").notNull(), + branch: text("branch").notNull().default("main"), + path: text("path").notNull(), + name: text("name").notNull(), + workspaceId: uuid("workspace_id") + .notNull() + .references(() => workspace.id, { onDelete: "cascade" }), + lastSyncedAt: timestamp("last_synced_at", { + withTimezone: true, + }).defaultNow(), +}); diff --git a/packages/validators/src/config-file.ts b/packages/validators/src/config-file.ts new file mode 100644 index 00000000..ec916a69 --- /dev/null +++ b/packages/validators/src/config-file.ts @@ -0,0 +1,13 @@ +import { z } from "zod"; + +export const configFile = z.object({ + deployments: z.array( + z.object({ + name: z.string(), + slug: z.string(), + description: z.string().optional(), + system: z.string(), + workspace: z.string(), + }), + ), +}); diff --git a/packages/validators/src/index.ts b/packages/validators/src/index.ts index 4fb967c3..57ffeb91 100644 --- a/packages/validators/src/index.ts +++ b/packages/validators/src/index.ts @@ -1,8 +1 @@ -import { z } from "zod"; - -export const unused = z.string().describe( - `This lib is currently not used as we use drizzle-zod for simple schemas - But as your application grows and you need other validators to share - with back and frontend, you can put them in here - `, -); +export * from "./config-file";