From 3955267bbe5eac816f8e80a9e6a41dbcefdd75b0 Mon Sep 17 00:00:00 2001 From: Anso Date: Wed, 15 Apr 2026 08:05:10 -0400 Subject: [PATCH] feat(git-sources): create a stack from a Git repository (#606) * refactor(git-sources): extract GitSourceFields from GitSourcePanel Pure extraction of the repo/branch/path/auth/apply-mode form fields into a reusable controlled component so the upcoming Create Stack from Git flow can render the same form in the Create Stack dialog. No behavior change. * feat(git-sources): create a stack from a Git repository Add a From Git tab to the Create Stack dialog so users can name a new stack, point it at a repo + branch + compose path, and have the compose fetched, validated, written to disk, and linked in one shot. Optional deploy-after-create runs the initial bring-up when requested. Backend: new POST /api/stacks/from-git route gated by stack:create. GitSourceService.createStackFromGit() fetches and validates before touching disk, then creates the stack, writes the compose (and .env if sync is enabled), and seeds the git source row with the fetched commit so future pulls produce a clean diff. Runs under the per-stack lock so a concurrent webhook cannot race the create. Deploy failure is non-fatal and surfaced to the caller. Frontend: the existing Create Stack dialog is now tabbed, with Empty keeping the original single-field flow unchanged. * test(git-sources): cover create-from-git endpoint and e2e flow Service tests verify createStackFromGit seeds the last_applied columns on success, writes the env file when sync is enabled, refuses an invalid apply-matrix without fetching, rejects invalid compose without leaving orphan state, and rolls back the on-disk stack dir when a post-create step fails. Route tests cover auth, missing stack_name, invalid stack name, http:// rejection, oversized repo_url, and the 409 collision guard. E2E adds a Create-stack-from-Git block covering tab visibility, client-side HTTPS check, backend .git/config rejection, and a happy-path fetch against a public demo repo (skipped on network failure). * docs(git-sources): document create-stack-from-git tab Add a new section near the top describing the From Git tab in the Create Stack dialog: what it does, the Deploy after create checkbox, and the four failure modes (name collision, unreachable repo, invalid compose, deploy-after-create failure). --- .../src/__tests__/git-source-routes.test.ts | 62 ++++++ .../src/__tests__/git-source-service.test.ts | 149 +++++++++++++ backend/src/index.ts | 113 ++++++++++ backend/src/services/GitSourceService.ts | 130 +++++++++++ docs/features/git-sources.mdx | 21 ++ .../git-sources/create-from-git-tab.png | Bin 0 -> 58541 bytes e2e/git-sources.spec.ts | 114 ++++++++++ frontend/src/components/EditorLayout.tsx | 208 ++++++++++++++++-- .../src/components/stack/GitSourceFields.tsx | 203 +++++++++++++++++ .../src/components/stack/GitSourcePanel.tsx | 158 ++----------- 10 files changed, 1006 insertions(+), 152 deletions(-) create mode 100644 docs/images/git-sources/create-from-git-tab.png create mode 100644 frontend/src/components/stack/GitSourceFields.tsx diff --git a/backend/src/__tests__/git-source-routes.test.ts b/backend/src/__tests__/git-source-routes.test.ts index 001ddf97..e5c0e912 100644 --- a/backend/src/__tests__/git-source-routes.test.ts +++ b/backend/src/__tests__/git-source-routes.test.ts @@ -165,6 +165,68 @@ describe('git-source routes — invalid stack names', () => { }); }); +describe('POST /api/stacks/from-git', () => { + const validBody = { + stack_name: 'route-from-git', + repo_url: 'https://github.com/example/repo.git', + branch: 'main', + compose_path: 'compose.yaml', + auth_type: 'none' as const, + }; + + it('returns 401 without auth', async () => { + const res = await request(app).post('/api/stacks/from-git').send(validBody); + expect(res.status).toBe(401); + }); + + it('rejects missing stack_name with 400', async () => { + const { stack_name: _unused, ...body } = validBody; + void _unused; + const res = await request(app) + .post('/api/stacks/from-git') + .set('Authorization', `Bearer ${adminToken()}`) + .send(body); + expect(res.status).toBe(400); + expect(res.body.error).toMatch(/stack_name/i); + }); + + it('rejects invalid stack name with 400', async () => { + const res = await request(app) + .post('/api/stacks/from-git') + .set('Authorization', `Bearer ${adminToken()}`) + .send({ ...validBody, stack_name: '../escape' }); + expect(res.status).toBe(400); + expect(res.body.error).toMatch(/stack name/i); + }); + + it('rejects http:// URLs with 400', async () => { + const res = await request(app) + .post('/api/stacks/from-git') + .set('Authorization', `Bearer ${adminToken()}`) + .send({ ...validBody, repo_url: 'http://github.com/example/repo.git' }); + expect(res.status).toBe(400); + expect(res.body.error).toMatch(/HTTPS/i); + }); + + it('rejects oversized repo_url with 400', async () => { + const res = await request(app) + .post('/api/stacks/from-git') + .set('Authorization', `Bearer ${adminToken()}`) + .send({ ...validBody, repo_url: 'https://example.com/' + 'a'.repeat(2048) }); + expect(res.status).toBe(400); + expect(res.body.error).toMatch(/repo_url/i); + }); + + it('returns 409 when a stack with that name already exists on disk', async () => { + const res = await request(app) + .post('/api/stacks/from-git') + .set('Authorization', `Bearer ${adminToken()}`) + .send({ ...validBody, stack_name: 'existing-stack' }); + expect(res.status).toBe(409); + expect(res.body.error).toMatch(/already exists/i); + }); +}); + describe('GET /api/git-sources', () => { it('returns 200 and a JSON array for an authenticated admin', async () => { const res = await request(app) diff --git a/backend/src/__tests__/git-source-service.test.ts b/backend/src/__tests__/git-source-service.test.ts index 51243c0b..dde02917 100644 --- a/backend/src/__tests__/git-source-service.test.ts +++ b/backend/src/__tests__/git-source-service.test.ts @@ -493,6 +493,155 @@ describe('GitSourceService.pull', () => { }); }); +describe('GitSourceService.createStackFromGit', () => { + async function cleanupStackDir(name: string) { + const { FileSystemService } = await import('../services/FileSystemService'); + try { + await FileSystemService.getInstance().deleteStack(name); + } catch { + // directory may not exist; ignore + } + } + + it('creates a stack on disk, writes compose, and seeds last_applied columns', async () => { + const sha = 'fedcba9876543210fedcba9876543210fedcba98'; + mockSuccessfulClone({ + compose: 'services:\n web:\n image: nginx\n', + sha, + }); + const svc = GitSourceService.getInstance(); + + const result = await svc.createStackFromGit({ + stackName: 'create-happy', + repoUrl: 'https://github.com/example/repo.git', + branch: 'main', + composePath: 'compose.yaml', + syncEnv: false, + envPath: null, + authType: 'none', + token: null, + autoApplyOnWebhook: false, + autoDeployOnApply: false, + }); + + expect(result.commitSha).toBe(sha); + expect(result.envWritten).toBe(false); + expect(result.source.last_applied_commit_sha).toBe(sha); + expect(result.source.pending_commit_sha).toBeNull(); + + const { FileSystemService } = await import('../services/FileSystemService'); + const onDisk = await FileSystemService.getInstance().getStackContent('create-happy'); + expect(onDisk).toContain('image: nginx'); + + await cleanupStackDir('create-happy'); + }); + + it('writes the env file when sync_env is enabled', async () => { + const sha = '0101010101010101010101010101010101010101'; + mockSuccessfulClone({ + compose: 'services:\n web:\n image: nginx\n', + env: 'FOO=bar\n', + envPath: '.env', + sha, + }); + const svc = GitSourceService.getInstance(); + + const result = await svc.createStackFromGit({ + stackName: 'create-env', + repoUrl: 'https://github.com/example/repo.git', + branch: 'main', + composePath: 'compose.yaml', + syncEnv: true, + envPath: '.env', + authType: 'none', + token: null, + autoApplyOnWebhook: false, + autoDeployOnApply: false, + }); + expect(result.envWritten).toBe(true); + + const { FileSystemService } = await import('../services/FileSystemService'); + const env = await FileSystemService.getInstance().getEnvContent('create-env'); + expect(env).toBe('FOO=bar\n'); + + await cleanupStackDir('create-env'); + }); + + it('rejects an invalid apply-matrix without fetching or writing disk', async () => { + const svc = GitSourceService.getInstance(); + await expect(svc.createStackFromGit({ + stackName: 'create-bad-matrix', + repoUrl: 'https://github.com/example/repo.git', + branch: 'main', + composePath: 'compose.yaml', + syncEnv: false, + envPath: null, + authType: 'none', + token: null, + autoApplyOnWebhook: false, + autoDeployOnApply: true, + })).rejects.toBeInstanceOf(GitSourceError); + + expect(mockGitClone).not.toHaveBeenCalled(); + expect(DatabaseService.getInstance().getGitSource('create-bad-matrix')).toBeUndefined(); + }); + + it('rejects when compose validation fails and leaves no stack/row behind', async () => { + mockSuccessfulClone({ + // Non-mapping root is rejected by validateCompose() pre-check + compose: '- not-a-mapping\n', + }); + const svc = GitSourceService.getInstance(); + + await expect(svc.createStackFromGit({ + stackName: 'create-bad-yaml', + repoUrl: 'https://github.com/example/repo.git', + branch: 'main', + composePath: 'compose.yaml', + syncEnv: false, + envPath: null, + authType: 'none', + token: null, + autoApplyOnWebhook: false, + autoDeployOnApply: false, + })).rejects.toMatchObject({ code: 'GIT_ERROR' }); + + expect(DatabaseService.getInstance().getGitSource('create-bad-yaml')).toBeUndefined(); + const { FileSystemService } = await import('../services/FileSystemService'); + const stacks = await FileSystemService.getInstance().getStacks(); + expect(stacks).not.toContain('create-bad-yaml'); + }); + + it('rolls back the stack dir when a post-create step fails', async () => { + mockSuccessfulClone({ + compose: 'services:\n web:\n image: nginx\n', + }); + const { FileSystemService } = await import('../services/FileSystemService'); + const saveSpy = vi.spyOn(FileSystemService.prototype, 'saveStackContent') + .mockRejectedValueOnce(new Error('simulated disk failure')); + + const svc = GitSourceService.getInstance(); + await expect(svc.createStackFromGit({ + stackName: 'create-rollback', + repoUrl: 'https://github.com/example/repo.git', + branch: 'main', + composePath: 'compose.yaml', + syncEnv: false, + envPath: null, + authType: 'none', + token: null, + autoApplyOnWebhook: false, + autoDeployOnApply: false, + })).rejects.toThrow(/simulated disk failure/); + + expect(DatabaseService.getInstance().getGitSource('create-rollback')).toBeUndefined(); + const stacks = await FileSystemService.getInstance().getStacks(); + expect(stacks).not.toContain('create-rollback'); + + saveSpy.mockRestore(); + }); +}); + describe('GitSourceService.apply', () => { async function seedPending(stackName: string, composeContent: string, commitSha: string) { mockSuccessfulClone({ compose: composeContent, sha: commitSha }); diff --git a/backend/src/index.ts b/backend/src/index.ts index fd001b20..738f36a2 100644 --- a/backend/src/index.ts +++ b/backend/src/index.ts @@ -3931,6 +3931,119 @@ app.post('/api/stacks', async (req: Request, res: Response) => { } }); +app.post('/api/stacks/from-git', async (req: Request, res: Response) => { + if (!requirePermission(req, res, 'stack:create')) return; + try { + const { + stack_name, + repo_url, + branch, + compose_path, + sync_env, + env_path, + auth_type, + token, + auto_apply_on_webhook, + auto_deploy_on_apply, + deploy_now, + } = req.body ?? {}; + + if (typeof stack_name !== 'string' || !stack_name.trim()) { + return res.status(400).json({ error: 'stack_name is required' }); + } + if (!isValidStackName(stack_name)) { + return res.status(400).json({ error: 'Stack name can only contain alphanumeric characters, hyphens, and underscores' }); + } + if (typeof repo_url !== 'string' || !repo_url.trim()) { + return res.status(400).json({ error: 'repo_url is required' }); + } + if (typeof branch !== 'string' || !branch.trim()) { + return res.status(400).json({ error: 'branch is required' }); + } + if (typeof compose_path !== 'string' || !compose_path.trim()) { + return res.status(400).json({ error: 'compose_path is required' }); + } + const resolvedAuthType = auth_type === 'token' ? 'token' : 'none'; + if (!/^https:\/\//i.test(repo_url)) { + return res.status(400).json({ error: 'Only HTTPS repository URLs are supported' }); + } + if (repo_url.length > 2048) { + return res.status(400).json({ error: 'repo_url is too long' }); + } + if (branch.length > 256) { + return res.status(400).json({ error: 'branch is too long' }); + } + if (compose_path.length > 1024) { + return res.status(400).json({ error: 'compose_path is too long' }); + } + if (typeof env_path === 'string' && env_path.length > 1024) { + return res.status(400).json({ error: 'env_path is too long' }); + } + if (typeof token === 'string' && token.length > 8192) { + return res.status(400).json({ error: 'token is too long' }); + } + + // Reject if a stack with this name already exists on disk. Without this + // the service would catch it at createStack() time, but erroring early + // avoids spinning up a temp clone we will not use. + const stacks = await FileSystemService.getInstance(req.nodeId).getStacks(); + if (stacks.includes(stack_name)) { + return res.status(409).json({ error: 'Stack already exists' }); + } + + const syncEnv = Boolean(sync_env); + const resolvedEnvPath = syncEnv + ? (typeof env_path === 'string' && env_path.trim() + ? env_path + : path.posix.join(path.posix.dirname(compose_path.replace(/\\/g, '/')) || '.', '.env')) + : null; + + const result = await GitSourceService.getInstance().createStackFromGit({ + stackName: stack_name.trim(), + repoUrl: repo_url.trim(), + branch: branch.trim(), + composePath: compose_path.trim(), + syncEnv, + envPath: resolvedEnvPath, + authType: resolvedAuthType, + token: resolvedAuthType === 'token' && typeof token === 'string' && token !== '' ? token : null, + autoApplyOnWebhook: Boolean(auto_apply_on_webhook), + autoDeployOnApply: Boolean(auto_deploy_on_apply), + }); + + invalidateNodeCaches(req.nodeId); + + // Deploy is best-effort. The compose file is already on disk and the + // git source is linked, so a deploy failure does not roll back the + // stack; the user can retry the deploy from the editor. This mirrors + // the apply-then-deploy behavior in GitSourceService.apply(). + let deployed = false; + let deployError: string | undefined; + if (deploy_now === true) { + try { + await ComposeService.getInstance(req.nodeId).deployStack(stack_name); + deployed = true; + invalidateNodeCaches(req.nodeId); + } catch (e) { + deployError = getErrorMessage(e, 'Deploy failed'); + console.error(`[Stacks] Deploy after create-from-git failed for ${stack_name}:`, deployError); + } + } + + console.log(`[Stacks] Stack created from Git: ${stack_name} at ${result.commitSha.slice(0, 7)}`); + res.json({ + name: stack_name, + source: result.source, + commitSha: result.commitSha, + envWritten: result.envWritten, + deployed, + deployError, + }); + } catch (error) { + sendGitSourceError(res, error); + } +}); + app.delete('/api/stacks/:stackName', async (req: Request, res: Response) => { const stackName = req.params.stackName as string; if (!requirePermission(req, res, 'stack:delete', 'stack', stackName)) return; diff --git a/backend/src/services/GitSourceService.ts b/backend/src/services/GitSourceService.ts index de924420..dd755060 100644 --- a/backend/src/services/GitSourceService.ts +++ b/backend/src/services/GitSourceService.ts @@ -65,6 +65,25 @@ export interface UpsertInput { autoDeployOnApply: boolean; } +export interface CreateStackFromGitInput { + stackName: string; + repoUrl: string; + branch: string; + composePath: string; + syncEnv: boolean; + envPath: string | null; + authType: GitSourceAuthType; + token: string | null; + autoApplyOnWebhook: boolean; + autoDeployOnApply: boolean; +} + +export interface CreateStackFromGitResult { + source: PublicGitSource; + commitSha: string; + envWritten: boolean; +} + export interface PullResult { commitSha: string; incomingCompose: string; @@ -677,6 +696,117 @@ export class GitSourceService { DatabaseService.getInstance().clearGitSourcePending(stackName); } + // ─── Create stack from Git ─────────────────────────────────────────────── + + /** + * Fetch a compose file from a Git repository and use it to create a + * brand-new stack on disk + the matching git-source row. The caller is + * responsible for rolling back (deleteStack + deleteGitSource) if a + * later step such as an optional deploy fails; this method itself will + * undo its own partial state if anything *before* the DB insert fails. + * + * Serialized under the same per-stack mutex as pull/apply so a racing + * webhook cannot collide with a fresh create. + */ + public async createStackFromGit(input: CreateStackFromGitInput): Promise { + return this.withStackLock(input.stackName, async () => { + const fsSvc = FileSystemService.getInstance(); + const db = DatabaseService.getInstance(); + const diag = isDebugEnabled(); + + if (input.autoDeployOnApply && !input.autoApplyOnWebhook) { + throw new GitSourceError('GIT_ERROR', 'Auto-deploy requires auto-apply-on-webhook to be enabled.'); + } + + // 1. Fetch from git BEFORE touching disk or DB. If the fetch + // fails there is nothing to clean up. + const fetched = await this.fetchFromGit({ + repoUrl: input.repoUrl, + branch: input.branch, + composePath: input.composePath, + envPath: input.syncEnv ? input.envPath : null, + token: input.token, + }); + + // 2. Validate against the same `docker compose config` check the + // apply path uses. Reject before creating anything on disk. + const validation = await this.validateCompose(fetched.composeContent, fetched.envContent); + if (!validation.ok) { + throw new GitSourceError('GIT_ERROR', `Compose validation failed: ${validation.error}`); + } + + // 3. Create directory + boilerplate, then overwrite with the + // fetched content. createStack() throws if the directory + // already exists, so a name collision is caught here. + let stackCreated = false; + try { + await fsSvc.createStack(input.stackName); + stackCreated = true; + await fsSvc.saveStackContent(input.stackName, fetched.composeContent); + let envWritten = false; + if (input.syncEnv && fetched.envContent !== null) { + await fsSvc.saveEnvContent(input.stackName, fetched.envContent); + envWritten = true; + } + + // 4. Insert the git-source row, then mark it applied so future + // pulls diff against the fetched commit rather than treating + // it as "local edits detected". + const encryptedToken = input.authType === 'token' && input.token + ? this.crypto.encrypt(input.token) + : null; + db.upsertGitSource({ + stack_name: input.stackName, + repo_url: input.repoUrl, + branch: input.branch, + compose_path: input.composePath, + sync_env: input.syncEnv, + env_path: input.syncEnv ? input.envPath : null, + auth_type: input.authType, + encrypted_token: encryptedToken, + auto_apply_on_webhook: input.autoApplyOnWebhook, + auto_deploy_on_apply: input.autoDeployOnApply, + last_applied_commit_sha: fetched.commitSha, + last_applied_content_hash: this.hashContent(fetched.composeContent, fetched.envContent), + pending_commit_sha: null, + pending_compose_content: null, + pending_env_content: null, + pending_fetched_at: null, + last_debounce_at: null, + }); + db.markGitSourceApplied( + input.stackName, + fetched.commitSha, + this.hashContent(fetched.composeContent, fetched.envContent), + ); + + const source = this.get(input.stackName); + if (!source) { + throw new GitSourceError('GIT_ERROR', 'Failed to read back created git source.'); + } + + console.log(`[GitSource] Created stack ${input.stackName} from ${repoHost(input.repoUrl)} at ${fetched.commitSha.slice(0, 7)}`); + if (diag) { + console.log(`[GitSource:diag] createStackFromGit ok stack=${input.stackName} sha=${fetched.commitSha.slice(0, 7)} envWritten=${envWritten}`); + } + return { source, commitSha: fetched.commitSha, envWritten }; + } catch (e) { + // Roll back any partial on-disk state so the caller can retry + // cleanly. The DB row is only inserted at step 4, so an error + // earlier leaves nothing to clean in the DB. + if (stackCreated) { + try { + await fsSvc.deleteStack(input.stackName); + } catch (cleanupErr) { + console.error(`[GitSource] Rollback: failed to remove partial stack dir ${input.stackName}:`, cleanupErr); + } + } + db.deleteGitSource(input.stackName); + throw e; + } + }); + } + // ─── Webhook-triggered pull ────────────────────────────────────────────── /** diff --git a/docs/features/git-sources.mdx b/docs/features/git-sources.mdx index a31b48bf..a9be7196 100644 --- a/docs/features/git-sources.mdx +++ b/docs/features/git-sources.mdx @@ -16,6 +16,27 @@ Git Sources are available to all Sencho users on the Community tier. Writes land in the stack's existing directory using the same storage Sencho uses for the in-browser editor. Existing history, alerts, and metrics are unaffected. +## Create a stack from a Git repository + +Skip the "empty stack then link later" detour and point at a repo from the start. Click **Create Stack** in the sidebar, switch to the **From Git** tab, and fill in the same fields you would on the Git Source panel. + + + Create Stack dialog with the From Git tab selected, showing stack name, repository URL, branch, compose path, and a Deploy after create checkbox + + +Sencho fetches the compose file, validates it with `docker compose config`, writes it to a fresh stack directory, and links the git source in one step. The last-applied commit sha is seeded from the fetch so the first pull produces a clean diff rather than a "local edits detected" warning. + +Tick **Deploy after create** to run `docker compose up -d` immediately after the files land. If the deploy fails, the stack and git source are kept on disk so you can fix the underlying issue (missing image, port conflict, host resources) and retry the deploy from the editor. + +### Failure modes on create + +| Situation | What happens | +|-----------|--------------| +| Stack name already exists | Sencho returns **409** with "Stack already exists" and makes no changes on disk or in the database. Pick a different name or remove the existing stack. | +| Repository unreachable or auth failed | Fetch fails before anything is created. The form stays open with an error toast describing the cause. | +| Fetched compose fails validation | The stack directory is not created and no git source row is inserted. The error toast shows the `docker compose config` message. | +| Fetch + validate succeed but optional deploy fails | The stack and git source are kept. The toast reads "Stack created, but deploy failed: ..." and you can retry the deploy from the editor. | + ## Configure a source diff --git a/docs/images/git-sources/create-from-git-tab.png b/docs/images/git-sources/create-from-git-tab.png new file mode 100644 index 0000000000000000000000000000000000000000..a4bf6531fd3ab0bc8cdbe6a72b730fab5bed6b5b GIT binary patch literal 58541 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%0A1PgvS9S=+YZufjV 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 XVebl(a3u3^cn1~0 { await expect(page.getByRole('dialog').getByRole('button', { name: /^Remove$/ })).not.toBeVisible({ timeout: 5_000 }); }); }); + +const CREATE_FROM_GIT_STACK = 'e2e-create-from-git'; + +async function deleteCreateFromGitStack(page: Page) { + await page.evaluate(async (name) => { + await fetch(`/api/stacks/${name}/git-source`, { method: 'DELETE', credentials: 'include' }).catch(() => {}); + await fetch(`/api/stacks/${name}`, { method: 'DELETE', credentials: 'include' }).catch(() => {}); + }, CREATE_FROM_GIT_STACK); +} + +async function openCreateStackDialog(page: Page) { + await page.getByRole('button', { name: 'Create Stack' }).click(); + await expect(page.getByRole('dialog').getByText('Create New Stack')).toBeVisible({ timeout: 5_000 }); +} + +test.describe('Create stack from Git', () => { + test.beforeAll(async ({ browser }) => { + const page = await browser.newPage(); + await loginAs(page); + await deleteCreateFromGitStack(page); + await page.close(); + }); + + test.afterAll(async ({ browser }) => { + const page = await browser.newPage(); + await loginAs(page); + await deleteCreateFromGitStack(page); + await page.close(); + }); + + test.beforeEach(async ({ page }) => { + await loginAs(page); + await expect(page.getByRole('button', { name: 'Create Stack' })).toBeVisible({ timeout: 15_000 }); + await expect(page.locator('[data-stacks-loaded="true"]')).toBeAttached({ timeout: 15_000 }); + }); + + test('dialog exposes Empty and From Git tabs', async ({ page }) => { + await openCreateStackDialog(page); + await expect(page.getByRole('dialog').getByRole('tab', { name: /Empty/i })).toBeVisible(); + await expect(page.getByRole('dialog').getByRole('tab', { name: /From Git/i })).toBeVisible(); + }); + + test('From Git tab rejects non-HTTPS URLs client-side', async ({ page }) => { + await openCreateStackDialog(page); + await page.getByRole('dialog').getByRole('tab', { name: /From Git/i }).click(); + + await page.locator('#create-git-stack-name').fill(CREATE_FROM_GIT_STACK); + await page.locator('#git-source-repo').fill('git@github.com:org/repo.git'); + await page.locator('#git-source-branch').fill('main'); + await page.locator('#git-source-path').fill('compose.yaml'); + + await page.getByRole('dialog').getByRole('button', { name: /Create from Git/i }).click(); + await expect(page.getByText(/Only HTTPS repository URLs are supported/i)).toBeVisible({ timeout: 5_000 }); + }); + + test('backend rejects .git/config compose_path on from-git', async ({ page }) => { + const body = await page.evaluate(async (name) => { + const res = await fetch(`/api/stacks/from-git`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + credentials: 'include', + body: JSON.stringify({ + stack_name: name, + repo_url: 'https://github.com/example/repo.git', + branch: 'main', + compose_path: '.git/config', + auth_type: 'none', + }), + }); + return { status: res.status, body: await res.json().catch(() => ({})) }; + }, CREATE_FROM_GIT_STACK); + expect(body.status).toBeGreaterThanOrEqual(400); + expect(JSON.stringify(body.body)).toMatch(/\.git|file/i); + }); + + test('happy path: fetches compose, creates stack, links git source', async ({ page }) => { + // Use a public demo repo. If network egress is blocked, the POST fails + // and we skip the rest of the test rather than hanging the suite. + const result = await page.evaluate(async (name) => { + const res = await fetch(`/api/stacks/from-git`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + credentials: 'include', + body: JSON.stringify({ + stack_name: name, + repo_url: 'https://github.com/docker/awesome-compose.git', + branch: 'master', + compose_path: 'nginx-golang/compose.yaml', + auth_type: 'none', + auto_apply_on_webhook: false, + auto_deploy_on_apply: false, + deploy_now: false, + }), + }); + return { status: res.status, body: await res.json().catch(() => ({})) }; + }, CREATE_FROM_GIT_STACK); + + if (result.status >= 400) { + test.skip(true, `Upstream unreachable (status ${result.status}); skipping happy path`); + return; + } + expect(result.status).toBe(200); + expect(result.body?.source?.stack_name).toBe(CREATE_FROM_GIT_STACK); + expect(result.body?.source?.last_applied_commit_sha).toBeTruthy(); + + // Stack dir should now exist and the compose should contain the upstream service names. + const contentStatus = await page.evaluate(async (name) => { + const res = await fetch(`/api/stacks/${name}`, { credentials: 'include' }); + return { status: res.status, body: await res.text() }; + }, CREATE_FROM_GIT_STACK); + expect(contentStatus.status).toBe(200); + expect(contentStatus.body).toMatch(/services:/); + }); +}); diff --git a/frontend/src/components/EditorLayout.tsx b/frontend/src/components/EditorLayout.tsx index e27ecc6f..4977b747 100644 --- a/frontend/src/components/EditorLayout.tsx +++ b/frontend/src/components/EditorLayout.tsx @@ -33,6 +33,8 @@ import { toast } from '@/components/ui/toast-store'; import { Label } from './ui/label'; import { Command, CommandInput, CommandList, CommandItem } from './ui/command'; import { ScrollArea } from './ui/scroll-area'; +import { Checkbox } from './ui/checkbox'; +import { GitSourceFields, type ApplyMode } from './stack/GitSourceFields'; import { Skeleton } from './ui/skeleton'; import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from './ui/tooltip'; import { HoverCard, HoverCardContent, HoverCardTrigger } from './ui/hover-card'; @@ -129,8 +131,19 @@ export default function EditorLayout() { const monacoEditorRef = useRef(null); const pendingStackLoadRef = useRef(null); const [createDialogOpen, setCreateDialogOpen] = useState(false); + const [createMode, setCreateMode] = useState<'empty' | 'git'>('empty'); const [deleteDialogOpen, setDeleteDialogOpen] = useState(false); const [newStackName, setNewStackName] = useState(''); + // "From Git" tab state + const [gitRepoUrl, setGitRepoUrl] = useState(''); + const [gitBranch, setGitBranch] = useState('main'); + const [gitComposePath, setGitComposePath] = useState('compose.yaml'); + const [gitSyncEnv, setGitSyncEnv] = useState(false); + const [gitAuthType, setGitAuthType] = useState<'none' | 'token'>('none'); + const [gitToken, setGitToken] = useState(''); + const [gitApplyMode, setGitApplyMode] = useState('review'); + const [gitDeployNow, setGitDeployNow] = useState(false); + const [creatingFromGit, setCreatingFromGit] = useState(false); const [stackToDelete, setStackToDelete] = useState(null); const [pendingUnsavedLoad, setPendingUnsavedLoad] = useState(null); const [isLoading, setIsLoading] = useState(false); @@ -1301,6 +1314,83 @@ export default function EditorLayout() { } }; + const resetCreateFromGitForm = () => { + setNewStackName(''); + setGitRepoUrl(''); + setGitBranch('main'); + setGitComposePath('compose.yaml'); + setGitSyncEnv(false); + setGitAuthType('none'); + setGitToken(''); + setGitApplyMode('review'); + setGitDeployNow(false); + }; + + const handleCreateStackFromGit = async () => { + const stackName = newStackName.trim(); + if (!stackName) { + toast.error('Stack name is required.'); + return; + } + if (!gitRepoUrl.trim() || !gitBranch.trim() || !gitComposePath.trim()) { + toast.error('Repository URL, branch, and compose path are required.'); + return; + } + if (!/^https:\/\//i.test(gitRepoUrl.trim())) { + toast.error('Only HTTPS repository URLs are supported.'); + return; + } + setCreatingFromGit(true); + const loadingId = toast.loading(gitDeployNow ? 'Fetching, creating, and deploying...' : 'Fetching and creating stack...'); + try { + const autoApply = gitApplyMode !== 'review'; + const autoDeploy = gitApplyMode === 'auto-deploy'; + const body: Record = { + stack_name: stackName, + repo_url: gitRepoUrl.trim(), + branch: gitBranch.trim(), + compose_path: gitComposePath.trim(), + sync_env: gitSyncEnv, + auth_type: gitAuthType, + auto_apply_on_webhook: autoApply, + auto_deploy_on_apply: autoDeploy, + deploy_now: gitDeployNow, + }; + if (gitAuthType === 'token' && gitToken !== '') { + body.token = gitToken; + } + const response = await apiFetch('/stacks/from-git', { + method: 'POST', + body: JSON.stringify(body), + }); + if (!response.ok) { + const err = await response.json().catch(() => ({})); + if (response.status === 409) { + throw new Error(err?.error || 'Stack already exists.'); + } + throw new Error(err?.error || 'Failed to create stack from Git.'); + } + const data: { deployed?: boolean; deployError?: string } = await response.json(); + if (gitDeployNow && data.deployError) { + toast.warning(`Stack created, but deploy failed: ${data.deployError}`); + } else if (gitDeployNow && data.deployed) { + toast.success('Stack created and deployed from Git.'); + } else { + toast.success('Stack created from Git.'); + } + setCreateDialogOpen(false); + resetCreateFromGitForm(); + await refreshStacks(); + await loadFile(stackName); + } catch (error) { + console.error('Failed to create stack from Git:', error); + toast.error((error as Error)?.message || 'Failed to create stack from Git.'); + } finally { + toast.dismiss(loadingId); + setCreatingFromGit(false); + } + }; + const openBashModal = (containerId: string, containerName: string) => { setSelectedContainer({ id: containerId, name: containerName }); setBashModalOpen(true); @@ -1427,29 +1517,119 @@ export default function EditorLayout() { {/* Create Stack & Scan Buttons */} {can('stack:create') &&
- + { + setCreateDialogOpen(o); + if (!o) { + setCreateMode('empty'); + resetCreateFromGitForm(); + } + }}> - - + + Create New Stack -
- - setNewStackName(e.target.value)} - /> + +
+ setCreateMode(v as 'empty' | 'git')}> + + + + + + Empty + + + + + + From Git + + + + +
- - - + + {createMode === 'empty' ? ( + <> +
+ + setNewStackName(e.target.value)} + /> +
+ + + + + ) : ( + <> + +
+
+ + setNewStackName(e.target.value)} + disabled={creatingFromGit} + /> +
+ + + +
+ setGitDeployNow(c === true)} + disabled={creatingFromGit} + /> + +
+
+
+ + + + + )}
diff --git a/frontend/src/components/stack/GitSourceFields.tsx b/frontend/src/components/stack/GitSourceFields.tsx new file mode 100644 index 00000000..95a7af67 --- /dev/null +++ b/frontend/src/components/stack/GitSourceFields.tsx @@ -0,0 +1,203 @@ +import { Input } from '@/components/ui/input'; +import { Label } from '@/components/ui/label'; +import { Checkbox } from '@/components/ui/checkbox'; +import { cn } from '@/lib/utils'; + +export type ApplyMode = 'review' | 'auto-write' | 'auto-deploy'; + +export interface GitSourceFieldsState { + repoUrl: string; + branch: string; + composePath: string; + syncEnv: boolean; + authType: 'none' | 'token'; + token: string; + /** When editing an existing source, the server tells us whether a token is already stored. */ + hasStoredToken: boolean; + applyMode: ApplyMode; +} + +export interface GitSourceFieldsProps extends GitSourceFieldsState { + disabled?: boolean; + /** 'edit' for the per-stack panel, 'create' for the new-stack dialog. Changes apply-mode copy. */ + variant: 'edit' | 'create'; + onRepoUrlChange: (value: string) => void; + onBranchChange: (value: string) => void; + onComposePathChange: (value: string) => void; + onSyncEnvChange: (value: boolean) => void; + onAuthTypeChange: (value: 'none' | 'token') => void; + onTokenChange: (value: string) => void; + onApplyModeChange: (value: ApplyMode) => void; +} + +const APPLY_MODE_COPY: Record<'edit' | 'create', Record> = { + edit: { + 'review': { title: 'Review only', description: 'Webhook fetches and flags a pending diff. You apply manually.' }, + 'auto-write': { title: 'Auto-write files', description: 'Webhook writes to disk. You deploy manually.' }, + 'auto-deploy': { title: 'Auto-deploy', description: 'Webhook writes and deploys in one step.' }, + }, + create: { + 'review': { title: 'Review only', description: 'Future webhook pulls surface a diff you apply manually.' }, + 'auto-write': { title: 'Auto-write files', description: 'Future webhook pulls write to disk. You deploy manually.' }, + 'auto-deploy': { title: 'Auto-deploy', description: 'Future webhook pulls write and redeploy automatically.' }, + }, +}; + +export function GitSourceFields({ + repoUrl, + branch, + composePath, + syncEnv, + authType, + token, + hasStoredToken, + applyMode, + disabled = false, + variant, + onRepoUrlChange, + onBranchChange, + onComposePathChange, + onSyncEnvChange, + onAuthTypeChange, + onTokenChange, + onApplyModeChange, +}: GitSourceFieldsProps) { + const copy = APPLY_MODE_COPY[variant]; + + const radioOption = (mode: ApplyMode) => ( + + ); + + return ( +
+
+ + onRepoUrlChange(e.target.value)} + disabled={disabled} + className="font-mono text-xs" + /> +
+ +
+
+ + onBranchChange(e.target.value)} + disabled={disabled} + className="font-mono text-xs" + /> +
+
+ + onComposePathChange(e.target.value)} + disabled={disabled} + className="font-mono text-xs" + /> +
+
+ +
+ onSyncEnvChange(c === true)} + disabled={disabled} + /> + +
+ +
+ +
+ + +
+ {authType === 'token' && ( +
+ onTokenChange(e.target.value)} + disabled={disabled} + className="font-mono text-xs" + autoComplete="off" + /> +

+ Token is encrypted at rest and never returned from the API. +

+
+ )} +
+ +
+ +
+ {radioOption('review')} + {radioOption('auto-write')} + {radioOption('auto-deploy')} +
+
+
+ ); +} diff --git a/frontend/src/components/stack/GitSourcePanel.tsx b/frontend/src/components/stack/GitSourcePanel.tsx index e1fc507a..8e49b6f9 100644 --- a/frontend/src/components/stack/GitSourcePanel.tsx +++ b/frontend/src/components/stack/GitSourcePanel.tsx @@ -12,15 +12,12 @@ import { AlertDialogTitle, } from '@/components/ui/alert-dialog'; import { Button } from '@/components/ui/button'; -import { Input } from '@/components/ui/input'; -import { Label } from '@/components/ui/label'; -import { Checkbox } from '@/components/ui/checkbox'; import { Skeleton } from '@/components/ui/skeleton'; import { ScrollArea } from '@/components/ui/scroll-area'; import { apiFetch } from '@/lib/api'; import { toast } from '@/components/ui/toast-store'; -import { cn } from '@/lib/utils'; import { GitSourceDiffDialog, type PullResult } from './GitSourceDiffDialog'; +import { GitSourceFields, type ApplyMode } from './GitSourceFields'; export interface GitSource { id: number; @@ -41,8 +38,6 @@ export interface GitSource { updated_at: number; } -type ApplyMode = 'review' | 'auto-write' | 'auto-deploy'; - interface GitSourcePanelProps { open: boolean; onOpenChange: (open: boolean) => void; @@ -277,33 +272,6 @@ export function GitSourcePanel({ } }; - const radioOption = (mode: ApplyMode, title: string, description: string) => ( - - ); - return ( <> @@ -350,111 +318,25 @@ export function GitSourcePanel({
)} -
- - setRepoUrl(e.target.value)} - disabled={!canEdit || saving} - className="font-mono text-xs" - /> -
- -
-
- - setBranch(e.target.value)} - disabled={!canEdit || saving} - className="font-mono text-xs" - /> -
-
- - setComposePath(e.target.value)} - disabled={!canEdit || saving} - className="font-mono text-xs" - /> -
-
- -
- setSyncEnv(c === true)} - disabled={!canEdit || saving} - /> - -
- -
- -
- - -
- {authType === 'token' && ( -
- setToken(e.target.value)} - disabled={!canEdit || saving} - className="font-mono text-xs" - autoComplete="off" - /> -

- Token is encrypted at rest and never returned from the API. -

-
- )} -
- -
- -
- {radioOption('review', 'Review only', 'Webhook fetches and flags a pending diff. You apply manually.')} - {radioOption('auto-write', 'Auto-write files', 'Webhook writes to disk. You deploy manually.')} - {radioOption('auto-deploy', 'Auto-deploy', 'Webhook writes and deploys in one step.')} -
-
+ {source && (