Compare commits

..

2 Commits

Author SHA1 Message Date
aa12881c4b fix(deployments): deployment kök yolunu sabitle 2026-01-19 13:31:29 +03:00
f8d22cc082 feat(deployments): repo tabanlı kurulum sistemi ekle ve root taramayı kaldır
Root dizin taraması yerine repo URL tabanlı otomatik kurulum sistemine geçiş yapıldı.
Deploy klasörü artık repo URL'sinden otomatik oluşturuluyor. Remote repo
üzerinden branch ve compose dosyası listelemesi eklendi.

- `deploymentsRoot` konfigürasyonu kaldırıldı
- `/deployments/scan` endpoint'i kaldırıldı
- `/deployments/compose-files` endpoint'i eklendi
- `repoUrl` alanı unique ve index olarak işaretlendi
- Proje oluştururken `rootPath` zorunluluğu kaldırıldı
- Deploy klasörü otomatik `deployments/{slug}` formatında oluşturuluyor
- Frontend'de root tarama UI'ı kaldırıldı, compose dosyası listeleme eklendi

BREAKING CHANGE: Root dizin tarama özelliği ve `rootPath` alanı kaldırıldı.
Artık deploymentlar sadece repo URL ile oluşturulabiliyor.
2026-01-19 12:54:33 +03:00
7 changed files with 140 additions and 167 deletions

View File

@@ -29,7 +29,7 @@
- **Log Akışı**: Gerçek zamanlı test loglarının izlenmesi - **Log Akışı**: Gerçek zamanlı test loglarının izlenmesi
### 🚀 Deployment Yönetimi ### 🚀 Deployment Yönetimi
- **Root Tarama**: `DEPLOYMENTS_ROOT_HOST` altında compose dosyası olan projeleri otomatik bulma - **Repo Bazlı Kurulum**: Repo URL ile proje oluşturma ve deploy klasörünü otomatik oluşturma
- **Webhook Tetikleme**: Gitea push event ile otomatik deploy - **Webhook Tetikleme**: Gitea push event ile otomatik deploy
- **Branch Seçimi**: Repo URL girince branch listesi alınır ve seçim yapılır - **Branch Seçimi**: Repo URL girince branch listesi alınır ve seçim yapılır
- **Deploy Geçmişi**: Her deploy için log ve süre kaydı - **Deploy Geçmişi**: Her deploy için log ve süre kaydı
@@ -223,8 +223,8 @@ docker compose up -d --build
### Deployment Yönetimi ### Deployment Yönetimi
1. **Deployments** sayfasına gidin 1. **Deployments** sayfasına gidin
2. **New Deployment** ile root altında taranan projeyi seçin 2. **New Deployment** ile Repo URL girin
3. Repo URL + Branch + Compose dosyasını girin 3. Branch ve Compose dosyasını seçin
4. Kaydettikten sonra **Webhook URL**i Giteada web istemci olarak tanımlayın 4. Kaydettikten sonra **Webhook URL**i Giteada web istemci olarak tanımlayın
#### Webhook Ayarları (Gitea) #### Webhook Ayarları (Gitea)
@@ -251,7 +251,7 @@ docker compose up -d --build
### 📖 API Referansı ### 📖 API Referansı
- **Authentication API'leri**: `/auth/login`, `/auth/me` - **Authentication API'leri**: `/auth/login`, `/auth/me`
- **Test Yönetim API'leri**: CRUD operasyonları, manuel çalıştırma - **Test Yönetim API'leri**: CRUD operasyonları, manuel çalıştırma
- **Deployment API'leri**: `/deployments`, `/deployments/:id`, `/deployments/scan`, `/deployments/branches` - **Deployment API'leri**: `/deployments`, `/deployments/:id`, `/deployments/branches`, `/deployments/compose-files`
- **Webhook Endpoint**: `/api/deployments/webhook/:token` - **Webhook Endpoint**: `/api/deployments/webhook/:token`
- **WebSocket Olayları**: Real-time iletişim ve durum güncellemeleri - **WebSocket Olayları**: Real-time iletişim ve durum güncellemeleri
- **Endpoint Detayları**: Her endpoint için istek/yanıt formatları - **Endpoint Detayları**: Her endpoint için istek/yanıt formatları

