mirror of
https://github.com/Dokploy/dokploy.git
synced 2026-07-13 18:05:31 +02:00
Merge pull request #2525 from divaltor/bitbucket-api-token
feat(bitbucket): Deprecate App password and replace it with API token
This commit is contained in:
@@ -1,7 +1,6 @@
|
|||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
import { ExternalLink } from "lucide-react";
|
import { ExternalLink } from "lucide-react";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { useRouter } from "next/router";
|
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { useForm } from "react-hook-form";
|
import { useForm } from "react-hook-form";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
@@ -27,18 +26,12 @@ import {
|
|||||||
} from "@/components/ui/form";
|
} from "@/components/ui/form";
|
||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
import { useUrl } from "@/utils/hooks/use-url";
|
|
||||||
|
|
||||||
const Schema = z.object({
|
const Schema = z.object({
|
||||||
name: z.string().min(1, {
|
name: z.string().min(1, { message: "Name is required" }),
|
||||||
message: "Name is required",
|
username: z.string().min(1, { message: "Username is required" }),
|
||||||
}),
|
email: z.string().email().optional(),
|
||||||
username: z.string().min(1, {
|
apiToken: z.string().min(1, { message: "API Token is required" }),
|
||||||
message: "Username is required",
|
|
||||||
}),
|
|
||||||
password: z.string().min(1, {
|
|
||||||
message: "App Password is required",
|
|
||||||
}),
|
|
||||||
workspaceName: z.string().optional(),
|
workspaceName: z.string().optional(),
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -47,14 +40,12 @@ type Schema = z.infer<typeof Schema>;
|
|||||||
export const AddBitbucketProvider = () => {
|
export const AddBitbucketProvider = () => {
|
||||||
const utils = api.useUtils();
|
const utils = api.useUtils();
|
||||||
const [isOpen, setIsOpen] = useState(false);
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
const _url = useUrl();
|
|
||||||
const { mutateAsync, error, isError } = api.bitbucket.create.useMutation();
|
const { mutateAsync, error, isError } = api.bitbucket.create.useMutation();
|
||||||
const { data: auth } = api.user.get.useQuery();
|
const { data: auth } = api.user.get.useQuery();
|
||||||
const _router = useRouter();
|
|
||||||
const form = useForm<Schema>({
|
const form = useForm<Schema>({
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
username: "",
|
username: "",
|
||||||
password: "",
|
apiToken: "",
|
||||||
workspaceName: "",
|
workspaceName: "",
|
||||||
},
|
},
|
||||||
resolver: zodResolver(Schema),
|
resolver: zodResolver(Schema),
|
||||||
@@ -63,7 +54,8 @@ export const AddBitbucketProvider = () => {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
form.reset({
|
form.reset({
|
||||||
username: "",
|
username: "",
|
||||||
password: "",
|
email: "",
|
||||||
|
apiToken: "",
|
||||||
workspaceName: "",
|
workspaceName: "",
|
||||||
});
|
});
|
||||||
}, [form, isOpen]);
|
}, [form, isOpen]);
|
||||||
@@ -71,10 +63,11 @@ export const AddBitbucketProvider = () => {
|
|||||||
const onSubmit = async (data: Schema) => {
|
const onSubmit = async (data: Schema) => {
|
||||||
await mutateAsync({
|
await mutateAsync({
|
||||||
bitbucketUsername: data.username,
|
bitbucketUsername: data.username,
|
||||||
appPassword: data.password,
|
apiToken: data.apiToken,
|
||||||
bitbucketWorkspaceName: data.workspaceName || "",
|
bitbucketWorkspaceName: data.workspaceName || "",
|
||||||
authId: auth?.id || "",
|
authId: auth?.id || "",
|
||||||
name: data.name || "",
|
name: data.name || "",
|
||||||
|
bitbucketEmail: data.email || "",
|
||||||
})
|
})
|
||||||
.then(async () => {
|
.then(async () => {
|
||||||
await utils.gitProvider.getAll.invalidate();
|
await utils.gitProvider.getAll.invalidate();
|
||||||
@@ -113,37 +106,46 @@ export const AddBitbucketProvider = () => {
|
|||||||
>
|
>
|
||||||
<CardContent className="p-0">
|
<CardContent className="p-0">
|
||||||
<div className="flex flex-col gap-4">
|
<div className="flex flex-col gap-4">
|
||||||
|
<AlertBlock type="warning">
|
||||||
|
Bitbucket App Passwords are deprecated for new providers. Use
|
||||||
|
an API Token instead. Existing providers with App Passwords
|
||||||
|
will continue to work until 9th June 2026.
|
||||||
|
</AlertBlock>
|
||||||
|
|
||||||
|
<div className="mt-1 text-sm">
|
||||||
|
Manage tokens in
|
||||||
|
<Link
|
||||||
|
href="https://id.atlassian.com/manage-profile/security/api-tokens"
|
||||||
|
target="_blank"
|
||||||
|
className="inline-flex items-center gap-1 ml-1"
|
||||||
|
>
|
||||||
|
<span>Bitbucket settings</span>
|
||||||
|
<ExternalLink className="w-fit text-primary size-4" />
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
<ul className="list-disc list-inside ml-4 text-sm text-muted-foreground">
|
||||||
|
<li className="text-muted-foreground text-sm">
|
||||||
|
Click on Create API token with scopes
|
||||||
|
</li>
|
||||||
|
<li className="text-muted-foreground text-sm">
|
||||||
|
Select the expiration date (Max 1 year)
|
||||||
|
</li>
|
||||||
|
<li className="text-muted-foreground text-sm">
|
||||||
|
Select Bitbucket product.
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
<p className="text-muted-foreground text-sm">
|
<p className="text-muted-foreground text-sm">
|
||||||
To integrate your Bitbucket account, you need to create a new
|
Select the following scopes:
|
||||||
App Password in your Bitbucket settings. Follow these steps:
|
|
||||||
</p>
|
</p>
|
||||||
<ol className="list-decimal list-inside text-sm text-muted-foreground">
|
|
||||||
<li className="flex flex-row gap-2 items-center">
|
<ul className="list-disc list-inside ml-4 text-sm text-muted-foreground">
|
||||||
Create new App Password{" "}
|
<li>read:repository:bitbucket</li>
|
||||||
<Link
|
<li>read:pullrequest:bitbucket</li>
|
||||||
href="https://bitbucket.org/account/settings/app-passwords/new"
|
<li>read:webhook:bitbucket</li>
|
||||||
target="_blank"
|
<li>read:workspace:bitbucket</li>
|
||||||
>
|
<li>write:webhook:bitbucket</li>
|
||||||
<ExternalLink className="w-fit text-primary size-4" />
|
</ul>
|
||||||
</Link>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
When creating the App Password, ensure you grant the
|
|
||||||
following permissions:
|
|
||||||
<ul className="list-disc list-inside ml-4">
|
|
||||||
<li>Account: Read</li>
|
|
||||||
<li>Workspace membership: Read</li>
|
|
||||||
<li>Projects: Read</li>
|
|
||||||
<li>Repositories: Read</li>
|
|
||||||
<li>Pull requests: Read</li>
|
|
||||||
<li>Webhooks: Read and write</li>
|
|
||||||
</ul>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
After creating, you'll receive an App Password. Copy it and
|
|
||||||
paste it below along with your Bitbucket username.
|
|
||||||
</li>
|
|
||||||
</ol>
|
|
||||||
<FormField
|
<FormField
|
||||||
control={form.control}
|
control={form.control}
|
||||||
name="name"
|
name="name"
|
||||||
@@ -152,7 +154,7 @@ export const AddBitbucketProvider = () => {
|
|||||||
<FormLabel>Name</FormLabel>
|
<FormLabel>Name</FormLabel>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Input
|
<Input
|
||||||
placeholder="Random Name eg(my-personal-account)"
|
placeholder="Your Bitbucket Provider, eg: my-personal-account"
|
||||||
{...field}
|
{...field}
|
||||||
/>
|
/>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
@@ -179,14 +181,27 @@ export const AddBitbucketProvider = () => {
|
|||||||
|
|
||||||
<FormField
|
<FormField
|
||||||
control={form.control}
|
control={form.control}
|
||||||
name="password"
|
name="email"
|
||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<FormItem>
|
<FormItem>
|
||||||
<FormLabel>App Password</FormLabel>
|
<FormLabel>Bitbucket Email</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input placeholder="Your Bitbucket email" {...field} />
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="apiToken"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>API Token</FormLabel>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Input
|
<Input
|
||||||
type="password"
|
placeholder="Paste your Bitbucket API token"
|
||||||
placeholder="ATBBPDYUC94nR96Nj7Cqpp4pfwKk03573DD2"
|
|
||||||
{...field}
|
{...field}
|
||||||
/>
|
/>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
@@ -200,7 +215,7 @@ export const AddBitbucketProvider = () => {
|
|||||||
name="workspaceName"
|
name="workspaceName"
|
||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<FormItem>
|
<FormItem>
|
||||||
<FormLabel>Workspace Name (Optional)</FormLabel>
|
<FormLabel>Workspace Name (optional)</FormLabel>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Input
|
<Input
|
||||||
placeholder="For organization accounts"
|
placeholder="For organization accounts"
|
||||||
|
|||||||
@@ -33,7 +33,10 @@ const Schema = z.object({
|
|||||||
username: z.string().min(1, {
|
username: z.string().min(1, {
|
||||||
message: "Username is required",
|
message: "Username is required",
|
||||||
}),
|
}),
|
||||||
|
email: z.string().email().optional(),
|
||||||
workspaceName: z.string().optional(),
|
workspaceName: z.string().optional(),
|
||||||
|
apiToken: z.string().optional(),
|
||||||
|
appPassword: z.string().optional(),
|
||||||
});
|
});
|
||||||
|
|
||||||
type Schema = z.infer<typeof Schema>;
|
type Schema = z.infer<typeof Schema>;
|
||||||
@@ -60,19 +63,28 @@ export const EditBitbucketProvider = ({ bitbucketId }: Props) => {
|
|||||||
const form = useForm<Schema>({
|
const form = useForm<Schema>({
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
username: "",
|
username: "",
|
||||||
|
email: "",
|
||||||
workspaceName: "",
|
workspaceName: "",
|
||||||
|
apiToken: "",
|
||||||
|
appPassword: "",
|
||||||
},
|
},
|
||||||
resolver: zodResolver(Schema),
|
resolver: zodResolver(Schema),
|
||||||
});
|
});
|
||||||
|
|
||||||
const username = form.watch("username");
|
const username = form.watch("username");
|
||||||
|
const email = form.watch("email");
|
||||||
const workspaceName = form.watch("workspaceName");
|
const workspaceName = form.watch("workspaceName");
|
||||||
|
const apiToken = form.watch("apiToken");
|
||||||
|
const appPassword = form.watch("appPassword");
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
form.reset({
|
form.reset({
|
||||||
username: bitbucket?.bitbucketUsername || "",
|
username: bitbucket?.bitbucketUsername || "",
|
||||||
|
email: bitbucket?.bitbucketEmail || "",
|
||||||
workspaceName: bitbucket?.bitbucketWorkspaceName || "",
|
workspaceName: bitbucket?.bitbucketWorkspaceName || "",
|
||||||
name: bitbucket?.gitProvider.name || "",
|
name: bitbucket?.gitProvider.name || "",
|
||||||
|
apiToken: bitbucket?.apiToken || "",
|
||||||
|
appPassword: bitbucket?.appPassword || "",
|
||||||
});
|
});
|
||||||
}, [form, isOpen, bitbucket]);
|
}, [form, isOpen, bitbucket]);
|
||||||
|
|
||||||
@@ -81,8 +93,11 @@ export const EditBitbucketProvider = ({ bitbucketId }: Props) => {
|
|||||||
bitbucketId,
|
bitbucketId,
|
||||||
gitProviderId: bitbucket?.gitProviderId || "",
|
gitProviderId: bitbucket?.gitProviderId || "",
|
||||||
bitbucketUsername: data.username,
|
bitbucketUsername: data.username,
|
||||||
|
bitbucketEmail: data.email || "",
|
||||||
bitbucketWorkspaceName: data.workspaceName || "",
|
bitbucketWorkspaceName: data.workspaceName || "",
|
||||||
name: data.name || "",
|
name: data.name || "",
|
||||||
|
apiToken: data.apiToken || "",
|
||||||
|
appPassword: data.appPassword || "",
|
||||||
})
|
})
|
||||||
.then(async () => {
|
.then(async () => {
|
||||||
await utils.gitProvider.getAll.invalidate();
|
await utils.gitProvider.getAll.invalidate();
|
||||||
@@ -121,6 +136,12 @@ export const EditBitbucketProvider = ({ bitbucketId }: Props) => {
|
|||||||
>
|
>
|
||||||
<CardContent className="p-0">
|
<CardContent className="p-0">
|
||||||
<div className="flex flex-col gap-4">
|
<div className="flex flex-col gap-4">
|
||||||
|
<p className="text-muted-foreground text-sm">
|
||||||
|
Update your Bitbucket authentication. Use API Token for
|
||||||
|
enhanced security (recommended) or App Password for legacy
|
||||||
|
support.
|
||||||
|
</p>
|
||||||
|
|
||||||
<FormField
|
<FormField
|
||||||
control={form.control}
|
control={form.control}
|
||||||
name="name"
|
name="name"
|
||||||
@@ -154,6 +175,24 @@ export const EditBitbucketProvider = ({ bitbucketId }: Props) => {
|
|||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="email"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Email (Required for API Tokens)</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input
|
||||||
|
type="email"
|
||||||
|
placeholder="Your Bitbucket email address"
|
||||||
|
{...field}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
<FormField
|
<FormField
|
||||||
control={form.control}
|
control={form.control}
|
||||||
name="workspaceName"
|
name="workspaceName"
|
||||||
@@ -171,6 +210,49 @@ export const EditBitbucketProvider = ({ bitbucketId }: Props) => {
|
|||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<div className="flex flex-col gap-2 border-t pt-4">
|
||||||
|
<h3 className="text-sm font-medium mb-2">
|
||||||
|
Authentication (Update to use API Token)
|
||||||
|
</h3>
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="apiToken"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>API Token (Recommended)</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input
|
||||||
|
type="password"
|
||||||
|
placeholder="Enter your Bitbucket API Token"
|
||||||
|
{...field}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="appPassword"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>
|
||||||
|
App Password (Legacy - will be deprecated June 2026)
|
||||||
|
</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input
|
||||||
|
type="password"
|
||||||
|
placeholder="Enter your Bitbucket App Password"
|
||||||
|
{...field}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className="flex w-full justify-between gap-4 mt-4">
|
<div className="flex w-full justify-between gap-4 mt-4">
|
||||||
<Button
|
<Button
|
||||||
type="button"
|
type="button"
|
||||||
@@ -180,7 +262,10 @@ export const EditBitbucketProvider = ({ bitbucketId }: Props) => {
|
|||||||
await testConnection({
|
await testConnection({
|
||||||
bitbucketId,
|
bitbucketId,
|
||||||
bitbucketUsername: username,
|
bitbucketUsername: username,
|
||||||
|
bitbucketEmail: email,
|
||||||
workspaceName: workspaceName,
|
workspaceName: workspaceName,
|
||||||
|
apiToken: apiToken,
|
||||||
|
appPassword: appPassword,
|
||||||
})
|
})
|
||||||
.then(async (message) => {
|
.then(async (message) => {
|
||||||
toast.info(`Message: ${message}`);
|
toast.info(`Message: ${message}`);
|
||||||
|
|||||||
@@ -157,7 +157,13 @@ export const ShowGitProviders = () => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex flex-row gap-1">
|
<div className="flex flex-row gap-1 items-center">
|
||||||
|
{isBitbucket &&
|
||||||
|
gitProvider.bitbucket?.appPassword &&
|
||||||
|
!gitProvider.bitbucket?.apiToken ? (
|
||||||
|
<Badge variant="yellow">Deprecated</Badge>
|
||||||
|
) : null}
|
||||||
|
|
||||||
{!haveGithubRequirements && isGithub && (
|
{!haveGithubRequirements && isGithub && (
|
||||||
<div className="flex flex-row gap-1 items-center">
|
<div className="flex flex-row gap-1 items-center">
|
||||||
<Badge
|
<Badge
|
||||||
|
|||||||
1
apps/dokploy/drizzle/0111_mushy_wolfsbane.sql
Normal file
1
apps/dokploy/drizzle/0111_mushy_wolfsbane.sql
Normal file
@@ -0,0 +1 @@
|
|||||||
|
ALTER TABLE "bitbucket" ADD COLUMN "apiToken" text;
|
||||||
1
apps/dokploy/drizzle/0112_freezing_skrulls.sql
Normal file
1
apps/dokploy/drizzle/0112_freezing_skrulls.sql
Normal file
@@ -0,0 +1 @@
|
|||||||
|
ALTER TABLE "bitbucket" ADD COLUMN "bitbucketEmail" text;
|
||||||
6565
apps/dokploy/drizzle/meta/0111_snapshot.json
Normal file
6565
apps/dokploy/drizzle/meta/0111_snapshot.json
Normal file
File diff suppressed because it is too large
Load Diff
6571
apps/dokploy/drizzle/meta/0112_snapshot.json
Normal file
6571
apps/dokploy/drizzle/meta/0112_snapshot.json
Normal file
File diff suppressed because it is too large
Load Diff
@@ -778,6 +778,20 @@
|
|||||||
"when": 1757189541734,
|
"when": 1757189541734,
|
||||||
"tag": "0110_red_psynapse",
|
"tag": "0110_red_psynapse",
|
||||||
"breakpoints": true
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 111,
|
||||||
|
"version": "7",
|
||||||
|
"when": 1758445844561,
|
||||||
|
"tag": "0111_mushy_wolfsbane",
|
||||||
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 112,
|
||||||
|
"version": "7",
|
||||||
|
"when": 1758483520214,
|
||||||
|
"tag": "0112_freezing_skrulls",
|
||||||
|
"breakpoints": true
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@@ -1,4 +1,9 @@
|
|||||||
import { IS_CLOUD, shouldDeploy } from "@dokploy/server";
|
import {
|
||||||
|
type Bitbucket,
|
||||||
|
getBitbucketHeaders,
|
||||||
|
IS_CLOUD,
|
||||||
|
shouldDeploy,
|
||||||
|
} from "@dokploy/server";
|
||||||
import { eq } from "drizzle-orm";
|
import { eq } from "drizzle-orm";
|
||||||
import type { NextApiRequest, NextApiResponse } from "next";
|
import type { NextApiRequest, NextApiResponse } from "next";
|
||||||
import { db } from "@/server/db";
|
import { db } from "@/server/db";
|
||||||
@@ -146,10 +151,10 @@ export default async function handler(
|
|||||||
|
|
||||||
const commitedPaths = await extractCommitedPaths(
|
const commitedPaths = await extractCommitedPaths(
|
||||||
req.body,
|
req.body,
|
||||||
application.bitbucketOwner,
|
application.bitbucket,
|
||||||
application.bitbucket?.appPassword || "",
|
|
||||||
application.bitbucketRepository || "",
|
application.bitbucketRepository || "",
|
||||||
);
|
);
|
||||||
|
|
||||||
const shouldDeployPaths = shouldDeploy(
|
const shouldDeployPaths = shouldDeploy(
|
||||||
application.watchPaths,
|
application.watchPaths,
|
||||||
commitedPaths,
|
commitedPaths,
|
||||||
@@ -354,9 +359,8 @@ export const getProviderByHeader = (headers: any) => {
|
|||||||
|
|
||||||
export const extractCommitedPaths = async (
|
export const extractCommitedPaths = async (
|
||||||
body: any,
|
body: any,
|
||||||
bitbucketUsername: string | null,
|
bitbucket: Bitbucket | null,
|
||||||
bitbucketAppPassword: string | null,
|
repository: string,
|
||||||
repository: string | null,
|
|
||||||
) => {
|
) => {
|
||||||
const changes = body.push?.changes || [];
|
const changes = body.push?.changes || [];
|
||||||
|
|
||||||
@@ -365,18 +369,16 @@ export const extractCommitedPaths = async (
|
|||||||
.filter(Boolean);
|
.filter(Boolean);
|
||||||
const commitedPaths: string[] = [];
|
const commitedPaths: string[] = [];
|
||||||
for (const commit of commitHashes) {
|
for (const commit of commitHashes) {
|
||||||
const url = `https://api.bitbucket.org/2.0/repositories/${bitbucketUsername}/${repository}/diffstat/${commit}`;
|
const url = `https://api.bitbucket.org/2.0/repositories/${bitbucket?.bitbucketUsername}/${repository}/diffstat/${commit}`;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await fetch(url, {
|
const response = await fetch(url, {
|
||||||
headers: {
|
headers: getBitbucketHeaders(bitbucket!),
|
||||||
Authorization: `Basic ${Buffer.from(`${bitbucketUsername}:${bitbucketAppPassword}`).toString("base64")}`,
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
for (const value of data.values) {
|
for (const value of data.values) {
|
||||||
commitedPaths.push(value.new?.path);
|
if (value?.new?.path) commitedPaths.push(value.new.path);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(
|
console.error(
|
||||||
|
|||||||
@@ -99,8 +99,7 @@ export default async function handler(
|
|||||||
|
|
||||||
const commitedPaths = await extractCommitedPaths(
|
const commitedPaths = await extractCommitedPaths(
|
||||||
req.body,
|
req.body,
|
||||||
composeResult.bitbucketOwner,
|
composeResult.bitbucket,
|
||||||
composeResult.bitbucket?.appPassword || "",
|
|
||||||
composeResult.bitbucketRepository || "",
|
composeResult.bitbucketRepository || "",
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -11,7 +11,9 @@ export const bitbucket = pgTable("bitbucket", {
|
|||||||
.primaryKey()
|
.primaryKey()
|
||||||
.$defaultFn(() => nanoid()),
|
.$defaultFn(() => nanoid()),
|
||||||
bitbucketUsername: text("bitbucketUsername"),
|
bitbucketUsername: text("bitbucketUsername"),
|
||||||
|
bitbucketEmail: text("bitbucketEmail"),
|
||||||
appPassword: text("appPassword"),
|
appPassword: text("appPassword"),
|
||||||
|
apiToken: text("apiToken"),
|
||||||
bitbucketWorkspaceName: text("bitbucketWorkspaceName"),
|
bitbucketWorkspaceName: text("bitbucketWorkspaceName"),
|
||||||
gitProviderId: text("gitProviderId")
|
gitProviderId: text("gitProviderId")
|
||||||
.notNull()
|
.notNull()
|
||||||
@@ -29,7 +31,9 @@ const createSchema = createInsertSchema(bitbucket);
|
|||||||
|
|
||||||
export const apiCreateBitbucket = createSchema.extend({
|
export const apiCreateBitbucket = createSchema.extend({
|
||||||
bitbucketUsername: z.string().optional(),
|
bitbucketUsername: z.string().optional(),
|
||||||
|
bitbucketEmail: z.string().email().optional(),
|
||||||
appPassword: z.string().optional(),
|
appPassword: z.string().optional(),
|
||||||
|
apiToken: z.string().optional(),
|
||||||
bitbucketWorkspaceName: z.string().optional(),
|
bitbucketWorkspaceName: z.string().optional(),
|
||||||
gitProviderId: z.string().optional(),
|
gitProviderId: z.string().optional(),
|
||||||
authId: z.string().min(1),
|
authId: z.string().min(1),
|
||||||
@@ -46,9 +50,19 @@ export const apiBitbucketTestConnection = createSchema
|
|||||||
.extend({
|
.extend({
|
||||||
bitbucketId: z.string().min(1),
|
bitbucketId: z.string().min(1),
|
||||||
bitbucketUsername: z.string().optional(),
|
bitbucketUsername: z.string().optional(),
|
||||||
|
bitbucketEmail: z.string().email().optional(),
|
||||||
workspaceName: z.string().optional(),
|
workspaceName: z.string().optional(),
|
||||||
|
apiToken: z.string().optional(),
|
||||||
|
appPassword: z.string().optional(),
|
||||||
})
|
})
|
||||||
.pick({ bitbucketId: true, bitbucketUsername: true, workspaceName: true });
|
.pick({
|
||||||
|
bitbucketId: true,
|
||||||
|
bitbucketUsername: true,
|
||||||
|
bitbucketEmail: true,
|
||||||
|
workspaceName: true,
|
||||||
|
apiToken: true,
|
||||||
|
appPassword: true,
|
||||||
|
});
|
||||||
|
|
||||||
export const apiFindBitbucketBranches = z.object({
|
export const apiFindBitbucketBranches = z.object({
|
||||||
owner: z.string(),
|
owner: z.string(),
|
||||||
@@ -60,6 +74,9 @@ export const apiUpdateBitbucket = createSchema.extend({
|
|||||||
bitbucketId: z.string().min(1),
|
bitbucketId: z.string().min(1),
|
||||||
name: z.string().min(1),
|
name: z.string().min(1),
|
||||||
bitbucketUsername: z.string().optional(),
|
bitbucketUsername: z.string().optional(),
|
||||||
|
bitbucketEmail: z.string().email().optional(),
|
||||||
|
appPassword: z.string().optional(),
|
||||||
|
apiToken: z.string().optional(),
|
||||||
bitbucketWorkspaceName: z.string().optional(),
|
bitbucketWorkspaceName: z.string().optional(),
|
||||||
organizationId: z.string().optional(),
|
organizationId: z.string().optional(),
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -68,10 +68,26 @@ export const updateBitbucket = async (
|
|||||||
input: typeof apiUpdateBitbucket._type,
|
input: typeof apiUpdateBitbucket._type,
|
||||||
) => {
|
) => {
|
||||||
return await db.transaction(async (tx) => {
|
return await db.transaction(async (tx) => {
|
||||||
|
// First get the current bitbucket provider to get gitProviderId
|
||||||
|
const currentProvider = await tx.query.bitbucket.findFirst({
|
||||||
|
where: eq(bitbucket.bitbucketId, bitbucketId),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!currentProvider) {
|
||||||
|
throw new TRPCError({
|
||||||
|
code: "NOT_FOUND",
|
||||||
|
message: "Bitbucket provider not found",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
const result = await tx
|
const result = await tx
|
||||||
.update(bitbucket)
|
.update(bitbucket)
|
||||||
.set({
|
.set({
|
||||||
...input,
|
bitbucketUsername: input.bitbucketUsername,
|
||||||
|
bitbucketEmail: input.bitbucketEmail,
|
||||||
|
appPassword: input.appPassword,
|
||||||
|
apiToken: input.apiToken,
|
||||||
|
bitbucketWorkspaceName: input.bitbucketWorkspaceName,
|
||||||
})
|
})
|
||||||
.where(eq(bitbucket.bitbucketId, bitbucketId))
|
.where(eq(bitbucket.bitbucketId, bitbucketId))
|
||||||
.returning();
|
.returning();
|
||||||
@@ -83,7 +99,7 @@ export const updateBitbucket = async (
|
|||||||
name: input.name,
|
name: input.name,
|
||||||
organizationId: input.organizationId,
|
organizationId: input.organizationId,
|
||||||
})
|
})
|
||||||
.where(eq(gitProvider.gitProviderId, input.gitProviderId))
|
.where(eq(gitProvider.gitProviderId, currentProvider.gitProviderId))
|
||||||
.returning();
|
.returning();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -5,7 +5,10 @@ import type {
|
|||||||
apiBitbucketTestConnection,
|
apiBitbucketTestConnection,
|
||||||
apiFindBitbucketBranches,
|
apiFindBitbucketBranches,
|
||||||
} from "@dokploy/server/db/schema";
|
} from "@dokploy/server/db/schema";
|
||||||
import { findBitbucketById } from "@dokploy/server/services/bitbucket";
|
import {
|
||||||
|
type Bitbucket,
|
||||||
|
findBitbucketById,
|
||||||
|
} from "@dokploy/server/services/bitbucket";
|
||||||
import type { Compose } from "@dokploy/server/services/compose";
|
import type { Compose } from "@dokploy/server/services/compose";
|
||||||
import type { InferResultType } from "@dokploy/server/types/with";
|
import type { InferResultType } from "@dokploy/server/types/with";
|
||||||
import { TRPCError } from "@trpc/server";
|
import { TRPCError } from "@trpc/server";
|
||||||
@@ -23,6 +26,39 @@ export type ComposeWithBitbucket = InferResultType<
|
|||||||
{ bitbucket: true }
|
{ bitbucket: true }
|
||||||
>;
|
>;
|
||||||
|
|
||||||
|
export const getBitbucketCloneUrl = (
|
||||||
|
bitbucketProvider: {
|
||||||
|
apiToken?: string | null;
|
||||||
|
bitbucketUsername?: string | null;
|
||||||
|
appPassword?: string | null;
|
||||||
|
} | null,
|
||||||
|
repoClone: string,
|
||||||
|
) => {
|
||||||
|
if (!bitbucketProvider) {
|
||||||
|
throw new Error("Bitbucket provider is required");
|
||||||
|
}
|
||||||
|
return bitbucketProvider.apiToken
|
||||||
|
? `https://x-token-auth:${bitbucketProvider.apiToken}@${repoClone}`
|
||||||
|
: `https://${bitbucketProvider.bitbucketUsername}:${bitbucketProvider.appPassword}@${repoClone}`;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getBitbucketHeaders = (bitbucketProvider: Bitbucket) => {
|
||||||
|
if (bitbucketProvider.apiToken) {
|
||||||
|
// For API tokens, use HTTP Basic auth with email and token
|
||||||
|
// According to Bitbucket docs: email:token for API calls
|
||||||
|
const email =
|
||||||
|
bitbucketProvider.bitbucketEmail || bitbucketProvider.bitbucketUsername;
|
||||||
|
return {
|
||||||
|
Authorization: `Basic ${Buffer.from(`${email}:${bitbucketProvider.apiToken}`).toString("base64")}`,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// For app passwords, use HTTP Basic auth with username and app password
|
||||||
|
return {
|
||||||
|
Authorization: `Basic ${Buffer.from(`${bitbucketProvider.bitbucketUsername}:${bitbucketProvider.appPassword}`).toString("base64")}`,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
export const cloneBitbucketRepository = async (
|
export const cloneBitbucketRepository = async (
|
||||||
entity: ApplicationWithBitbucket | ComposeWithBitbucket,
|
entity: ApplicationWithBitbucket | ComposeWithBitbucket,
|
||||||
logPath: string,
|
logPath: string,
|
||||||
@@ -51,7 +87,7 @@ export const cloneBitbucketRepository = async (
|
|||||||
const outputPath = join(basePath, appName, "code");
|
const outputPath = join(basePath, appName, "code");
|
||||||
await recreateDirectory(outputPath);
|
await recreateDirectory(outputPath);
|
||||||
const repoclone = `bitbucket.org/${bitbucketOwner}/${bitbucketRepository}.git`;
|
const repoclone = `bitbucket.org/${bitbucketOwner}/${bitbucketRepository}.git`;
|
||||||
const cloneUrl = `https://${bitbucket?.bitbucketUsername}:${bitbucket?.appPassword}@${repoclone}`;
|
const cloneUrl = getBitbucketCloneUrl(bitbucket, repoclone);
|
||||||
try {
|
try {
|
||||||
writeStream.write(`\nCloning Repo ${repoclone} to ${outputPath}: ✅\n`);
|
writeStream.write(`\nCloning Repo ${repoclone} to ${outputPath}: ✅\n`);
|
||||||
const cloneArgs = [
|
const cloneArgs = [
|
||||||
@@ -103,7 +139,7 @@ export const cloneRawBitbucketRepository = async (entity: Compose) => {
|
|||||||
const outputPath = join(basePath, appName, "code");
|
const outputPath = join(basePath, appName, "code");
|
||||||
await recreateDirectory(outputPath);
|
await recreateDirectory(outputPath);
|
||||||
const repoclone = `bitbucket.org/${bitbucketOwner}/${bitbucketRepository}.git`;
|
const repoclone = `bitbucket.org/${bitbucketOwner}/${bitbucketRepository}.git`;
|
||||||
const cloneUrl = `https://${bitbucketProvider?.bitbucketUsername}:${bitbucketProvider?.appPassword}@${repoclone}`;
|
const cloneUrl = getBitbucketCloneUrl(bitbucketProvider, repoclone);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const cloneArgs = [
|
const cloneArgs = [
|
||||||
@@ -153,7 +189,7 @@ export const cloneRawBitbucketRepositoryRemote = async (compose: Compose) => {
|
|||||||
const basePath = COMPOSE_PATH;
|
const basePath = COMPOSE_PATH;
|
||||||
const outputPath = join(basePath, appName, "code");
|
const outputPath = join(basePath, appName, "code");
|
||||||
const repoclone = `bitbucket.org/${bitbucketOwner}/${bitbucketRepository}.git`;
|
const repoclone = `bitbucket.org/${bitbucketOwner}/${bitbucketRepository}.git`;
|
||||||
const cloneUrl = `https://${bitbucketProvider?.bitbucketUsername}:${bitbucketProvider?.appPassword}@${repoclone}`;
|
const cloneUrl = getBitbucketCloneUrl(bitbucketProvider, repoclone);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const cloneCommand = `
|
const cloneCommand = `
|
||||||
@@ -206,7 +242,7 @@ export const getBitbucketCloneCommand = async (
|
|||||||
const outputPath = join(basePath, appName, "code");
|
const outputPath = join(basePath, appName, "code");
|
||||||
await recreateDirectory(outputPath);
|
await recreateDirectory(outputPath);
|
||||||
const repoclone = `bitbucket.org/${bitbucketOwner}/${bitbucketRepository}.git`;
|
const repoclone = `bitbucket.org/${bitbucketOwner}/${bitbucketRepository}.git`;
|
||||||
const cloneUrl = `https://${bitbucketProvider?.bitbucketUsername}:${bitbucketProvider?.appPassword}@${repoclone}`;
|
const cloneUrl = getBitbucketCloneUrl(bitbucketProvider, repoclone);
|
||||||
|
|
||||||
const cloneCommand = `
|
const cloneCommand = `
|
||||||
rm -rf ${outputPath};
|
rm -rf ${outputPath};
|
||||||
@@ -241,9 +277,7 @@ export const getBitbucketRepositories = async (bitbucketId?: string) => {
|
|||||||
while (url) {
|
while (url) {
|
||||||
const response = await fetch(url, {
|
const response = await fetch(url, {
|
||||||
method: "GET",
|
method: "GET",
|
||||||
headers: {
|
headers: getBitbucketHeaders(bitbucketProvider),
|
||||||
Authorization: `Basic ${Buffer.from(`${bitbucketProvider.bitbucketUsername}:${bitbucketProvider.appPassword}`).toString("base64")}`,
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
@@ -279,35 +313,43 @@ export const getBitbucketBranches = async (
|
|||||||
}
|
}
|
||||||
const bitbucketProvider = await findBitbucketById(input.bitbucketId);
|
const bitbucketProvider = await findBitbucketById(input.bitbucketId);
|
||||||
const { owner, repo } = input;
|
const { owner, repo } = input;
|
||||||
const url = `https://api.bitbucket.org/2.0/repositories/${owner}/${repo}/refs/branches?pagelen=100`;
|
let url = `https://api.bitbucket.org/2.0/repositories/${owner}/${repo}/refs/branches?pagelen=1`;
|
||||||
|
let allBranches: {
|
||||||
|
name: string;
|
||||||
|
commit: {
|
||||||
|
sha: string;
|
||||||
|
};
|
||||||
|
}[] = [];
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await fetch(url, {
|
while (url) {
|
||||||
method: "GET",
|
const response = await fetch(url, {
|
||||||
headers: {
|
method: "GET",
|
||||||
Authorization: `Basic ${Buffer.from(`${bitbucketProvider.bitbucketUsername}:${bitbucketProvider.appPassword}`).toString("base64")}`,
|
headers: getBitbucketHeaders(bitbucketProvider),
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!response.ok) {
|
|
||||||
throw new TRPCError({
|
|
||||||
code: "BAD_REQUEST",
|
|
||||||
message: `HTTP error! status: ${response.status}`,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new TRPCError({
|
||||||
|
code: "BAD_REQUEST",
|
||||||
|
message: `HTTP error! status: ${response.status}`,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
const mappedData = data.values.map((branch: any) => {
|
||||||
|
return {
|
||||||
|
name: branch.name,
|
||||||
|
commit: {
|
||||||
|
sha: branch.target.hash,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
});
|
||||||
|
allBranches = allBranches.concat(mappedData);
|
||||||
|
url = data.next || null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const data = await response.json();
|
return allBranches as {
|
||||||
|
|
||||||
const mappedData = data.values.map((branch: any) => {
|
|
||||||
return {
|
|
||||||
name: branch.name,
|
|
||||||
commit: {
|
|
||||||
sha: branch.target.hash,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
return mappedData as {
|
|
||||||
name: string;
|
name: string;
|
||||||
commit: {
|
commit: {
|
||||||
sha: string;
|
sha: string;
|
||||||
@@ -335,9 +377,7 @@ export const testBitbucketConnection = async (
|
|||||||
try {
|
try {
|
||||||
const response = await fetch(url, {
|
const response = await fetch(url, {
|
||||||
method: "GET",
|
method: "GET",
|
||||||
headers: {
|
headers: getBitbucketHeaders(bitbucketProvider),
|
||||||
Authorization: `Basic ${Buffer.from(`${bitbucketProvider.bitbucketUsername}:${bitbucketProvider.appPassword}`).toString("base64")}`,
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
|
|||||||
@@ -362,17 +362,22 @@ export const testGiteaConnection = async (input: { giteaId: string }) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const baseUrl = provider.giteaUrl.replace(/\/+$/, "");
|
const baseUrl = provider.giteaUrl.replace(/\/+$/, "");
|
||||||
const limit = 30;
|
|
||||||
let allRepos = 0;
|
|
||||||
let nextUrl = `${baseUrl}/api/v1/repos/search?limit=${limit}`;
|
|
||||||
|
|
||||||
while (nextUrl) {
|
// Use /user/repos to get authenticated user's repositories with pagination
|
||||||
const response = await fetch(nextUrl, {
|
let allRepos = 0;
|
||||||
headers: {
|
let page = 1;
|
||||||
Accept: "application/json",
|
const limit = 50; // Max per page
|
||||||
Authorization: `token ${provider.accessToken}`,
|
|
||||||
|
while (true) {
|
||||||
|
const response = await fetch(
|
||||||
|
`${baseUrl}/api/v1/user/repos?page=${page}&limit=${limit}`,
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
Accept: "application/json",
|
||||||
|
Authorization: `token ${provider.accessToken}`,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
});
|
);
|
||||||
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
@@ -381,22 +386,18 @@ export const testGiteaConnection = async (input: { giteaId: string }) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const repos = await response.json();
|
const repos = await response.json();
|
||||||
allRepos += repos.data.length;
|
if (!Array.isArray(repos) || repos.length === 0) {
|
||||||
|
break; // No more repositories
|
||||||
const linkHeader = response.headers.get("link");
|
|
||||||
nextUrl = "";
|
|
||||||
|
|
||||||
if (linkHeader) {
|
|
||||||
const nextLink = linkHeader
|
|
||||||
.split(",")
|
|
||||||
.find((link) => link.includes('rel="next"'));
|
|
||||||
if (nextLink) {
|
|
||||||
const matches = nextLink.match(/<([^>]+)>/);
|
|
||||||
if (matches?.[1]) {
|
|
||||||
nextUrl = matches[1];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
allRepos += repos.length;
|
||||||
|
|
||||||
|
// Check if there are more pages
|
||||||
|
if (repos.length < limit) {
|
||||||
|
break; // Last page (fewer results than limit)
|
||||||
|
}
|
||||||
|
|
||||||
|
page++;
|
||||||
}
|
}
|
||||||
|
|
||||||
await updateGitea(giteaId, {
|
await updateGitea(giteaId, {
|
||||||
@@ -418,17 +419,22 @@ export const getGiteaRepositories = async (giteaId?: string) => {
|
|||||||
const giteaProvider = await findGiteaById(giteaId);
|
const giteaProvider = await findGiteaById(giteaId);
|
||||||
|
|
||||||
const baseUrl = giteaProvider.giteaUrl.replace(/\/+$/, "");
|
const baseUrl = giteaProvider.giteaUrl.replace(/\/+$/, "");
|
||||||
const limit = 30;
|
|
||||||
let allRepositories: any[] = [];
|
|
||||||
let nextUrl = `${baseUrl}/api/v1/repos/search?limit=${limit}`;
|
|
||||||
|
|
||||||
while (nextUrl) {
|
// Use /user/repos to get authenticated user's repositories with pagination
|
||||||
const response = await fetch(nextUrl, {
|
let allRepositories: any[] = [];
|
||||||
headers: {
|
let page = 1;
|
||||||
Accept: "application/json",
|
const limit = 50; // Max per page
|
||||||
Authorization: `token ${giteaProvider.accessToken}`,
|
|
||||||
|
while (true) {
|
||||||
|
const response = await fetch(
|
||||||
|
`${baseUrl}/api/v1/user/repos?page=${page}&limit=${limit}`,
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
Accept: "application/json",
|
||||||
|
Authorization: `token ${giteaProvider.accessToken}`,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
});
|
);
|
||||||
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
throw new TRPCError({
|
throw new TRPCError({
|
||||||
@@ -437,23 +443,19 @@ export const getGiteaRepositories = async (giteaId?: string) => {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const result = await response.json();
|
const repos = await response.json();
|
||||||
allRepositories = [...allRepositories, ...result.data];
|
if (!Array.isArray(repos) || repos.length === 0) {
|
||||||
|
break; // No more repositories
|
||||||
const linkHeader = response.headers.get("link");
|
|
||||||
nextUrl = "";
|
|
||||||
|
|
||||||
if (linkHeader) {
|
|
||||||
const nextLink = linkHeader
|
|
||||||
.split(",")
|
|
||||||
.find((link) => link.includes('rel="next"'));
|
|
||||||
if (nextLink) {
|
|
||||||
const matches = nextLink.match(/<([^>]+)>/);
|
|
||||||
if (matches?.[1]) {
|
|
||||||
nextUrl = matches[1];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
allRepositories = [...allRepositories, ...repos];
|
||||||
|
|
||||||
|
// Check if there are more pages
|
||||||
|
if (repos.length < limit) {
|
||||||
|
break; // Last page (fewer results than limit)
|
||||||
|
}
|
||||||
|
|
||||||
|
page++;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -482,25 +484,43 @@ export const getGiteaBranches = async (input: {
|
|||||||
const giteaProvider = await findGiteaById(input.giteaId);
|
const giteaProvider = await findGiteaById(input.giteaId);
|
||||||
|
|
||||||
const baseUrl = giteaProvider.giteaUrl.replace(/\/+$/, "");
|
const baseUrl = giteaProvider.giteaUrl.replace(/\/+$/, "");
|
||||||
const url = `${baseUrl}/api/v1/repos/${input.owner}/${input.repo}/branches`;
|
|
||||||
|
|
||||||
const response = await fetch(url, {
|
// Handle pagination for branches
|
||||||
headers: {
|
let allBranches: any[] = [];
|
||||||
Accept: "application/json",
|
let page = 1;
|
||||||
Authorization: `token ${giteaProvider.accessToken}`,
|
const limit = 50; // Max per page
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!response.ok) {
|
while (true) {
|
||||||
throw new Error(`Failed to fetch branches: ${response.statusText}`);
|
const response = await fetch(
|
||||||
|
`${baseUrl}/api/v1/repos/${input.owner}/${input.repo}/branches?page=${page}&limit=${limit}`,
|
||||||
|
{
|
||||||
|
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();
|
||||||
|
if (!Array.isArray(branches) || branches.length === 0) {
|
||||||
|
break; // No more branches
|
||||||
|
}
|
||||||
|
|
||||||
|
allBranches = [...allBranches, ...branches];
|
||||||
|
|
||||||
|
// Check if there are more pages
|
||||||
|
if (branches.length < limit) {
|
||||||
|
break; // Last page (fewer results than limit)
|
||||||
|
}
|
||||||
|
|
||||||
|
page++;
|
||||||
}
|
}
|
||||||
|
|
||||||
const branches = await response.json();
|
return allBranches?.map((branch: any) => ({
|
||||||
|
|
||||||
if (!branches) {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
return branches?.map((branch: any) => ({
|
|
||||||
id: branch.name,
|
id: branch.name,
|
||||||
name: branch.name,
|
name: branch.name,
|
||||||
commit: {
|
commit: {
|
||||||
|
|||||||
Reference in New Issue
Block a user