Files
dokploy/packages/server/src/services/github.ts

349 lines
8.3 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { db } from "@dokploy/server/db";
import {
type apiCreateGithub,
github,
gitProvider,
} from "@dokploy/server/db/schema";
import { TRPCError } from "@trpc/server";
import { eq } from "drizzle-orm";
import type { z } from "zod";
import { authGithub } from "../utils/providers/github";
import { updatePreviewDeployment } from "./preview-deployment";
export type Github = typeof github.$inferSelect;
export const createGithub = async (
input: z.infer<typeof apiCreateGithub>,
organizationId: string,
userId: string,
) => {
return await db.transaction(async (tx) => {
const newGitProvider = await tx
.insert(gitProvider)
.values({
providerType: "github",
organizationId: organizationId,
name: input.name,
userId: userId,
})
.returning()
.then((response) => response[0]);
if (!newGitProvider) {
throw new TRPCError({
code: "BAD_REQUEST",
message: "Error creating the Git provider",
});
}
return await tx
.insert(github)
.values({
...input,
gitProviderId: newGitProvider?.gitProviderId,
})
.returning()
.then((response) => response[0]);
});
};
export const findGithubById = async (githubId: string) => {
const githubProviderResult = await db.query.github.findFirst({
where: eq(github.githubId, githubId),
with: {
gitProvider: true,
},
});
if (!githubProviderResult) {
throw new TRPCError({
code: "NOT_FOUND",
message: "Github Provider not found",
});
}
return githubProviderResult;
};
export const updateGithub = async (
githubId: string,
input: Partial<Github>,
) => {
return await db
.update(github)
.set({
...input,
})
.where(eq(github.githubId, githubId))
.returning()
.then((response) => response[0]);
};
export const getIssueComment = (
appName: string,
status: "success" | "error" | "running" | "initializing",
previewDomain: string,
) => {
let statusMessage = "";
if (status === "success") {
statusMessage = "✅ Done";
} else if (status === "error") {
statusMessage = "❌ Failed";
} else if (status === "initializing") {
statusMessage = "🔄 Building";
} else {
statusMessage = "🔄 Building";
}
const finished = `
| Name | Status | Preview | Updated (UTC) |
|------------|--------------|-------------------------------------|-----------------------|
| ${appName} | ${statusMessage} | [Preview URL](${previewDomain}) | ${new Date().toISOString()} |
`;
return finished;
};
interface CommentExists {
owner: string;
repository: string;
comment_id: number;
githubId: string;
}
export const issueCommentExists = async ({
owner,
repository,
comment_id,
githubId,
}: CommentExists) => {
const github = await findGithubById(githubId);
const octokit = authGithub(github);
try {
await octokit.rest.issues.getComment({
owner: owner || "",
repo: repository || "",
comment_id: comment_id,
});
return true;
} catch {
return false;
}
};
interface Comment {
owner: string;
repository: string;
issue_number: string;
body: string;
comment_id: number;
githubId: string;
}
export const updateIssueComment = async ({
owner,
repository,
issue_number,
body,
comment_id,
githubId,
}: Comment) => {
const github = await findGithubById(githubId);
const octokit = authGithub(github);
await octokit.rest.issues.updateComment({
owner: owner || "",
repo: repository || "",
issue_number: issue_number,
body,
comment_id: comment_id,
});
};
interface CommentCreate {
appName: string;
owner: string;
repository: string;
issue_number: string;
previewDomain: string;
githubId: string;
previewDeploymentId: string;
}
export const createPreviewDeploymentComment = async ({
owner,
repository,
issue_number,
previewDomain,
appName,
githubId,
previewDeploymentId,
}: CommentCreate) => {
const github = await findGithubById(githubId);
const octokit = authGithub(github);
const runningComment = getIssueComment(
appName,
"initializing",
previewDomain,
);
const issue = await octokit.rest.issues.createComment({
owner: owner || "",
repo: repository || "",
issue_number: Number.parseInt(issue_number),
body: `### Dokploy Preview Deployment\n\n${runningComment}`,
});
return await updatePreviewDeployment(previewDeploymentId, {
pullRequestCommentId: `${issue.data.id}`,
}).then((response) => response[0]);
};
/**
* Generate security notification message for blocked PR deployments
*/
export const getSecurityBlockedMessage = (
prAuthor: string,
repositoryName: string,
permission: string | null,
) => {
return `### 🚨 Preview Deployment Blocked - Security Protection
**Your pull request was blocked from triggering preview deployments**
#### Why was this blocked?
- **User**: \`${prAuthor}\`
- **Repository**: \`${repositoryName}\`
- **Permission Level**: \`${permission || "none"}\`
- **Required Level**: \`write\`, \`maintain\`, or \`admin\`
#### How to resolve this:
**Option 1: Get Collaborator Access (Recommended)**
Ask a repository maintainer to invite you as a collaborator with **write permissions** or higher.
**Option 2: Request Permission Override**
Ask a repository administrator to disable security validation for this specific application if appropriate.
#### For Repository Administrators:
To disable this security check (⚠️ **not recommended for public repositories**):
Enter to preview settings and disable the security check.
---
*This security measure protects against malicious code execution in preview deployments. Only trusted collaborators should have the ability to trigger deployments.*
<details>
<summary>🛡️ Learn more about this security feature</summary>
This protection prevents unauthorized users from:
- Executing malicious code on the deployment server
- Accessing environment variables and secrets
- Potentially compromising the infrastructure
Preview deployments are powerful but require trust. Only users with repository write access can trigger them.
</details>`;
};
/**
* Check if a security notification comment already exists on a GitHub PR
* This prevents creating duplicate security comments on subsequent pushes
*/
export const hasExistingSecurityComment = async ({
owner,
repository,
prNumber,
githubId,
}: {
owner: string;
repository: string;
prNumber: number;
githubId: string;
}): Promise<boolean> => {
try {
const github = await findGithubById(githubId);
const octokit = authGithub(github);
// Get all comments for this PR
const { data: comments } = await octokit.rest.issues.listComments({
owner,
repo: repository,
issue_number: prNumber,
});
// Check if any comment contains our security notification marker
const securityCommentExists = comments.some((comment) =>
comment.body?.includes(
"🚨 Preview Deployment Blocked - Security Protection",
),
);
return securityCommentExists;
} catch (error) {
console.error(
`❌ Failed to check existing comments on PR #${prNumber}:`,
error,
);
// If we can't check, assume no comment exists to avoid blocking functionality
return false;
}
};
/**
* Create a security notification comment on a GitHub PR
*/
export const createSecurityBlockedComment = async ({
owner,
repository,
prNumber,
prAuthor,
permission,
githubId,
}: {
owner: string;
repository: string;
prNumber: number;
prAuthor: string;
permission: string | null;
githubId: string;
}) => {
try {
// Check if a security comment already exists to prevent duplicates
const commentExists = await hasExistingSecurityComment({
owner,
repository,
prNumber,
githubId,
});
if (commentExists) {
console.log(
` Security notification comment already exists on PR #${prNumber}, skipping duplicate`,
);
return null;
}
const github = await findGithubById(githubId);
const octokit = authGithub(github);
const securityMessage = getSecurityBlockedMessage(
prAuthor,
repository,
permission,
);
const issue = await octokit.rest.issues.createComment({
owner,
repo: repository,
issue_number: prNumber,
body: securityMessage,
});
console.log(
`✅ Security notification comment created on PR #${prNumber}: ${issue.data.html_url}`,
);
return issue.data;
} catch (error) {
console.error(
`❌ Failed to create security comment on PR #${prNumber}:`,
error,
);
// Don't throw error - security comment is nice-to-have, not critical
return null;
}
};