View File

@@ -8,8 +8,7 @@ export const config = {
adminUsername: process.env.ADMIN_USERNAME || "admin", adminUsername: process.env.ADMIN_USERNAME || "admin",
adminPassword: process.env.ADMIN_PASSWORD || "password", adminPassword: process.env.ADMIN_PASSWORD || "password",
jwtSecret: process.env.JWT_SECRET || "changeme", jwtSecret: process.env.JWT_SECRET || "changeme",
clientOrigin: process.env.CLIENT_ORIGIN || "http://localhost:5173", clientOrigin: process.env.CLIENT_ORIGIN || "http://localhost:5173"
deploymentsRoot: "/workspace"
}; };
if (!config.jwtSecret) { if (!config.jwtSecret) {

View File

@@ -24,7 +24,7 @@ const DeploymentProjectSchema = new Schema<DeploymentProjectDocument>(
{ {
name: { type: String, required: true, trim: true }, name: { type: String, required: true, trim: true },
rootPath: { type: String, required: true, trim: true }, rootPath: { type: String, required: true, trim: true },
repoUrl: { type: String, required: true, trim: true }, repoUrl: { type: String, required: true, trim: true, unique: true, index: true },
branch: { type: String, required: true, trim: true }, branch: { type: String, required: true, trim: true },
composeFile: { composeFile: {
type: String, type: String,

View File

@@ -39,17 +39,6 @@ router.get("/:id/favicon", async (req, res) => {
return res.status(404).end(); return res.status(404).end();
}); });
router.get("/scan", async (req, res) => {
authMiddleware(req, res, async () => {
try {
const candidates = await deploymentService.scanRoot();
return res.json(candidates);
} catch (err) {
return res.status(500).json({ message: "Root taraması yapılamadı" });
}
});
});
router.get("/branches", async (req, res) => { router.get("/branches", async (req, res) => {
authMiddleware(req, res, async () => { authMiddleware(req, res, async () => {
const repoUrl = req.query.repoUrl as string | undefined; const repoUrl = req.query.repoUrl as string | undefined;
@@ -65,6 +54,22 @@ router.get("/branches", async (req, res) => {
}); });
}); });
router.get("/compose-files", async (req, res) => {
authMiddleware(req, res, async () => {
const repoUrl = req.query.repoUrl as string | undefined;
const branch = req.query.branch as string | undefined;
if (!repoUrl || !branch) {
return res.status(400).json({ message: "repoUrl ve branch gerekli" });
}
try {
const files = await deploymentService.listRemoteComposeFiles(repoUrl, branch);
return res.json({ files });
} catch (err) {
return res.status(400).json({ message: "Compose listesi alınamadı", error: (err as Error).message });
}
});
});
router.get("/metrics/summary", async (req, res) => { router.get("/metrics/summary", async (req, res) => {
authMiddleware(req, res, async () => { authMiddleware(req, res, async () => {
const since = new Date(); const since = new Date();
@@ -123,14 +128,13 @@ router.get("/:id", async (req, res) => {
router.post("/", async (req, res) => { router.post("/", async (req, res) => {
authMiddleware(req, res, async () => { authMiddleware(req, res, async () => {
const { name, rootPath, repoUrl, branch, composeFile, port } = req.body; const { name, repoUrl, branch, composeFile, port } = req.body;
if (!name || !rootPath || !repoUrl || !branch || !composeFile) { if (!name || !repoUrl || !branch || !composeFile) {
return res.status(400).json({ message: "Tüm alanlar gerekli" }); return res.status(400).json({ message: "Tüm alanlar gerekli" });
} }
try { try {
const created = await deploymentService.createProject({ const created = await deploymentService.createProject({
name, name,
rootPath,
repoUrl, repoUrl,
branch, branch,
composeFile, composeFile,

View File

@@ -2,7 +2,6 @@ import fs from "fs";
import path from "path"; import path from "path";
import crypto from "crypto"; import crypto from "crypto";
import { spawn } from "child_process"; import { spawn } from "child_process";
import { config } from "../config/env.js";
import { import {
DeploymentProject, DeploymentProject,
DeploymentProjectDocument, DeploymentProjectDocument,
@@ -14,14 +13,18 @@ import { Settings } from "../models/settings.js";
const composeFileCandidates: ComposeFile[] = ["docker-compose.yml", "docker-compose.dev.yml"]; const composeFileCandidates: ComposeFile[] = ["docker-compose.yml", "docker-compose.dev.yml"];
function normalizeRoot(rootPath: string) { const deploymentsRoot = "/workspace/deployments";
return path.resolve(rootPath);
function slugify(value: string) {
return value
.toLowerCase()
.replace(/\.git$/i, "")
.replace(/[^a-z0-9]+/g, "-")
.replace(/^-+|-+$/g, "");
} }
function isWithinRoot(rootPath: string, targetPath: string) { function normalizeRepoUrl(value: string) {
const resolvedRoot = normalizeRoot(rootPath); return value.trim().replace(/\/+$/, "").replace(/\.git$/i, "");
const resolvedTarget = path.resolve(targetPath);
return resolvedTarget === resolvedRoot || resolvedTarget.startsWith(`${resolvedRoot}${path.sep}`);
} }
function generateWebhookToken() { function generateWebhookToken() {
@@ -132,10 +135,7 @@ async function ensureRepo(project: DeploymentProjectDocument, onData: (line: str
} }
} }
async function runCompose( async function runCompose(project: DeploymentProjectDocument, onData: (line: string) => void) {
project: DeploymentProjectDocument,
onData: (line: string) => void
) {
const composePath = path.join(project.rootPath, project.composeFile); const composePath = path.join(project.rootPath, project.composeFile);
if (!fs.existsSync(composePath)) { if (!fs.existsSync(composePath)) {
throw new Error("Compose dosyası bulunamadı"); throw new Error("Compose dosyası bulunamadı");
@@ -153,31 +153,6 @@ async function runCompose(
class DeploymentService { class DeploymentService {
private running: Map<string, boolean> = new Map(); private running: Map<string, boolean> = new Map();
async scanRoot() {
const rootPath = normalizeRoot(config.deploymentsRoot);
if (!fs.existsSync(rootPath)) {
throw new Error("Deployments root bulunamadı");
}
const entries = await fs.promises.readdir(rootPath, { withFileTypes: true });
const candidates = [];
for (const entry of entries) {
if (!entry.isDirectory()) continue;
if (entry.name.startsWith(".")) continue;
const folderPath = path.join(rootPath, entry.name);
const available = composeFileCandidates.filter((file) =>
fs.existsSync(path.join(folderPath, file))
);
if (available.length === 0) continue;
candidates.push({
name: entry.name,
rootPath: folderPath,
composeFiles: available
});
}
return candidates;
}
async listRemoteBranches(repoUrl: string) { async listRemoteBranches(repoUrl: string) {
const output = await runCommandCapture("git", ["ls-remote", "--heads", repoUrl], process.cwd()); const output = await runCommandCapture("git", ["ls-remote", "--heads", repoUrl], process.cwd());
const branches = output const branches = output
@@ -190,6 +165,24 @@ class DeploymentService {
return branches; return branches;
} }
async listRemoteComposeFiles(repoUrl: string, branch: string) {
await fs.promises.mkdir(deploymentsRoot, { recursive: true });
const tmpBase = await fs.promises.mkdtemp(path.join(deploymentsRoot, ".tmp-"));
try {
await runCommand(
`git clone --depth 1 --single-branch --branch ${branch} ${repoUrl} ${tmpBase}`,
process.cwd(),
() => undefined
);
const available = composeFileCandidates.filter((file) =>
fs.existsSync(path.join(tmpBase, file))
);
return available;
} finally {
await fs.promises.rm(tmpBase, { recursive: true, force: true });
}
}
async ensureSettings() { async ensureSettings() {
const existing = await Settings.findOne(); const existing = await Settings.findOne();
if (existing) return existing; if (existing) return existing;
@@ -216,27 +209,15 @@ class DeploymentService {
async createProject(input: { async createProject(input: {
name: string; name: string;
rootPath: string;
repoUrl: string; repoUrl: string;
branch: string; branch: string;
composeFile: ComposeFile; composeFile: ComposeFile;
port?: number; port?: number;
}) { }) {
const rootPath = path.resolve(input.rootPath); const repoUrl = normalizeRepoUrl(input.repoUrl);
if (!isWithinRoot(config.deploymentsRoot, rootPath)) { const existingRepo = await DeploymentProject.findOne({ repoUrl });
throw new Error("Root path deployments root dışında"); if (existingRepo) {
} throw new Error("Bu repo zaten eklenmiş");
if (!fs.existsSync(rootPath)) {
throw new Error("Root path bulunamadı");
}
const composePath = path.join(rootPath, input.composeFile);
if (!fs.existsSync(composePath)) {
throw new Error("Compose dosyası bulunamadı");
}
const existing = await DeploymentProject.findOne({ rootPath });
if (existing) {
throw new Error("Bu klasör zaten eklenmiş");
} }
let webhookToken = generateWebhookToken(); let webhookToken = generateWebhookToken();
@@ -244,11 +225,23 @@ class DeploymentService {
webhookToken = generateWebhookToken(); webhookToken = generateWebhookToken();
} }
await fs.promises.mkdir(deploymentsRoot, { recursive: true });
const baseName = slugify(path.basename(repoUrl));
const suffix = crypto.randomBytes(3).toString("hex");
const slug = baseName ? `${baseName}-${suffix}` : `deployment-${suffix}`;
const rootPath = path.join(deploymentsRoot, slug);
await fs.promises.mkdir(rootPath, { recursive: true });
const available = await this.listRemoteComposeFiles(repoUrl, input.branch);
if (!available.includes(input.composeFile)) {
throw new Error("Compose dosyası repoda bulunamadı");
}
const env = deriveEnv(input.composeFile); const env = deriveEnv(input.composeFile);
return DeploymentProject.create({ return DeploymentProject.create({
name: input.name, name: input.name,
rootPath, rootPath,
repoUrl: input.repoUrl, repoUrl,
branch: input.branch, branch: input.branch,
composeFile: input.composeFile, composeFile: input.composeFile,
webhookToken, webhookToken,
@@ -269,16 +262,23 @@ class DeploymentService {
) { ) {
const project = await DeploymentProject.findById(id); const project = await DeploymentProject.findById(id);
if (!project) return null; if (!project) return null;
const composePath = path.join(project.rootPath, input.composeFile); const repoUrl = normalizeRepoUrl(input.repoUrl);
if (!fs.existsSync(composePath)) { if (repoUrl !== project.repoUrl) {
throw new Error("Compose dosyası bulunamadı"); const existingRepo = await DeploymentProject.findOne({ repoUrl });
if (existingRepo && existingRepo._id.toString() !== id) {
throw new Error("Bu repo zaten eklenmiş");
}
}
const available = await this.listRemoteComposeFiles(repoUrl, input.branch);
if (!available.includes(input.composeFile)) {
throw new Error("Compose dosyası repoda bulunamadı");
} }
const env = deriveEnv(input.composeFile); const env = deriveEnv(input.composeFile);
const updated = await DeploymentProject.findByIdAndUpdate( const updated = await DeploymentProject.findByIdAndUpdate(
id, id,
{ {
name: input.name, name: input.name,
repoUrl: input.repoUrl, repoUrl,
branch: input.branch, branch: input.branch,
composeFile: input.composeFile, composeFile: input.composeFile,
env, env,

View File

@@ -54,15 +54,8 @@ export interface DeploymentMetrics {
recentRuns: DeploymentRunWithProject[]; recentRuns: DeploymentRunWithProject[];
} }
export interface DeploymentCandidate {
name: string;
rootPath: string;
composeFiles: ComposeFile[];
}
export interface DeploymentInput { export interface DeploymentInput {
name: string; name: string;
rootPath: string;
repoUrl: string; repoUrl: string;
branch: string; branch: string;
composeFile: ComposeFile; composeFile: ComposeFile;
@@ -81,17 +74,12 @@ export async function fetchDeploymentBranches(repoUrl: string): Promise<string[]
return (data as { branches: string[] }).branches; return (data as { branches: string[] }).branches;
} }
export async function scanDeployments(): Promise<DeploymentCandidate[]> {
const { data } = await apiClient.get("/deployments/scan");
return data as DeploymentCandidate[];
}
export async function createDeployment(payload: DeploymentInput): Promise<DeploymentProject> { export async function createDeployment(payload: DeploymentInput): Promise<DeploymentProject> {
const { data } = await apiClient.post("/deployments", payload); const { data } = await apiClient.post("/deployments", payload);
return data as DeploymentProject; return data as DeploymentProject;
} }
export async function updateDeployment(id: string, payload: Omit<DeploymentInput, "rootPath">) { export async function updateDeployment(id: string, payload: DeploymentInput) {
const { data } = await apiClient.put(`/deployments/${id}`, payload); const { data } = await apiClient.put(`/deployments/${id}`, payload);
return data as DeploymentProject; return data as DeploymentProject;
} }
@@ -113,3 +101,13 @@ export async function fetchDeploymentMetrics(): Promise<DeploymentMetrics> {
const { data } = await apiClient.get("/deployments/metrics/summary"); const { data } = await apiClient.get("/deployments/metrics/summary");
return data as DeploymentMetrics; return data as DeploymentMetrics;
} }
export async function fetchDeploymentComposeFiles(
repoUrl: string,
branch: string
): Promise<ComposeFile[]> {
const { data } = await apiClient.get("/deployments/compose-files", {
params: { repoUrl, branch }
});
return (data as { files: ComposeFile[] }).files;
}

View File

@@ -16,13 +16,12 @@ import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from ".
import { import {
createDeployment, createDeployment,
deleteDeployment, deleteDeployment,
DeploymentCandidate,
DeploymentInput, DeploymentInput,
DeploymentProject, DeploymentProject,
fetchDeploymentComposeFiles,
fetchDeploymentBranches, fetchDeploymentBranches,
fetchDeployments, fetchDeployments,
runDeployment, runDeployment,
scanDeployments,
updateDeployment updateDeployment
} from "../api/deployments"; } from "../api/deployments";
import { JobStatusBadge } from "../components/JobStatusBadge"; import { JobStatusBadge } from "../components/JobStatusBadge";
@@ -30,7 +29,6 @@ import { JobStatusBadge } from "../components/JobStatusBadge";
type FormState = { type FormState = {
_id?: string; _id?: string;
name: string; name: string;
rootPath: string;
repoUrl: string; repoUrl: string;
branch: string; branch: string;
composeFile: DeploymentInput["composeFile"]; composeFile: DeploymentInput["composeFile"];
@@ -39,7 +37,6 @@ type FormState = {
const defaultForm: FormState = { const defaultForm: FormState = {
name: "", name: "",
rootPath: "",
repoUrl: "", repoUrl: "",
branch: "main", branch: "main",
composeFile: "docker-compose.yml", composeFile: "docker-compose.yml",
@@ -54,19 +51,15 @@ export function DeploymentsPage() {
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [modalOpen, setModalOpen] = useState(false); const [modalOpen, setModalOpen] = useState(false);
const [saving, setSaving] = useState(false); const [saving, setSaving] = useState(false);
const [scanning, setScanning] = useState(false);
const [candidates, setCandidates] = useState<DeploymentCandidate[]>([]);
const [form, setForm] = useState<FormState>(defaultForm); const [form, setForm] = useState<FormState>(defaultForm);
const [pendingEditId, setPendingEditId] = useState<string | null>(null); const [pendingEditId, setPendingEditId] = useState<string | null>(null);
const [branchOptions, setBranchOptions] = useState<string[]>([]); const [branchOptions, setBranchOptions] = useState<string[]>([]);
const [branchLoading, setBranchLoading] = useState(false); const [branchLoading, setBranchLoading] = useState(false);
const [composeOptions, setComposeOptions] = useState<DeploymentInput["composeFile"][]>([]);
const [composeLoading, setComposeLoading] = useState(false);
const [faviconErrors, setFaviconErrors] = useState<Record<string, boolean>>({}); const [faviconErrors, setFaviconErrors] = useState<Record<string, boolean>>({});
const isEdit = useMemo(() => !!form._id, [form._id]); const isEdit = useMemo(() => !!form._id, [form._id]);
const selectedCandidate = useMemo(
() => candidates.find((c) => c.rootPath === form.rootPath),
[candidates, form.rootPath]
);
const loadDeployments = async () => { const loadDeployments = async () => {
setLoading(true); setLoading(true);
@@ -80,18 +73,6 @@ export function DeploymentsPage() {
} }
}; };
const loadCandidates = async () => {
setScanning(true);
try {
const data = await scanDeployments();
setCandidates(data);
} catch {
toast.error("Root taraması yapılamadı");
} finally {
setScanning(false);
}
};
useEffect(() => { useEffect(() => {
loadDeployments(); loadDeployments();
}, []); }, []);
@@ -100,6 +81,7 @@ export function DeploymentsPage() {
const repoUrl = form.repoUrl.trim(); const repoUrl = form.repoUrl.trim();
if (!repoUrl) { if (!repoUrl) {
setBranchOptions([]); setBranchOptions([]);
setComposeOptions([]);
return; return;
} }
const timer = setTimeout(async () => { const timer = setTimeout(async () => {
@@ -119,6 +101,30 @@ export function DeploymentsPage() {
return () => clearTimeout(timer); return () => clearTimeout(timer);
}, [form.repoUrl, form.branch]); }, [form.repoUrl, form.branch]);
useEffect(() => {
const repoUrl = form.repoUrl.trim();
const branch = form.branch.trim();
if (!repoUrl || !branch) {
setComposeOptions([]);
return;
}
const timer = setTimeout(async () => {
setComposeLoading(true);
try {
const files = await fetchDeploymentComposeFiles(repoUrl, branch);
setComposeOptions(files);
if (files.length > 0 && !files.includes(form.composeFile)) {
setForm((prev) => ({ ...prev, composeFile: files[0] }));
}
} catch {
setComposeOptions([]);
} finally {
setComposeLoading(false);
}
}, 400);
return () => clearTimeout(timer);
}, [form.repoUrl, form.branch, form.composeFile]);
useEffect(() => { useEffect(() => {
const state = location.state as { editDeploymentId?: string } | null; const state = location.state as { editDeploymentId?: string } | null;
if (state?.editDeploymentId) { if (state?.editDeploymentId) {
@@ -139,16 +145,15 @@ export function DeploymentsPage() {
const handleOpenNew = async () => { const handleOpenNew = async () => {
setForm(defaultForm); setForm(defaultForm);
setBranchOptions([]); setBranchOptions([]);
setComposeOptions([]);
setModalOpen(true); setModalOpen(true);
await loadCandidates();
}; };
const handleEdit = (deployment: DeploymentProject) => { const handleEdit = (deployment: DeploymentProject) => {
const { _id, name, rootPath, repoUrl, branch, composeFile, port } = deployment; const { _id, name, repoUrl, branch, composeFile, port } = deployment;
setForm({ setForm({
_id, _id,
name, name,
rootPath,
repoUrl, repoUrl,
branch, branch,
composeFile, composeFile,
@@ -166,14 +171,13 @@ export function DeploymentsPage() {
try { try {
const payload: DeploymentInput = { const payload: DeploymentInput = {
name: form.name, name: form.name,
rootPath: form.rootPath,
repoUrl: form.repoUrl, repoUrl: form.repoUrl,
branch: form.branch, branch: form.branch,
composeFile: form.composeFile, composeFile: form.composeFile,
port: form.port ? Number(form.port) : undefined port: form.port ? Number(form.port) : undefined
}; };
if (!payload.name || !payload.rootPath || !payload.repoUrl || !payload.branch || !payload.composeFile) { if (!payload.name || !payload.repoUrl || !payload.branch || !payload.composeFile) {
toast.error("Tüm alanları doldurun"); toast.error("Tüm alanları doldurun");
setSaving(false); setSaving(false);
return; return;
@@ -374,48 +378,8 @@ export function DeploymentsPage() {
<div className="max-h-[70vh] space-y-4 overflow-y-auto px-5 py-4"> <div className="max-h-[70vh] space-y-4 overflow-y-auto px-5 py-4">
{!isEdit && ( {!isEdit && (
<div className="space-y-2">
<div className="flex items-center justify-between">
<Label>Proje Klasörü</Label>
<Button
variant="outline"
size="sm"
onClick={loadCandidates}
disabled={scanning}
>
{scanning ? "Taranıyor..." : "Yeniden Tara"}
</Button>
</div>
<Select
value={form.rootPath}
onValueChange={(value) => {
const candidate = candidates.find((c) => c.rootPath === value);
setForm((prev) => ({
...prev,
rootPath: value,
name: candidate?.name || prev.name,
composeFile: candidate?.composeFiles[0] || prev.composeFile
}));
}}
>
<SelectTrigger>
<SelectValue placeholder="Root altında proje seçin" />
</SelectTrigger>
<SelectContent>
{candidates.map((candidate) => (
<SelectItem key={candidate.rootPath} value={candidate.rootPath}>
{candidate.name}
</SelectItem>
))}
</SelectContent>
</Select>
<div className="text-xs text-muted-foreground"> <div className="text-xs text-muted-foreground">
{scanning Repo URL girildiğinde branch ve compose dosyaları listelenir.
? "Root dizin taranıyor..."
: candidates.length === 0
? "Root altında compose dosyası bulunan proje yok."
: "Compose dosyası bulunan klasörleri listeler."}
</div>
</div> </div>
)} )}
@@ -491,15 +455,23 @@ export function DeploymentsPage() {
<SelectValue placeholder="Compose seçin" /> <SelectValue placeholder="Compose seçin" />
</SelectTrigger> </SelectTrigger>
<SelectContent> <SelectContent>
{(selectedCandidate?.composeFiles || ["docker-compose.yml", "docker-compose.dev.yml"]).map( {(composeOptions.length > 0
(file) => ( ? composeOptions
: ["docker-compose.yml", "docker-compose.dev.yml"]
).map((file) => (
<SelectItem key={file} value={file}> <SelectItem key={file} value={file}>
{file} {file}
</SelectItem> </SelectItem>
) ))}
)}
</SelectContent> </SelectContent>
</Select> </Select>
<div className="text-xs text-muted-foreground">
{composeLoading
? "Compose dosyaları alınıyor..."
: composeOptions.length > 0
? "Repo üzerindeki compose dosyaları listelendi."
: "Repo URL ve branch sonrası compose dosyaları listelenir."}
</div>
</div> </div>
<div className="space-y-2"> <div className="space-y-2">