mirror of
https://github.com/Dokploy/dokploy
synced 2025-06-26 18:27:59 +00:00
feat(gitea): Added Gitea Repo Integration
This commit is contained in:
@@ -15,6 +15,7 @@ import { deployments } from "./deployment";
|
||||
import { domains } from "./domain";
|
||||
import { github } from "./github";
|
||||
import { gitlab } from "./gitlab";
|
||||
import { gitea } from "./gitea";
|
||||
import { mounts } from "./mount";
|
||||
import { ports } from "./port";
|
||||
import { previewDeployments } from "./preview-deployments";
|
||||
@@ -33,6 +34,7 @@ export const sourceType = pgEnum("sourceType", [
|
||||
"github",
|
||||
"gitlab",
|
||||
"bitbucket",
|
||||
"gitea",
|
||||
"drop",
|
||||
]);
|
||||
|
||||
@@ -152,6 +154,13 @@ export const applications = pgTable("application", {
|
||||
gitlabBranch: text("gitlabBranch"),
|
||||
gitlabBuildPath: text("gitlabBuildPath").default("/"),
|
||||
gitlabPathNamespace: text("gitlabPathNamespace"),
|
||||
// Gitea
|
||||
giteaProjectId: integer("giteaProjectId"),
|
||||
giteaRepository: text("giteaRepository"),
|
||||
giteaOwner: text("giteaOwner"),
|
||||
giteaBranch: text("giteaBranch"),
|
||||
giteaBuildPath: text("giteaBuildPath").default("/"),
|
||||
giteaPathNamespace: text("giteaPathNamespace"),
|
||||
// Bitbucket
|
||||
bitbucketRepository: text("bitbucketRepository"),
|
||||
bitbucketOwner: text("bitbucketOwner"),
|
||||
@@ -209,6 +218,9 @@ export const applications = pgTable("application", {
|
||||
gitlabId: text("gitlabId").references(() => gitlab.gitlabId, {
|
||||
onDelete: "set null",
|
||||
}),
|
||||
giteaId: text("giteaId").references(() => gitea.giteaId, {
|
||||
onDelete: "set null",
|
||||
}),
|
||||
bitbucketId: text("bitbucketId").references(() => bitbucket.bitbucketId, {
|
||||
onDelete: "set null",
|
||||
}),
|
||||
@@ -246,6 +258,10 @@ export const applicationsRelations = relations(
|
||||
fields: [applications.gitlabId],
|
||||
references: [gitlab.gitlabId],
|
||||
}),
|
||||
gitea: one(gitea, {
|
||||
fields: [applications.giteaId],
|
||||
references: [gitea.giteaId],
|
||||
}),
|
||||
bitbucket: one(bitbucket, {
|
||||
fields: [applications.bitbucketId],
|
||||
references: [bitbucket.bitbucketId],
|
||||
@@ -376,7 +392,7 @@ const createSchema = createInsertSchema(applications, {
|
||||
customGitUrl: z.string().optional(),
|
||||
buildPath: z.string().optional(),
|
||||
projectId: z.string(),
|
||||
sourceType: z.enum(["github", "docker", "git"]).optional(),
|
||||
sourceType: z.enum(["github", "docker", "git", "gitlab", "bitbucket", "gitea", "drop"]).optional(),
|
||||
applicationStatus: z.enum(["idle", "running", "done", "error"]),
|
||||
buildType: z.enum([
|
||||
"dockerfile",
|
||||
@@ -475,6 +491,19 @@ export const apiSaveBitbucketProvider = createSchema
|
||||
})
|
||||
.required();
|
||||
|
||||
export const apiSaveGiteaProvider = createSchema
|
||||
.pick({
|
||||
applicationId: true,
|
||||
giteaBranch: true,
|
||||
giteaBuildPath: true,
|
||||
giteaOwner: true,
|
||||
giteaRepository: true,
|
||||
giteaId: true,
|
||||
giteaProjectId: true,
|
||||
giteaPathNamespace: true,
|
||||
})
|
||||
.required();
|
||||
|
||||
export const apiSaveDockerProvider = createSchema
|
||||
.pick({
|
||||
dockerImage: true,
|
||||
@@ -518,4 +547,4 @@ export const apiUpdateApplication = createSchema
|
||||
.extend({
|
||||
applicationId: z.string().min(1),
|
||||
})
|
||||
.omit({ serverId: true });
|
||||
.omit({ serverId: true });
|
||||
@@ -14,12 +14,14 @@ import { server } from "./server";
|
||||
import { applicationStatus } from "./shared";
|
||||
import { sshKeys } from "./ssh-key";
|
||||
import { generateAppName } from "./utils";
|
||||
import { gitea } from "./gitea";
|
||||
|
||||
export const sourceTypeCompose = pgEnum("sourceTypeCompose", [
|
||||
"git",
|
||||
"github",
|
||||
"gitlab",
|
||||
"bitbucket",
|
||||
"gitea",
|
||||
"raw",
|
||||
]);
|
||||
|
||||
@@ -55,6 +57,10 @@ export const compose = pgTable("compose", {
|
||||
bitbucketRepository: text("bitbucketRepository"),
|
||||
bitbucketOwner: text("bitbucketOwner"),
|
||||
bitbucketBranch: text("bitbucketBranch"),
|
||||
// Gitea
|
||||
giteaRepository: text("giteaRepository"),
|
||||
giteaOwner: text("giteaOwner"),
|
||||
giteaBranch: text("giteaBranch"),
|
||||
// Git
|
||||
customGitUrl: text("customGitUrl"),
|
||||
customGitBranch: text("customGitBranch"),
|
||||
@@ -87,6 +93,9 @@ export const compose = pgTable("compose", {
|
||||
bitbucketId: text("bitbucketId").references(() => bitbucket.bitbucketId, {
|
||||
onDelete: "set null",
|
||||
}),
|
||||
giteaId: text("giteaId").references(() => gitea.giteaId, {
|
||||
onDelete: "set null",
|
||||
}),
|
||||
serverId: text("serverId").references(() => server.serverId, {
|
||||
onDelete: "cascade",
|
||||
}),
|
||||
@@ -116,6 +125,10 @@ export const composeRelations = relations(compose, ({ one, many }) => ({
|
||||
fields: [compose.bitbucketId],
|
||||
references: [bitbucket.bitbucketId],
|
||||
}),
|
||||
gitea: one(gitea, {
|
||||
fields: [compose.giteaId],
|
||||
references: [gitea.giteaId],
|
||||
}),
|
||||
server: one(server, {
|
||||
fields: [compose.serverId],
|
||||
references: [server.serverId],
|
||||
|
||||
@@ -7,11 +7,13 @@ import { organization } from "./account";
|
||||
import { bitbucket } from "./bitbucket";
|
||||
import { github } from "./github";
|
||||
import { gitlab } from "./gitlab";
|
||||
import { gitea } from "./gitea";
|
||||
|
||||
export const gitProviderType = pgEnum("gitProviderType", [
|
||||
"github",
|
||||
"gitlab",
|
||||
"bitbucket",
|
||||
"gitea",
|
||||
]);
|
||||
|
||||
export const gitProvider = pgTable("git_provider", {
|
||||
@@ -42,6 +44,10 @@ export const gitProviderRelations = relations(gitProvider, ({ one }) => ({
|
||||
fields: [gitProvider.gitProviderId],
|
||||
references: [bitbucket.gitProviderId],
|
||||
}),
|
||||
gitea: one(gitea, {
|
||||
fields: [gitProvider.gitProviderId],
|
||||
references: [gitea.gitProviderId],
|
||||
}),
|
||||
organization: one(organization, {
|
||||
fields: [gitProvider.organizationId],
|
||||
references: [organization.id],
|
||||
|
||||
96
packages/server/src/db/schema/gitea.ts
Normal file
96
packages/server/src/db/schema/gitea.ts
Normal file
@@ -0,0 +1,96 @@
|
||||
import { relations } from "drizzle-orm";
|
||||
import { integer, pgTable, text } from "drizzle-orm/pg-core";
|
||||
import { createInsertSchema } from "drizzle-zod";
|
||||
import { nanoid } from "nanoid";
|
||||
import { z } from "zod";
|
||||
import { gitProvider } from "./git-provider";
|
||||
|
||||
// Gitea table definition
|
||||
export const gitea = pgTable("gitea", {
|
||||
giteaId: text("giteaId")
|
||||
.notNull()
|
||||
.primaryKey()
|
||||
.$defaultFn(() => nanoid()), // Using nanoid for unique ID
|
||||
giteaUrl: text("giteaUrl").default("https://gitea.com").notNull(), // Default URL for Gitea
|
||||
redirectUri: text("redirect_uri"),
|
||||
clientId: text("client_id"),
|
||||
clientSecret: text("client_secret"),
|
||||
gitProviderId: text("gitProviderId")
|
||||
.notNull()
|
||||
.references(() => gitProvider.gitProviderId, { onDelete: "cascade" }),
|
||||
giteaUsername: text("gitea_username"),
|
||||
accessToken: text("access_token"),
|
||||
refreshToken: text("refresh_token"),
|
||||
expiresAt: integer("expires_at"),
|
||||
scopes: text("scopes").default('repo,repo:status,read:user,read:org'),
|
||||
lastAuthenticatedAt: integer("last_authenticated_at"),
|
||||
});
|
||||
|
||||
// Gitea relations with gitProvider
|
||||
export const giteaProviderRelations = relations(gitea, ({ one }) => ({
|
||||
gitProvider: one(gitProvider, {
|
||||
fields: [gitea.gitProviderId],
|
||||
references: [gitProvider.gitProviderId],
|
||||
}),
|
||||
}));
|
||||
|
||||
// Create schema for Gitea
|
||||
const createSchema = createInsertSchema(gitea);
|
||||
|
||||
// API schema for creating a Gitea instance
|
||||
export const apiCreateGitea = createSchema.extend({
|
||||
clientId: z.string().optional(),
|
||||
clientSecret: z.string().optional(),
|
||||
gitProviderId: z.string().optional(),
|
||||
redirectUri: z.string().optional(),
|
||||
name: z.string().min(1),
|
||||
giteaUrl: z.string().min(1),
|
||||
giteaUsername: z.string().optional(),
|
||||
accessToken: z.string().optional(),
|
||||
refreshToken: z.string().optional(),
|
||||
expiresAt: z.number().optional(),
|
||||
organizationName: z.string().optional(),
|
||||
scopes: z.string().optional(),
|
||||
lastAuthenticatedAt: z.number().optional(),
|
||||
});
|
||||
|
||||
// API schema for finding one Gitea instance
|
||||
export const apiFindOneGitea = createSchema
|
||||
.extend({
|
||||
giteaId: z.string().min(1),
|
||||
})
|
||||
.pick({ giteaId: true });
|
||||
|
||||
// API schema for testing Gitea connection
|
||||
export const apiGiteaTestConnection = createSchema
|
||||
.extend({
|
||||
organizationName: z.string().optional(),
|
||||
})
|
||||
.pick({ giteaId: true, organizationName: true });
|
||||
|
||||
export type ApiGiteaTestConnection = z.infer<typeof apiGiteaTestConnection>;
|
||||
|
||||
// API schema for finding branches in Gitea
|
||||
export const apiFindGiteaBranches = z.object({
|
||||
id: z.number().optional(),
|
||||
owner: z.string().min(1),
|
||||
repositoryName: z.string().min(1),
|
||||
giteaId: z.string().optional(),
|
||||
});
|
||||
|
||||
// API schema for updating Gitea instance
|
||||
export const apiUpdateGitea = createSchema.extend({
|
||||
clientId: z.string().optional(),
|
||||
clientSecret: z.string().optional(),
|
||||
redirectUri: z.string().optional(),
|
||||
name: z.string().min(1),
|
||||
giteaId: z.string().min(1),
|
||||
giteaUrl: z.string().min(1),
|
||||
giteaUsername: z.string().optional(),
|
||||
accessToken: z.string().optional(),
|
||||
refreshToken: z.string().optional(),
|
||||
expiresAt: z.number().optional(),
|
||||
organizationName: z.string().optional(),
|
||||
scopes: z.string().optional(),
|
||||
lastAuthenticatedAt: z.number().optional(),
|
||||
});
|
||||
@@ -25,6 +25,7 @@ export * from "./git-provider";
|
||||
export * from "./bitbucket";
|
||||
export * from "./github";
|
||||
export * from "./gitlab";
|
||||
export * from "./gitea";
|
||||
export * from "./server";
|
||||
export * from "./utils";
|
||||
export * from "./preview-deployments";
|
||||
|
||||
@@ -46,6 +46,7 @@ enum gitProviderType {
|
||||
github
|
||||
gitlab
|
||||
bitbucket
|
||||
gitea
|
||||
}
|
||||
|
||||
enum mountType {
|
||||
@@ -98,6 +99,7 @@ enum sourceType {
|
||||
github
|
||||
gitlab
|
||||
bitbucket
|
||||
gitea
|
||||
drop
|
||||
}
|
||||
|
||||
@@ -106,6 +108,7 @@ enum sourceTypeCompose {
|
||||
github
|
||||
gitlab
|
||||
bitbucket
|
||||
gitea
|
||||
raw
|
||||
}
|
||||
|
||||
@@ -205,6 +208,7 @@ table application {
|
||||
githubId text
|
||||
gitlabId text
|
||||
bitbucketId text
|
||||
giteaId text
|
||||
serverId text
|
||||
}
|
||||
|
||||
@@ -279,6 +283,9 @@ table compose {
|
||||
bitbucketRepository text
|
||||
bitbucketOwner text
|
||||
bitbucketBranch text
|
||||
giteaRepository text
|
||||
giteaOwner text
|
||||
giteaBranch text
|
||||
customGitUrl text
|
||||
customGitBranch text
|
||||
customGitSSHKeyId text
|
||||
@@ -293,6 +300,7 @@ table compose {
|
||||
githubId text
|
||||
gitlabId text
|
||||
bitbucketId text
|
||||
giteaId text
|
||||
serverId text
|
||||
}
|
||||
|
||||
@@ -388,6 +396,20 @@ table gitlab {
|
||||
gitProviderId text [not null]
|
||||
}
|
||||
|
||||
table gitea {
|
||||
giteaId text [pk, not null]
|
||||
giteaUrl text [not null, default: 'https://gitea.com']
|
||||
redirect_uri text
|
||||
client_id text [not null]
|
||||
client_secret text [not null]
|
||||
access_token text
|
||||
refresh_token text
|
||||
expires_at integer
|
||||
gitProviderId text [not null]
|
||||
scopes text [default: 'repo,repo:status,read:user,read:org']
|
||||
last_authenticated_at integer
|
||||
}
|
||||
|
||||
table gotify {
|
||||
gotifyId text [pk, not null]
|
||||
serverUrl text [not null]
|
||||
@@ -819,6 +841,8 @@ ref: github.gitProviderId - git_provider.gitProviderId
|
||||
|
||||
ref: gitlab.gitProviderId - git_provider.gitProviderId
|
||||
|
||||
ref: gitea.gitProviderId - git_provider.gitProviderId
|
||||
|
||||
ref: git_provider.userId - user.id
|
||||
|
||||
ref: mariadb.projectId > project.projectId
|
||||
|
||||
@@ -28,6 +28,7 @@ export * from "./services/git-provider";
|
||||
export * from "./services/bitbucket";
|
||||
export * from "./services/github";
|
||||
export * from "./services/gitlab";
|
||||
export * from "./services/gitea";
|
||||
export * from "./services/server";
|
||||
export * from "./services/application";
|
||||
|
||||
@@ -89,6 +90,7 @@ export * from "./utils/providers/docker";
|
||||
export * from "./utils/providers/git";
|
||||
export * from "./utils/providers/github";
|
||||
export * from "./utils/providers/gitlab";
|
||||
export * from "./utils/providers/gitea";
|
||||
export * from "./utils/providers/raw";
|
||||
|
||||
export * from "./utils/servers/remote-docker";
|
||||
|
||||
@@ -34,6 +34,10 @@ import {
|
||||
cloneGitlabRepository,
|
||||
getGitlabCloneCommand,
|
||||
} from "@dokploy/server/utils/providers/gitlab";
|
||||
import {
|
||||
cloneGiteaRepository,
|
||||
getGiteaCloneCommand,
|
||||
} from "@dokploy/server/utils/providers/gitea";
|
||||
import { createTraefikConfig } from "@dokploy/server/utils/traefik/application";
|
||||
import { TRPCError } from "@trpc/server";
|
||||
import { eq } from "drizzle-orm";
|
||||
@@ -111,6 +115,7 @@ export const findApplicationById = async (applicationId: string) => {
|
||||
gitlab: true,
|
||||
github: true,
|
||||
bitbucket: true,
|
||||
gitea: true,
|
||||
server: true,
|
||||
previewDeployments: true,
|
||||
},
|
||||
@@ -197,6 +202,9 @@ export const deployApplication = async ({
|
||||
} else if (application.sourceType === "gitlab") {
|
||||
await cloneGitlabRepository(application, deployment.logPath);
|
||||
await buildApplication(application, deployment.logPath);
|
||||
} else if (application.sourceType === "gitea") {
|
||||
await cloneGiteaRepository(application, deployment.logPath);
|
||||
await buildApplication(application, deployment.logPath);
|
||||
} else if (application.sourceType === "bitbucket") {
|
||||
await cloneBitbucketRepository(application, deployment.logPath);
|
||||
await buildApplication(application, deployment.logPath);
|
||||
@@ -325,6 +333,11 @@ export const deployRemoteApplication = async ({
|
||||
application,
|
||||
deployment.logPath,
|
||||
);
|
||||
} else if (application.sourceType === "gitea") {
|
||||
command += await getGiteaCloneCommand(
|
||||
application,
|
||||
deployment.logPath,
|
||||
);
|
||||
} else if (application.sourceType === "git") {
|
||||
command += await getCustomGitCloneCommand(
|
||||
application,
|
||||
|
||||
@@ -37,6 +37,10 @@ import {
|
||||
cloneGitlabRepository,
|
||||
getGitlabCloneCommand,
|
||||
} from "@dokploy/server/utils/providers/gitlab";
|
||||
import {
|
||||
cloneGiteaRepository,
|
||||
getGiteaCloneCommand,
|
||||
} from "@dokploy/server/utils/providers/gitea";
|
||||
import {
|
||||
createComposeFile,
|
||||
getCreateComposeFileCommand,
|
||||
@@ -125,6 +129,7 @@ export const findComposeById = async (composeId: string) => {
|
||||
github: true,
|
||||
gitlab: true,
|
||||
bitbucket: true,
|
||||
gitea: true,
|
||||
server: true,
|
||||
},
|
||||
});
|
||||
@@ -232,9 +237,11 @@ export const deployCompose = async ({
|
||||
await cloneBitbucketRepository(compose, deployment.logPath, true);
|
||||
} else if (compose.sourceType === "git") {
|
||||
await cloneGitRepository(compose, deployment.logPath, true);
|
||||
} else if (compose.sourceType === "gitea") {
|
||||
await cloneGiteaRepository(compose, deployment.logPath, true);
|
||||
} else if (compose.sourceType === "raw") {
|
||||
await createComposeFile(compose, deployment.logPath);
|
||||
}
|
||||
}
|
||||
await buildCompose(compose, deployment.logPath);
|
||||
await updateDeploymentStatus(deployment.deploymentId, "done");
|
||||
await updateCompose(composeId, {
|
||||
@@ -364,6 +371,12 @@ export const deployRemoteCompose = async ({
|
||||
);
|
||||
} else if (compose.sourceType === "raw") {
|
||||
command += getCreateComposeFileCommand(compose, deployment.logPath);
|
||||
} else if (compose.sourceType === "gitea") {
|
||||
command += await getGiteaCloneCommand(
|
||||
compose,
|
||||
deployment.logPath,
|
||||
true
|
||||
);
|
||||
}
|
||||
|
||||
await execAsyncRemote(compose.serverId, command);
|
||||
|
||||
104
packages/server/src/services/gitea.ts
Normal file
104
packages/server/src/services/gitea.ts
Normal file
@@ -0,0 +1,104 @@
|
||||
import { db } from "@dokploy/server/db";
|
||||
import {
|
||||
type apiCreateGitea,
|
||||
gitProvider,
|
||||
gitea,
|
||||
} from "@dokploy/server/db/schema";
|
||||
import { TRPCError } from "@trpc/server";
|
||||
import { eq } from "drizzle-orm";
|
||||
export type Gitea = typeof gitea.$inferSelect;
|
||||
|
||||
export const createGitea = async (
|
||||
input: typeof apiCreateGitea._type,
|
||||
organizationId: string,
|
||||
) => {
|
||||
return await db.transaction(async (tx) => {
|
||||
// Insert new Git provider (Gitea)
|
||||
const newGitProvider = await tx
|
||||
.insert(gitProvider)
|
||||
.values({
|
||||
providerType: "gitea", // Set providerType to 'gitea'
|
||||
organizationId: organizationId,
|
||||
name: input.name,
|
||||
})
|
||||
.returning()
|
||||
.then((response) => response[0]);
|
||||
|
||||
if (!newGitProvider) {
|
||||
throw new TRPCError({
|
||||
code: "BAD_REQUEST",
|
||||
message: "Error creating the Git provider",
|
||||
});
|
||||
}
|
||||
|
||||
// Insert the Gitea data into the `gitea` table
|
||||
await tx
|
||||
.insert(gitea)
|
||||
.values({
|
||||
...input,
|
||||
gitProviderId: newGitProvider?.gitProviderId,
|
||||
})
|
||||
.returning()
|
||||
.then((response) => response[0]);
|
||||
});
|
||||
};
|
||||
|
||||
export const findGiteaById = async (giteaId: string) => {
|
||||
try {
|
||||
|
||||
const giteaProviderResult = await db.query.gitea.findFirst({
|
||||
where: eq(gitea.giteaId, giteaId),
|
||||
with: {
|
||||
gitProvider: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!giteaProviderResult) {
|
||||
console.error('No Gitea Provider found:', { giteaId });
|
||||
throw new TRPCError({
|
||||
code: "NOT_FOUND",
|
||||
message: "Gitea Provider not found",
|
||||
});
|
||||
}
|
||||
|
||||
return giteaProviderResult;
|
||||
} catch (error) {
|
||||
console.error('Error finding Gitea Provider:', error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
export const updateGitea = async (
|
||||
giteaId: string,
|
||||
input: Partial<Gitea>,
|
||||
) => {
|
||||
console.log('Updating Gitea Provider:', {
|
||||
giteaId,
|
||||
updateData: {
|
||||
accessTokenPresent: !!input.accessToken,
|
||||
refreshTokenPresent: !!input.refreshToken,
|
||||
expiresAt: input.expiresAt,
|
||||
}
|
||||
});
|
||||
|
||||
try {
|
||||
const updateResult = await db
|
||||
.update(gitea)
|
||||
.set(input)
|
||||
.where(eq(gitea.giteaId, giteaId))
|
||||
.returning();
|
||||
|
||||
// Explicitly type the result and handle potential undefined
|
||||
const result = updateResult[0] as Gitea | undefined;
|
||||
|
||||
if (!result) {
|
||||
console.error('No rows were updated', { giteaId, input });
|
||||
throw new Error(`Failed to update Gitea provider with ID ${giteaId}`);
|
||||
}
|
||||
|
||||
return result;
|
||||
} catch (error) {
|
||||
console.error('Error updating Gitea provider:', error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
@@ -62,6 +62,7 @@ export const findApplicationByPreview = async (applicationId: string) => {
|
||||
gitlab: true,
|
||||
github: true,
|
||||
bitbucket: true,
|
||||
gitea: true,
|
||||
server: true,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -112,99 +112,118 @@ export const getBuildCommand = (
|
||||
|
||||
export const mechanizeDockerContainer = async (
|
||||
application: ApplicationNested,
|
||||
) => {
|
||||
) => {
|
||||
console.log(`Starting to mechanize Docker container for ${application.appName}`);
|
||||
|
||||
const {
|
||||
appName,
|
||||
env,
|
||||
mounts,
|
||||
cpuLimit,
|
||||
memoryLimit,
|
||||
memoryReservation,
|
||||
cpuReservation,
|
||||
command,
|
||||
ports,
|
||||
appName,
|
||||
env,
|
||||
mounts,
|
||||
cpuLimit,
|
||||
memoryLimit,
|
||||
memoryReservation,
|
||||
cpuReservation,
|
||||
command,
|
||||
ports,
|
||||
} = application;
|
||||
|
||||
|
||||
const resources = calculateResources({
|
||||
memoryLimit,
|
||||
memoryReservation,
|
||||
cpuLimit,
|
||||
cpuReservation,
|
||||
memoryLimit,
|
||||
memoryReservation,
|
||||
cpuLimit,
|
||||
cpuReservation,
|
||||
});
|
||||
|
||||
|
||||
const volumesMount = generateVolumeMounts(mounts);
|
||||
|
||||
|
||||
const {
|
||||
HealthCheck,
|
||||
RestartPolicy,
|
||||
Placement,
|
||||
Labels,
|
||||
Mode,
|
||||
RollbackConfig,
|
||||
UpdateConfig,
|
||||
Networks,
|
||||
HealthCheck,
|
||||
RestartPolicy,
|
||||
Placement,
|
||||
Labels,
|
||||
Mode,
|
||||
RollbackConfig,
|
||||
UpdateConfig,
|
||||
Networks,
|
||||
} = generateConfigContainer(application);
|
||||
|
||||
|
||||
const bindsMount = generateBindMounts(mounts);
|
||||
const filesMount = generateFileMounts(appName, application);
|
||||
const envVariables = prepareEnvironmentVariables(
|
||||
env,
|
||||
application.project.env,
|
||||
env,
|
||||
application.project.env,
|
||||
);
|
||||
|
||||
|
||||
const image = getImageName(application);
|
||||
const authConfig = getAuthConfig(application);
|
||||
const docker = await getRemoteDocker(application.serverId);
|
||||
|
||||
|
||||
const settings: CreateServiceOptions = {
|
||||
authconfig: authConfig,
|
||||
Name: appName,
|
||||
TaskTemplate: {
|
||||
ContainerSpec: {
|
||||
HealthCheck,
|
||||
Image: image,
|
||||
Env: envVariables,
|
||||
Mounts: [...volumesMount, ...bindsMount, ...filesMount],
|
||||
...(command
|
||||
? {
|
||||
Command: ["/bin/sh"],
|
||||
Args: ["-c", command],
|
||||
}
|
||||
: {}),
|
||||
Labels,
|
||||
},
|
||||
Networks,
|
||||
RestartPolicy,
|
||||
Placement,
|
||||
Resources: {
|
||||
...resources,
|
||||
},
|
||||
authconfig: authConfig,
|
||||
Name: appName,
|
||||
TaskTemplate: {
|
||||
ContainerSpec: {
|
||||
HealthCheck,
|
||||
Image: image,
|
||||
Env: envVariables,
|
||||
Mounts: [...volumesMount, ...bindsMount, ...filesMount],
|
||||
...(command
|
||||
? {
|
||||
Command: ["/bin/sh"],
|
||||
Args: ["-c", command],
|
||||
}
|
||||
: {}),
|
||||
Labels,
|
||||
},
|
||||
Mode,
|
||||
RollbackConfig,
|
||||
EndpointSpec: {
|
||||
Ports: ports.map((port) => ({
|
||||
Protocol: port.protocol,
|
||||
TargetPort: port.targetPort,
|
||||
PublishedPort: port.publishedPort,
|
||||
})),
|
||||
Networks,
|
||||
RestartPolicy,
|
||||
Placement,
|
||||
Resources: {
|
||||
...resources,
|
||||
},
|
||||
UpdateConfig,
|
||||
},
|
||||
Mode,
|
||||
RollbackConfig,
|
||||
EndpointSpec: {
|
||||
Ports: ports.map((port) => ({
|
||||
Protocol: port.protocol,
|
||||
TargetPort: port.targetPort,
|
||||
PublishedPort: port.publishedPort,
|
||||
})),
|
||||
},
|
||||
UpdateConfig,
|
||||
};
|
||||
|
||||
|
||||
try {
|
||||
const service = docker.getService(appName);
|
||||
const inspect = await service.inspect();
|
||||
await service.update({
|
||||
version: Number.parseInt(inspect.Version.Index),
|
||||
...settings,
|
||||
TaskTemplate: {
|
||||
...settings.TaskTemplate,
|
||||
ForceUpdate: inspect.Spec.TaskTemplate.ForceUpdate + 1,
|
||||
},
|
||||
});
|
||||
} catch (_error) {
|
||||
console.log(`Attempting to find existing service: ${appName}`);
|
||||
const service = docker.getService(appName);
|
||||
const inspect = await service.inspect();
|
||||
console.log(`Found existing service, updating: ${appName}`);
|
||||
|
||||
await service.update({
|
||||
version: Number.parseInt(inspect.Version.Index),
|
||||
...settings,
|
||||
TaskTemplate: {
|
||||
...settings.TaskTemplate,
|
||||
ForceUpdate: inspect.Spec.TaskTemplate.ForceUpdate + 1,
|
||||
},
|
||||
});
|
||||
console.log(`Service updated successfully: ${appName}`);
|
||||
} catch (error: unknown) {
|
||||
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
|
||||
console.log(`Service not found or error: ${errorMessage}`);
|
||||
console.log(`Creating new service: ${appName}`);
|
||||
|
||||
try {
|
||||
await docker.createService(settings);
|
||||
console.log(`Service created successfully: ${appName}`);
|
||||
} catch (createError: unknown) {
|
||||
const createErrorMessage = createError instanceof Error
|
||||
? createError.message
|
||||
: 'Unknown error';
|
||||
console.error(`Failed to create service: ${createErrorMessage}`);
|
||||
throw createError;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -35,7 +35,7 @@ export const buildNixpacks = async (
|
||||
for (const env of envVariables) {
|
||||
args.push("--env", env);
|
||||
}
|
||||
|
||||
|
||||
if (publishDirectory) {
|
||||
/* No need for any start command, since we'll use nginx later on */
|
||||
args.push("--no-error-without-start");
|
||||
@@ -81,7 +81,18 @@ export const buildNixpacks = async (
|
||||
}
|
||||
return true;
|
||||
} catch (e) {
|
||||
await spawnAsync("docker", ["rm", buildContainerId], writeToStream);
|
||||
// Only try to remove the container if it might exist
|
||||
try {
|
||||
await spawnAsync("docker", ["rm", buildContainerId], writeToStream);
|
||||
} catch (rmError) {
|
||||
// Ignore errors from container removal
|
||||
const errorMessage = rmError instanceof Error
|
||||
? rmError.message
|
||||
: 'Unknown container cleanup error';
|
||||
|
||||
// Just log it but don't let it cause another error
|
||||
writeToStream(`Container cleanup attempt: ${errorMessage}\n`);
|
||||
}
|
||||
|
||||
throw e;
|
||||
}
|
||||
|
||||
@@ -22,6 +22,10 @@ import {
|
||||
cloneRawGitlabRepository,
|
||||
cloneRawGitlabRepositoryRemote,
|
||||
} from "../providers/gitlab";
|
||||
import {
|
||||
cloneRawGiteaRepository,
|
||||
cloneRawGiteaRepositoryRemote,
|
||||
} from "../providers/gitea";
|
||||
import {
|
||||
createComposeFileRaw,
|
||||
createComposeFileRawRemote,
|
||||
@@ -44,6 +48,8 @@ export const cloneCompose = async (compose: Compose) => {
|
||||
await cloneRawBitbucketRepository(compose);
|
||||
} else if (compose.sourceType === "git") {
|
||||
await cloneGitRawRepository(compose);
|
||||
} else if (compose.sourceType === "gitea") {
|
||||
await cloneRawGiteaRepository(compose);
|
||||
} else if (compose.sourceType === "raw") {
|
||||
await createComposeFileRaw(compose);
|
||||
}
|
||||
@@ -58,6 +64,8 @@ export const cloneComposeRemote = async (compose: Compose) => {
|
||||
await cloneRawBitbucketRepositoryRemote(compose);
|
||||
} else if (compose.sourceType === "git") {
|
||||
await cloneRawGitRepositoryRemote(compose);
|
||||
} else if (compose.sourceType === "gitea") {
|
||||
await cloneRawGiteaRepository(compose);
|
||||
} else if (compose.sourceType === "raw") {
|
||||
await createComposeFileRawRemote(compose);
|
||||
}
|
||||
|
||||
@@ -113,6 +113,8 @@ export const getBuildAppDirectory = (application: Application) => {
|
||||
buildPath = application?.gitlabBuildPath || "";
|
||||
} else if (sourceType === "bitbucket") {
|
||||
buildPath = application?.bitbucketBuildPath || "";
|
||||
} else if (sourceType === "gitea") {
|
||||
buildPath = application?.giteaBuildPath || "";
|
||||
} else if (sourceType === "drop") {
|
||||
buildPath = application?.dropBuildPath || "";
|
||||
} else if (sourceType === "git") {
|
||||
|
||||
582
packages/server/src/utils/providers/gitea.ts
Normal file
582
packages/server/src/utils/providers/gitea.ts
Normal file
@@ -0,0 +1,582 @@
|
||||
import { createWriteStream } from "node:fs";
|
||||
import { join } from "node:path";
|
||||
import fs from "fs/promises";
|
||||
import { paths } from "@dokploy/server/constants";
|
||||
import { findGiteaById, updateGitea } from "@dokploy/server/services/gitea";
|
||||
import { TRPCError } from "@trpc/server";
|
||||
import { recreateDirectory } from "../filesystem/directory";
|
||||
import { execAsyncRemote } from "../process/execAsync";
|
||||
import { spawnAsync } from "../process/spawnAsync";
|
||||
|
||||
/**
|
||||
* Wrapper function to maintain compatibility with the existing implementation
|
||||
*/
|
||||
export const fetchGiteaBranches = async (
|
||||
giteaId: string,
|
||||
repoFullName: string
|
||||
) => {
|
||||
// Ensure owner and repo are non-empty strings
|
||||
const parts = repoFullName.split('/');
|
||||
|
||||
// Validate that we have exactly two parts
|
||||
if (parts.length !== 2 || !parts[0] || !parts[1]) {
|
||||
throw new Error(`Invalid repository name format: ${repoFullName}. Expected format: owner/repo`);
|
||||
}
|
||||
|
||||
const [owner, repo] = parts;
|
||||
|
||||
// Call the existing getGiteaBranches function with the correct object structure
|
||||
return await getGiteaBranches({
|
||||
giteaId,
|
||||
owner,
|
||||
repo,
|
||||
id: 0 // Provide a default value for optional id
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Helper function to check if the required fields are filled for Gitea repository operations
|
||||
*/
|
||||
export const getErrorCloneRequirements = (entity: {
|
||||
giteaRepository?: string | null;
|
||||
giteaOwner?: string | null;
|
||||
giteaBranch?: string | null;
|
||||
giteaPathNamespace?: string | null
|
||||
}) => {
|
||||
const reasons: string[] = [];
|
||||
const { giteaBranch, giteaOwner, giteaRepository, giteaPathNamespace } = entity;
|
||||
|
||||
if (!giteaRepository) reasons.push("1. Repository not assigned.");
|
||||
if (!giteaOwner) reasons.push("2. Owner not specified.");
|
||||
if (!giteaBranch) reasons.push("3. Branch not defined.");
|
||||
if (!giteaPathNamespace) reasons.push("4. Path namespace not defined.");
|
||||
|
||||
return reasons;
|
||||
};
|
||||
|
||||
/**
|
||||
* Function to refresh the Gitea token if expired
|
||||
*/
|
||||
export const refreshGiteaToken = async (giteaProviderId: string) => {
|
||||
try {
|
||||
console.log('Attempting to refresh Gitea token:', {
|
||||
giteaProviderId,
|
||||
timestamp: new Date().toISOString()
|
||||
});
|
||||
|
||||
const giteaProvider = await findGiteaById(giteaProviderId);
|
||||
|
||||
if (!giteaProvider?.clientId || !giteaProvider?.clientSecret || !giteaProvider?.refreshToken) {
|
||||
console.warn('Missing credentials for token refresh');
|
||||
return giteaProvider?.accessToken || null;
|
||||
}
|
||||
|
||||
const tokenEndpoint = `${giteaProvider.giteaUrl}/login/oauth/access_token`;
|
||||
const params = new URLSearchParams({
|
||||
grant_type: 'refresh_token',
|
||||
refresh_token: giteaProvider.refreshToken,
|
||||
client_id: giteaProvider.clientId,
|
||||
client_secret: giteaProvider.clientSecret,
|
||||
});
|
||||
|
||||
console.log('Token Endpoint:', tokenEndpoint);
|
||||
console.log('Request Parameters:', params.toString());
|
||||
|
||||
const response = await fetch(tokenEndpoint, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
'Accept': 'application/json'
|
||||
},
|
||||
body: params.toString(),
|
||||
});
|
||||
|
||||
console.log('Token Refresh Response:', {
|
||||
status: response.status,
|
||||
statusText: response.statusText
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text();
|
||||
console.error('Token Refresh Failed:', errorText);
|
||||
return giteaProvider?.accessToken || null;
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
const { access_token, refresh_token, expires_in } = data;
|
||||
|
||||
if (!access_token) {
|
||||
console.error('Missing access token in refresh response');
|
||||
return giteaProvider?.accessToken || null;
|
||||
}
|
||||
|
||||
const expiresAt = Date.now() + ((expires_in || 3600) * 1000);
|
||||
const expiresAtSeconds = Math.floor(expiresAt / 1000);
|
||||
|
||||
await updateGitea(giteaProviderId, {
|
||||
accessToken: access_token,
|
||||
refreshToken: refresh_token || giteaProvider.refreshToken,
|
||||
expiresAt: expiresAtSeconds,
|
||||
});
|
||||
|
||||
console.log('Gitea token refreshed successfully.');
|
||||
return access_token;
|
||||
} catch (error) {
|
||||
console.error('Token Refresh Error:', error);
|
||||
// Return the existing token if refresh fails
|
||||
const giteaProvider = await findGiteaById(giteaProviderId);
|
||||
return giteaProvider?.accessToken || null;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Generate a secure Git clone command with proper validation
|
||||
*/
|
||||
export const getGiteaCloneCommand = async (entity: any, logPath: string, isCompose = false) => {
|
||||
const { appName, giteaBranch, giteaId, giteaOwner, giteaRepository, serverId } = entity;
|
||||
|
||||
if (!serverId) {
|
||||
throw new TRPCError({
|
||||
code: "NOT_FOUND",
|
||||
message: "Server not found",
|
||||
});
|
||||
}
|
||||
|
||||
if (!giteaId) {
|
||||
throw new TRPCError({
|
||||
code: "NOT_FOUND",
|
||||
message: "Gitea Provider not found",
|
||||
});
|
||||
}
|
||||
|
||||
// Use paths(true) for remote operations
|
||||
const { COMPOSE_PATH, APPLICATIONS_PATH } = paths(true);
|
||||
const basePath = isCompose ? COMPOSE_PATH : APPLICATIONS_PATH;
|
||||
const outputPath = join(basePath, appName, "code");
|
||||
|
||||
const giteaProvider = await findGiteaById(giteaId);
|
||||
const baseUrl = giteaProvider.giteaUrl.replace(/^https?:\/\//, '');
|
||||
const repoClone = `${giteaOwner}/${giteaRepository}.git`;
|
||||
const cloneUrl = `https://oauth2:${giteaProvider.accessToken}@${baseUrl}/${repoClone}`;
|
||||
|
||||
const cloneCommand = `
|
||||
# Ensure output directory exists and is empty
|
||||
rm -rf ${outputPath};
|
||||
mkdir -p ${outputPath};
|
||||
|
||||
# Clone with detailed logging
|
||||
echo "Cloning repository to ${outputPath}" >> ${logPath};
|
||||
echo "Repository: ${repoClone}" >> ${logPath};
|
||||
|
||||
if ! git clone --branch ${giteaBranch} --depth 1 --recurse-submodules ${cloneUrl} ${outputPath} >> ${logPath} 2>&1; then
|
||||
echo "❌ [ERROR] Failed to clone the repository ${repoClone}" >> ${logPath};
|
||||
exit 1;
|
||||
fi
|
||||
|
||||
# Verify clone
|
||||
CLONE_COUNT=$(find ${outputPath} -type f | wc -l)
|
||||
echo "Files cloned: $CLONE_COUNT" >> ${logPath};
|
||||
|
||||
if [ "$CLONE_COUNT" -eq 0 ]; then
|
||||
echo "⚠️ WARNING: No files cloned" >> ${logPath};
|
||||
exit 1;
|
||||
fi
|
||||
|
||||
echo "Cloned ${repoClone} to ${outputPath}: ✅" >> ${logPath};
|
||||
`;
|
||||
|
||||
return cloneCommand;
|
||||
};
|
||||
|
||||
/**
|
||||
* Main function to clone a Gitea repository with improved validation and robust directory handling
|
||||
*/
|
||||
export const cloneGiteaRepository = async (
|
||||
entity: any,
|
||||
logPath?: string,
|
||||
isCompose: boolean = false
|
||||
) => {
|
||||
// If logPath is not provided, generate a default log path
|
||||
const actualLogPath = logPath || join(
|
||||
paths()[isCompose ? 'COMPOSE_PATH' : 'APPLICATIONS_PATH'],
|
||||
entity.appName,
|
||||
'clone.log'
|
||||
);
|
||||
|
||||
const writeStream = createWriteStream(actualLogPath, { flags: "a" });
|
||||
const { appName, giteaBranch, giteaId, giteaOwner, giteaRepository } = entity;
|
||||
|
||||
try {
|
||||
if (!giteaId) {
|
||||
throw new TRPCError({
|
||||
code: "NOT_FOUND",
|
||||
message: "Gitea Provider not found",
|
||||
});
|
||||
}
|
||||
|
||||
// Refresh the access token
|
||||
await refreshGiteaToken(giteaId);
|
||||
|
||||
// Fetch the Gitea provider
|
||||
const giteaProvider = await findGiteaById(giteaId);
|
||||
if (!giteaProvider) {
|
||||
throw new TRPCError({
|
||||
code: "NOT_FOUND",
|
||||
message: "Gitea provider not found in the database",
|
||||
});
|
||||
}
|
||||
|
||||
const { COMPOSE_PATH, APPLICATIONS_PATH } = paths();
|
||||
const basePath = isCompose ? COMPOSE_PATH : APPLICATIONS_PATH;
|
||||
const outputPath = join(basePath, appName, "code");
|
||||
|
||||
// Log path information
|
||||
writeStream.write(`\nPath Information:\n`);
|
||||
writeStream.write(`Base Path: ${basePath}\n`);
|
||||
writeStream.write(`Output Path: ${outputPath}\n`);
|
||||
|
||||
writeStream.write(`\nRecreating directory: ${outputPath}\n`);
|
||||
await recreateDirectory(outputPath);
|
||||
|
||||
// Additional step - verify directory exists and is empty
|
||||
try {
|
||||
const filesCheck = await fs.readdir(outputPath);
|
||||
writeStream.write(`Directory after cleanup - files: ${filesCheck.length}\n`);
|
||||
|
||||
if (filesCheck.length > 0) {
|
||||
writeStream.write(`WARNING: Directory not empty after cleanup!\n`);
|
||||
|
||||
// Force remove with shell command if recreateDirectory didn't work
|
||||
if (entity.serverId) {
|
||||
writeStream.write(`Attempting forceful cleanup via shell command\n`);
|
||||
await execAsyncRemote(entity.serverId, `rm -rf "${outputPath}" && mkdir -p "${outputPath}"`,
|
||||
data => writeStream.write(`Cleanup output: ${data}\n`));
|
||||
} else {
|
||||
// Fallback to direct fs operations if serverId not available
|
||||
writeStream.write(`Attempting direct fs removal\n`);
|
||||
await fs.rm(outputPath, { recursive: true, force: true });
|
||||
await fs.mkdir(outputPath, { recursive: true });
|
||||
}
|
||||
}
|
||||
} catch (verifyError) {
|
||||
writeStream.write(`Error verifying directory: ${verifyError}\n`);
|
||||
// Continue anyway - the clone operation might handle this
|
||||
}
|
||||
|
||||
const repoClone = `${giteaOwner}/${giteaRepository}.git`;
|
||||
const baseUrl = giteaProvider.giteaUrl.replace(/^https?:\/\//, '');
|
||||
const cloneUrl = `https://oauth2:${giteaProvider.accessToken}@${baseUrl}/${repoClone}`;
|
||||
|
||||
writeStream.write(`\nCloning Repo ${repoClone} to ${outputPath}...\n`);
|
||||
writeStream.write(`Clone URL (masked): https://oauth2:***@${baseUrl}/${repoClone}\n`);
|
||||
|
||||
// First try standard git clone
|
||||
try {
|
||||
await spawnAsync(
|
||||
"git",
|
||||
[
|
||||
"clone",
|
||||
"--branch",
|
||||
giteaBranch,
|
||||
"--depth",
|
||||
"1",
|
||||
"--recurse-submodules",
|
||||
cloneUrl,
|
||||
outputPath,
|
||||
"--progress"
|
||||
],
|
||||
(data) => {
|
||||
if (writeStream.writable) {
|
||||
writeStream.write(data);
|
||||
}
|
||||
}
|
||||
);
|
||||
writeStream.write(`\nStandard git clone succeeded\n`);
|
||||
} catch (cloneError) {
|
||||
writeStream.write(`\nStandard git clone failed: ${cloneError}\n`);
|
||||
writeStream.write(`Falling back to git init + fetch approach...\n`);
|
||||
|
||||
// Retry cleanup one more time
|
||||
if (entity.serverId) {
|
||||
await execAsyncRemote(entity.serverId, `rm -rf "${outputPath}" && mkdir -p "${outputPath}"`,
|
||||
data => writeStream.write(`Cleanup retry: ${data}\n`));
|
||||
} else {
|
||||
await fs.rm(outputPath, { recursive: true, force: true });
|
||||
await fs.mkdir(outputPath, { recursive: true });
|
||||
}
|
||||
|
||||
// Initialize git repo
|
||||
writeStream.write(`Initializing git repository...\n`);
|
||||
await spawnAsync("git", ["init", outputPath], data => writeStream.write(data));
|
||||
|
||||
// Set remote origin
|
||||
writeStream.write(`Setting remote origin...\n`);
|
||||
await spawnAsync(
|
||||
"git",
|
||||
["-C", outputPath, "remote", "add", "origin", cloneUrl],
|
||||
data => writeStream.write(data)
|
||||
);
|
||||
|
||||
// Fetch branch
|
||||
writeStream.write(`Fetching branch: ${giteaBranch}...\n`);
|
||||
await spawnAsync(
|
||||
"git",
|
||||
["-C", outputPath, "fetch", "--depth", "1", "origin", giteaBranch],
|
||||
data => writeStream.write(data)
|
||||
);
|
||||
|
||||
// Checkout branch
|
||||
writeStream.write(`Checking out branch: ${giteaBranch}...\n`);
|
||||
await spawnAsync(
|
||||
"git",
|
||||
["-C", outputPath, "checkout", "FETCH_HEAD"],
|
||||
data => writeStream.write(data)
|
||||
);
|
||||
|
||||
writeStream.write(`Git init and fetch completed successfully\n`);
|
||||
}
|
||||
|
||||
// Verify clone
|
||||
const files = await fs.readdir(outputPath);
|
||||
writeStream.write(`\nClone Verification:\n`);
|
||||
writeStream.write(`Files found: ${files.length}\n`);
|
||||
if (files.length > 0) {
|
||||
files.slice(0, 10).forEach(file => writeStream.write(`- ${file}\n`));
|
||||
}
|
||||
|
||||
if (files.length === 0) {
|
||||
throw new Error("Repository clone failed - directory is empty");
|
||||
}
|
||||
|
||||
writeStream.write(`\nCloned ${repoClone} successfully: ✅\n`);
|
||||
} catch (error) {
|
||||
writeStream.write(`\nClone Error: ${error}\n`);
|
||||
throw error;
|
||||
} finally {
|
||||
writeStream.end();
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Clone a Gitea repository locally for a Compose configuration
|
||||
* Leverages the existing comprehensive cloneGiteaRepository function
|
||||
*/
|
||||
export const cloneRawGiteaRepository = async (entity: any) => {
|
||||
// Merge the existing entity with compose-specific properties
|
||||
const composeEntity = {
|
||||
...entity,
|
||||
sourceType: 'compose',
|
||||
isCompose: true
|
||||
};
|
||||
|
||||
// Call cloneGiteaRepository with the modified entity
|
||||
await cloneGiteaRepository(composeEntity);
|
||||
};
|
||||
|
||||
/**
|
||||
* Clone a Gitea repository remotely for a Compose configuration
|
||||
* Uses the existing getGiteaCloneCommand function for remote cloning
|
||||
*/
|
||||
export const cloneRawGiteaRepositoryRemote = async (compose: any) => {
|
||||
const { COMPOSE_PATH } = paths(true);
|
||||
const logPath = join(COMPOSE_PATH, compose.appName, 'clone.log');
|
||||
|
||||
// Reuse the existing getGiteaCloneCommand function
|
||||
const command = await getGiteaCloneCommand({
|
||||
...compose,
|
||||
isCompose: true
|
||||
}, logPath, true);
|
||||
|
||||
if (!compose.serverId) {
|
||||
throw new TRPCError({
|
||||
code: "NOT_FOUND",
|
||||
message: "Server not found",
|
||||
});
|
||||
}
|
||||
|
||||
// Execute the clone command on the remote server
|
||||
await execAsyncRemote(compose.serverId, command);
|
||||
};
|
||||
|
||||
// Helper function to check if a Gitea provider meets the necessary requirements
|
||||
export const haveGiteaRequirements = (giteaProvider: any) => {
|
||||
return !!(giteaProvider?.clientId && giteaProvider?.clientSecret);
|
||||
};
|
||||
|
||||
/**
|
||||
* Function to test the connection to a Gitea provider
|
||||
*/
|
||||
export const testGiteaConnection = async (input: { giteaId: string }) => {
|
||||
try {
|
||||
const { giteaId } = input;
|
||||
|
||||
if (!giteaId) {
|
||||
throw new Error("Gitea provider not found");
|
||||
}
|
||||
|
||||
// Fetch the Gitea provider from the database
|
||||
const giteaProvider = await findGiteaById(giteaId);
|
||||
if (!giteaProvider) {
|
||||
throw new TRPCError({
|
||||
code: "NOT_FOUND",
|
||||
message: "Gitea provider not found in the database",
|
||||
});
|
||||
}
|
||||
|
||||
console.log('Gitea Provider Found:', {
|
||||
id: giteaProvider.giteaId,
|
||||
url: giteaProvider.giteaUrl,
|
||||
hasAccessToken: !!giteaProvider.accessToken,
|
||||
});
|
||||
|
||||
// Refresh the token if needed
|
||||
await refreshGiteaToken(giteaId);
|
||||
|
||||
// Fetch the provider again in case the token was refreshed
|
||||
const provider = await findGiteaById(giteaId);
|
||||
if (!provider || !provider.accessToken) {
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
message: "No access token available. Please authorize with Gitea.",
|
||||
});
|
||||
}
|
||||
|
||||
// Make API request to test connection
|
||||
console.log('Making API request to test connection...');
|
||||
|
||||
// Construct proper URL for the API request
|
||||
const baseUrl = provider.giteaUrl.replace(/\/+$/, ''); // Remove trailing slashes
|
||||
const url = `${baseUrl}/api/v1/user/repos`;
|
||||
|
||||
console.log(`Testing connection to: ${url}`);
|
||||
|
||||
const response = await fetch(url, {
|
||||
headers: {
|
||||
'Accept': 'application/json',
|
||||
'Authorization': `token ${provider.accessToken}`
|
||||
}
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text();
|
||||
console.error('Repository API failed:', errorText);
|
||||
throw new Error(`Failed to connect to Gitea API: ${response.status} ${response.statusText}`);
|
||||
}
|
||||
|
||||
const repos = await response.json();
|
||||
console.log(`Successfully connected to Gitea API. Found ${repos.length} repositories.`);
|
||||
|
||||
// Update lastAuthenticatedAt
|
||||
await updateGitea(giteaId, {
|
||||
lastAuthenticatedAt: Math.floor(Date.now() / 1000)
|
||||
});
|
||||
|
||||
return repos.length;
|
||||
} catch (error) {
|
||||
console.error('Gitea Connection Test Error:', error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Function to fetch repositories from a Gitea provider
|
||||
*/
|
||||
export const getGiteaRepositories = async (giteaId?: string) => {
|
||||
if (!giteaId) {
|
||||
return [];
|
||||
}
|
||||
|
||||
// Refresh the token
|
||||
await refreshGiteaToken(giteaId);
|
||||
|
||||
// Fetch the Gitea provider
|
||||
const giteaProvider = await findGiteaById(giteaId);
|
||||
|
||||
// Construct the URL for fetching repositories
|
||||
const baseUrl = giteaProvider.giteaUrl.replace(/\/+$/, '');
|
||||
const url = `${baseUrl}/api/v1/user/repos`;
|
||||
|
||||
const response = await fetch(url, {
|
||||
headers: {
|
||||
'Accept': 'application/json',
|
||||
'Authorization': `token ${giteaProvider.accessToken}`
|
||||
}
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new TRPCError({
|
||||
code: "BAD_REQUEST",
|
||||
message: `Failed to fetch repositories: ${response.statusText}`,
|
||||
});
|
||||
}
|
||||
|
||||
const repositories = await response.json();
|
||||
|
||||
// Map repositories to a consistent format
|
||||
const mappedRepositories = repositories.map((repo: any) => ({
|
||||
id: repo.id,
|
||||
name: repo.name,
|
||||
url: repo.full_name,
|
||||
owner: {
|
||||
username: repo.owner.login
|
||||
}
|
||||
}));
|
||||
|
||||
return mappedRepositories;
|
||||
};
|
||||
|
||||
/**
|
||||
* Function to fetch branches for a specific Gitea repository
|
||||
*/
|
||||
export const getGiteaBranches = async (input: {
|
||||
id?: number;
|
||||
giteaId?: string;
|
||||
owner: string;
|
||||
repo: string;
|
||||
}) => {
|
||||
if (!input.giteaId) {
|
||||
return [];
|
||||
}
|
||||
|
||||
// Fetch the Gitea provider
|
||||
const giteaProvider = await findGiteaById(input.giteaId);
|
||||
|
||||
// Construct the URL for fetching branches
|
||||
const baseUrl = giteaProvider.giteaUrl.replace(/\/+$/, '');
|
||||
const url = `${baseUrl}/api/v1/repos/${input.owner}/${input.repo}/branches`;
|
||||
|
||||
const response = await fetch(url, {
|
||||
headers: {
|
||||
'Accept': 'application/json',
|
||||
'Authorization': `token ${giteaProvider.accessToken}`
|
||||
}
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to fetch branches: ${response.statusText}`);
|
||||
}
|
||||
|
||||
const branches = await response.json();
|
||||
|
||||
// Map branches to a consistent format
|
||||
return branches.map((branch: any) => ({
|
||||
id: branch.name,
|
||||
name: branch.name,
|
||||
commit: {
|
||||
id: branch.commit.id
|
||||
}
|
||||
}));
|
||||
};
|
||||
|
||||
export default {
|
||||
cloneGiteaRepository,
|
||||
cloneRawGiteaRepository,
|
||||
cloneRawGiteaRepositoryRemote,
|
||||
refreshGiteaToken,
|
||||
haveGiteaRequirements,
|
||||
testGiteaConnection,
|
||||
getGiteaRepositories,
|
||||
getGiteaBranches,
|
||||
fetchGiteaBranches
|
||||
};
|
||||
Reference in New Issue
Block a user