Skip to content

Commit

Permalink
fix: Refactor gh integration (#288)
Browse files Browse the repository at this point in the history
  • Loading branch information
adityachoudhari26 authored Jan 22, 2025
1 parent 990405e commit 49ef30c
Show file tree
Hide file tree
Showing 22 changed files with 5,243 additions and 537 deletions.
150 changes: 97 additions & 53 deletions apps/event-worker/src/job-dispatch/github.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { and, eq, takeFirstOrNull } from "@ctrlplane/db";
import { db } from "@ctrlplane/db/client";
import {
environment,
githubOrganization,
githubEntity,
job,
releaseJobTrigger,
runbook,
Expand All @@ -18,29 +18,15 @@ import { JobStatus } from "@ctrlplane/validators/jobs";

import { getInstallationOctokit } from "../github-utils.js";

export const dispatchGithubJob = async (je: Job) => {
logger.info(`Dispatching github job ${je.id}...`);

const config = je.jobAgentConfig;
const parsed = configSchema.safeParse(config);
if (!parsed.success) {
logger.error(
`Invalid job agent config for job ${je.id}: ${parsed.error.message}`,
);
await db
.update(job)
.set({
status: JobStatus.InvalidJobAgent,
message: `Invalid job agent config for job ${je.id}: ${parsed.error.message}`,
})
.where(eq(job.id, je.id));
return;
}

const releaseGhOrgResult = await db
const getGithubEntity = async (
jobId: string,
installationId: number,
owner: string,
) => {
const releaseGhEntityPromise = db
.select()
.from(githubOrganization)
.innerJoin(workspace, eq(githubOrganization.workspaceId, workspace.id))
.from(githubEntity)
.innerJoin(workspace, eq(githubEntity.workspaceId, workspace.id))
.innerJoin(system, eq(system.workspaceId, workspace.id))
.innerJoin(environment, eq(environment.systemId, system.id))
.innerJoin(
Expand All @@ -49,43 +35,77 @@ export const dispatchGithubJob = async (je: Job) => {
)
.where(
and(
eq(githubOrganization.installationId, parsed.data.installationId),
eq(githubOrganization.organizationName, parsed.data.owner),
eq(releaseJobTrigger.jobId, je.id),
eq(githubEntity.installationId, installationId),
eq(githubEntity.slug, owner),
eq(releaseJobTrigger.jobId, jobId),
),
)
.then(takeFirstOrNull);

const runbookGhOrgResult = await db
const runbookGhEntityPromise = db
.select()
.from(githubOrganization)
.innerJoin(workspace, eq(githubOrganization.workspaceId, workspace.id))
.from(githubEntity)
.innerJoin(workspace, eq(githubEntity.workspaceId, workspace.id))
.innerJoin(system, eq(system.workspaceId, workspace.id))
.innerJoin(runbook, eq(runbook.systemId, system.id))
.innerJoin(runbookJobTrigger, eq(runbookJobTrigger.runbookId, runbook.id))
.where(
and(
eq(githubOrganization.installationId, parsed.data.installationId),
eq(githubOrganization.organizationName, parsed.data.owner),
eq(runbookJobTrigger.jobId, je.id),
eq(githubEntity.installationId, installationId),
eq(githubEntity.slug, owner),
eq(runbookJobTrigger.jobId, jobId),
),
)
.then(takeFirstOrNull);
const ghOrgResult = releaseGhOrgResult ?? runbookGhOrgResult;

if (ghOrgResult == null) {
const [releaseGhEntityResult, runbookGhEntityResult] = await Promise.all([
releaseGhEntityPromise,
runbookGhEntityPromise,
]);

return (
releaseGhEntityResult?.github_entity ?? runbookGhEntityResult?.github_entity
);
};

export const dispatchGithubJob = async (je: Job) => {
logger.info(`Dispatching github job ${je.id}...`);

const config = je.jobAgentConfig;
const parsed = configSchema.safeParse(config);
if (!parsed.success) {
logger.error(
`Invalid job agent config for job ${je.id}: ${parsed.error.message}`,
);
await db
.update(job)
.set({
status: JobStatus.InvalidJobAgent,
message: `Invalid job agent config for job ${je.id}: ${parsed.error.message}`,
})
.where(eq(job.id, je.id));
return;
}

const { data: parsedConfig } = parsed;

const ghEntity = await getGithubEntity(
je.id,
parsedConfig.installationId,
parsedConfig.owner,
);
if (ghEntity == null) {
await db
.update(job)
.set({
status: JobStatus.InvalidIntegration,
message: `GitHub organization not found for job ${je.id}`,
message: `GitHub entity not found for job ${je.id}`,
})
.where(eq(job.id, je.id));
return;
}
const ghOrg = ghOrgResult.github_organization;

const octokit = getInstallationOctokit(parsed.data.installationId);
const octokit = getInstallationOctokit(ghEntity.installationId);
if (octokit == null) {
logger.error(`GitHub bot not configured for job ${je.id}`);
await db
Expand All @@ -100,28 +120,52 @@ export const dispatchGithubJob = async (je: Job) => {

const installationToken = (await octokit.auth({
type: "installation",
installationId: parsed.data.installationId,
installationId: parsedConfig.installationId,
})) as { token: string };

const headers = {
"X-GitHub-Api-Version": "2022-11-28",
authorization: `Bearer ${installationToken.token}`,
};

const ref =
parsedConfig.ref ??
(await octokit.rest.repos
.get({ ...parsedConfig, headers })
.then((r) => r.data.default_branch)
.catch((e) => {
logger.error(`Failed to get ref for github action job ${je.id}`, {
error: e,
});
return null;
}));

if (ref == null) {
logger.error(`Failed to get ref for github action job ${je.id}`);
await db
.update(job)
.set({
status: JobStatus.InvalidJobAgent,
message: "Failed to get ref for github action job",
})
.where(eq(job.id, je.id));
return;
}

logger.info(`Creating workflow dispatch for job ${je.id}...`, {
owner: parsed.data.owner,
repo: parsed.data.repo,
workflow_id: parsed.data.workflowId,
ref: ghOrg.branch,
inputs: {
job_id: je.id,
},
owner: parsedConfig.owner,
repo: parsedConfig.repo,
workflow_id: parsedConfig.workflowId,
ref,
inputs: { job_id: je.id },
});

await octokit.actions.createWorkflowDispatch({
owner: parsed.data.owner,
repo: parsed.data.repo,
workflow_id: parsed.data.workflowId,
ref: ghOrg.branch,
owner: parsedConfig.owner,
repo: parsedConfig.repo,
workflow_id: parsedConfig.workflowId,
ref,
inputs: { job_id: je.id },
headers: {
"X-GitHub-Api-Version": "2022-11-28",
authorization: `Bearer ${installationToken.token}`,
},
headers,
});
};
Original file line number Diff line number Diff line change
Expand Up @@ -235,11 +235,6 @@ export const CreateDeploymentDialog: React.FC<{
jobAgent={selectedJobAgent}
value={value ?? {}}
onChange={onChange}
githubFormStyleConfig={{
className:
"flex flex-col gap-2 items-center w-[450px]",
buttonWidth: "w-[450px]",
}}
/>
</FormControl>
<FormMessage />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -206,10 +206,6 @@ export const CreateHookDialog: React.FC<CreateHookDialogProps> = ({
jobAgent={jobAgent}
value={value ?? {}}
onChange={onChange}
githubFormStyleConfig={{
className: "flex flex-col gap-2 items-center w-[450px]",
buttonWidth: "w-[450px]",
}}
/>
</FormControl>
<FormMessage />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -210,10 +210,6 @@ export const EditHookDialog: React.FC<EditHookDialogProps> = ({
jobAgent={jobAgent}
value={value ?? {}}
onChange={onChange}
githubFormStyleConfig={{
className: "flex flex-col gap-2 items-center w-[450px]",
buttonWidth: "w-[450px]",
}}
/>
</FormControl>
<FormMessage />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -188,7 +188,6 @@ export const CreateRunbook: React.FC<{
workspace={workspace}
value={value}
onChange={onChange}
githubFormStyleConfig={{ buttonWidth: "w-72" }}
/>
</FormControl>
<FormMessage />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,21 +11,22 @@ import {
} from "@ctrlplane/ui/dropdown-menu";

import { DisconnectDropdownActionButton } from "./DisconnectDropdownActionButton";
import { GithubRemoveOrgDialog } from "./GithubRemoveOrgDialog";
import { GithubRemoveEntityDialog } from "./GithubRemoveEntityDialog";

type OrgActionDropdownProps = {
githubConfig: {
url: string;
botName: string;
clientId: string;
};
org: schema.GithubOrganization;
type EntityActionDropdownProps = {
githubConfig: { url: string; botName: string; clientId: string };
entity: schema.GithubEntity;
};

export const OrgActionDropdown: React.FC<OrgActionDropdownProps> = ({
export const EntityActionDropdown: React.FC<EntityActionDropdownProps> = ({
githubConfig,
org,
entity,
}) => {
const { type, slug, installationId } = entity;
const link =
type === "organization"
? `${githubConfig.url}/organizations/${slug}/settings/installations/${installationId}`
: `${githubConfig.url}/settings/installations/${installationId}`;
return (
<DropdownMenu>
<DropdownMenuTrigger asChild>
Expand All @@ -36,19 +37,15 @@ export const OrgActionDropdown: React.FC<OrgActionDropdownProps> = ({
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent>
<Link
href={`${githubConfig.url}/organizations/${org.organizationName}/settings/installations/${org.installationId}`}
target="_blank"
rel="noopener noreferrer"
>
<Link href={link} target="_blank" rel="noopener noreferrer">
<DropdownMenuItem>
Configure <IconExternalLink className="ml-2 h-4 w-4" />
</DropdownMenuItem>
</Link>

<GithubRemoveOrgDialog githubOrganization={org}>
<GithubRemoveEntityDialog githubEntity={entity}>
<DisconnectDropdownActionButton />
</GithubRemoveOrgDialog>
</GithubRemoveEntityDialog>
</DropdownMenuContent>
</DropdownMenu>
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,26 +17,26 @@ import {
DialogTrigger,
} from "@ctrlplane/ui/dialog";

import type { GithubOrg } from "./SelectPreconnectedOrgDialogContent";
import { SelectPreconnectedOrgDialogContent } from "./SelectPreconnectedOrgDialogContent";
import { SelectPreconnectedEntityDialogContent } from "./SelectPreconnectedEntityDialogContent";

type GithubAddOrgDialogProps = {
type GithubAddEntityDialogProps = {
githubUser: GithubUser;
children: React.ReactNode;
githubConfig: {
url: string;
botName: string;
clientId: string;
};
validOrgsToAdd: GithubOrg[];
githubConfig: { url: string; botName: string; clientId: string };
validEntitiesToAdd: {
installationId: number;
type: "user" | "organization";
slug: string;
avatarUrl: string;
}[];
workspaceId: string;
};

export const GithubAddOrgDialog: React.FC<GithubAddOrgDialogProps> = ({
export const GithubAddEntityDialog: React.FC<GithubAddEntityDialogProps> = ({
githubUser,
children,
githubConfig,
validOrgsToAdd,
validEntitiesToAdd,
workspaceId,
}) => {
const [dialogStep, setDialogStep] = useState<"choose-org" | "pre-connected">(
Expand All @@ -52,15 +52,15 @@ export const GithubAddOrgDialog: React.FC<GithubAddOrgDialogProps> = ({
<>
<DialogHeader>
<DialogTitle>Connect a new Organization</DialogTitle>
{validOrgsToAdd.length === 0 && (
{validEntitiesToAdd.length === 0 && (
<DialogDescription>
Install the ctrlplane Github app on an organization to connect
it to your workspace.
</DialogDescription>
)}
</DialogHeader>

{validOrgsToAdd.length > 0 && (
{validEntitiesToAdd.length > 0 && (
<Alert variant="secondary">
<IconBulb className="h-5 w-5" />
<AlertTitle>Connect an organization</AlertTitle>
Expand Down Expand Up @@ -99,7 +99,7 @@ export const GithubAddOrgDialog: React.FC<GithubAddOrgDialogProps> = ({
<Button variant="outline">Connect new organization</Button>
</Link>

{validOrgsToAdd.length > 0 && (
{validEntitiesToAdd.length > 0 && (
<div className="flex flex-grow justify-end">
<Button
className="w-fit"
Expand All @@ -115,8 +115,8 @@ export const GithubAddOrgDialog: React.FC<GithubAddOrgDialogProps> = ({
)}

{dialogStep === "pre-connected" && (
<SelectPreconnectedOrgDialogContent
githubOrgs={validOrgsToAdd}
<SelectPreconnectedEntityDialogContent
githubEntities={validEntitiesToAdd}
githubUser={githubUser}
workspaceId={workspaceId}
onNavigateBack={() => setDialogStep("choose-org")}
Expand Down
Loading

0 comments on commit 49ef30c

Please sign in to comment.