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.
-
-
-
-
-
-
-
- Github
-
-
-
-
- setValue("github-app")}>
- Github App
-
- Github OAuth
-
-
-
-
-
-
-
- Kubernetes
-
-
-
-
- setValue("kubernetes-job")}>
- Job
-
-
-
-
-
-
-
-
- Google
-
-
-
-
- Workflows
- Cloud Function
-
-
-
-
-
- Circle CI
-
- setValue("webhook")}
- >
-
- Webook
-
-
-
- );
-};
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.
+
+
+
+
+
+
+
+
+ {image !== null && (
+
+
+ {value?.slice(0, 2)}
+
+ )}
+
+
+ {value ?? "Select organization..."}
+
+
+
+
+
+
+
+
+
+ {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
+
+
+
+
+
+
+
+
+
{
+ const existingOrg = githubOrgsInstalled.data?.find(
+ (o) => o.github_organization.organizationName === value,
+ );
+
+ if (existingOrg != null)
+ await githubOrgUpdate.mutateAsync({
+ id: existingOrg.github_organization.id,
+ data: {
+ connected: true,
+ },
+ });
+
+ const org = githubOrgs.data?.find(
+ ({ data }) => data.login === value,
+ );
+
+ if (org == null) return;
+
+ await githubOrgCreate.mutateAsync({
+ installationId: org.installationId,
+ workspaceId: workspaceId ?? "",
+ organizationName: org.data.login,
+ addedByUserId: githubUser?.userId ?? "",
+ avatarUrl: org.data.avatar_url,
+ });
+
+ await jobAgentCreate.mutateAsync({
+ workspaceId: workspaceId ?? "",
+ type: "github-app",
+ name: org.data.login,
+ config: {
+ installationId: org.installationId,
+ login: org.data.login,
+ },
+ });
+
+ await utils.github.organizations.list.invalidate(
+ workspaceId ?? "",
+ );
+ await utils.github.configFile.list.invalidate(
+ workspaceId ?? "",
+ );
+ }}
+ >
+ Save
+
+
+
+
+
+
+ {(loading || githubOrgsInstalled.isLoading) && (
+
+ {_.range(3).map((i) => (
+
+ ))}
+
+ )}
+ {!loading && !githubOrgsInstalled.isLoading && (
+
+ {githubOrgsInstalled.data?.map(
+ ({ github_organization, github_user }) => (
+
+ ),
+ )}
+
+ )}
+
+ );
+};
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";