diff --git a/.claude/settings.json b/.claude/settings.json index 25df048..77b80d2 100644 --- a/.claude/settings.json +++ b/.claude/settings.json @@ -1,7 +1,8 @@ { "permissions": { "allow": [ - "Bash(npm install:*)" + "Bash(npm install:*)", + "Bash(go build:*)" ] } } diff --git a/internal/deployer/bluegreen.go b/internal/deployer/bluegreen.go index f0a8edc..fd7c593 100644 --- a/internal/deployer/bluegreen.go +++ b/internal/deployer/bluegreen.go @@ -142,24 +142,29 @@ func (d *Deployer) blueGreenDeploy( } // Step 5: Swap NPM proxy to green. - if err := d.store.UpdateDeployStatus(deployID, "configuring_proxy", ""); err != nil { - slog.Warn("update deploy status", "error", err) - } - d.publishDeployStatus(deployID, project.ID, stage.ID, imageTag, "configuring_proxy", "") + var npmProxyID int + if stage.EnableProxy { + if err := d.store.UpdateDeployStatus(deployID, "configuring_proxy", ""); err != nil { + slog.Warn("update deploy status", "error", err) + } + d.publishDeployStatus(deployID, project.ID, stage.ID, imageTag, "configuring_proxy", "") - npmProxyID, err := d.configureProxy(ctx, deployID, settings, containerName, project.Port, subdomain) - if err != nil { - return containerID, 0, instanceID, fmt.Errorf("configure proxy: %w", err) + npmProxyID, err = d.configureProxy(ctx, deployID, settings, containerName, project.Port, subdomain) + if err != nil { + return containerID, 0, instanceID, fmt.Errorf("configure proxy: %w", err) + } + + inst.NpmProxyID = npmProxyID + d.logDeploy(deployID, "Blue-green: proxy swapped to green container", "info") + } else { + d.logDeploy(deployID, "Blue-green: proxy skipped (disabled for this stage)", "info") } - inst.NpmProxyID = npmProxyID inst.Subdomain = subdomain if err := d.store.UpdateInstance(inst); err != nil { slog.Warn("update instance with proxy ID", "error", err) } - d.logDeploy(deployID, "Blue-green: proxy swapped to green container", "info") - // Step 6: Stop the blue container. if blueInstance != nil { d.logDeploy(deployID, fmt.Sprintf("Blue-green: stopping blue instance %s (tag: %s)", blueInstance.ID, blueInstance.ImageTag), "info") diff --git a/internal/deployer/deployer.go b/internal/deployer/deployer.go index 9290f99..99e12e4 100644 --- a/internal/deployer/deployer.go +++ b/internal/deployer/deployer.go @@ -336,22 +336,30 @@ func (d *Deployer) executeDeploy( d.publishInstanceStatus(instanceID, project.ID, stage.ID, "running") d.logDeploy(deployID, "Container started", "info") - // Step 4: Configure NPM proxy. - if err := d.store.UpdateDeployStatus(deployID, "configuring_proxy", ""); err != nil { - slog.Warn("update deploy status", "error", err) - } - d.publishDeployStatus(deployID, project.ID, stage.ID, imageTag, "configuring_proxy", "") + // Step 4: Configure NPM proxy (optional per stage). + if stage.EnableProxy { + if err := d.store.UpdateDeployStatus(deployID, "configuring_proxy", ""); err != nil { + slog.Warn("update deploy status", "error", err) + } + d.publishDeployStatus(deployID, project.ID, stage.ID, imageTag, "configuring_proxy", "") - npmProxyID, err = d.configureProxy(ctx, deployID, settings, containerName, project.Port, subdomain) - if err != nil { - return containerID, npmProxyID, instanceID, fmt.Errorf("configure proxy: %w", err) - } + npmProxyID, err = d.configureProxy(ctx, deployID, settings, containerName, project.Port, subdomain) + if err != nil { + return containerID, npmProxyID, instanceID, fmt.Errorf("configure proxy: %w", err) + } - // Update instance with NPM proxy ID. - inst.NpmProxyID = npmProxyID - inst.Subdomain = subdomain - if err := d.store.UpdateInstance(inst); err != nil { - slog.Warn("update instance with proxy ID", "error", err) + // Update instance with NPM proxy ID. + inst.NpmProxyID = npmProxyID + inst.Subdomain = subdomain + if err := d.store.UpdateInstance(inst); err != nil { + slog.Warn("update instance with proxy ID", "error", err) + } + } else { + d.logDeploy(deployID, "Proxy creation skipped (disabled for this stage)", "info") + inst.Subdomain = subdomain + if err := d.store.UpdateInstance(inst); err != nil { + slog.Warn("update instance", "error", err) + } } // Step 5: Health check. diff --git a/internal/store/models.go b/internal/store/models.go index 2c48e50..304ba41 100644 --- a/internal/store/models.go +++ b/internal/store/models.go @@ -23,6 +23,7 @@ type Stage struct { AutoDeploy bool `json:"auto_deploy"` MaxInstances int `json:"max_instances"` Confirm bool `json:"confirm"` + EnableProxy bool `json:"enable_proxy"` PromoteFrom string `json:"promote_from"` Subdomain string `json:"subdomain"` CreatedAt string `json:"created_at"` diff --git a/internal/store/stages.go b/internal/store/stages.go index a483d7e..824b1f5 100644 --- a/internal/store/stages.go +++ b/internal/store/stages.go @@ -8,6 +8,8 @@ import ( "github.com/google/uuid" ) +const stageColumns = `id, project_id, name, tag_pattern, auto_deploy, max_instances, confirm, enable_proxy, promote_from, subdomain, created_at, updated_at` + // CreateStage inserts a new stage for a project. func (s *Store) CreateStage(st Stage) (Stage, error) { st.ID = uuid.New().String() @@ -15,10 +17,9 @@ func (s *Store) CreateStage(st Stage) (Stage, error) { st.UpdatedAt = st.CreatedAt _, err := s.db.Exec( - `INSERT INTO stages (id, project_id, name, tag_pattern, auto_deploy, max_instances, confirm, promote_from, subdomain, created_at, updated_at) - VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, + `INSERT INTO stages (`+stageColumns+`) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, st.ID, st.ProjectID, st.Name, st.TagPattern, BoolToInt(st.AutoDeploy), st.MaxInstances, - BoolToInt(st.Confirm), st.PromoteFrom, st.Subdomain, st.CreatedAt, st.UpdatedAt, + BoolToInt(st.Confirm), BoolToInt(st.EnableProxy), st.PromoteFrom, st.Subdomain, st.CreatedAt, st.UpdatedAt, ) if err != nil { return Stage{}, fmt.Errorf("insert stage: %w", err) @@ -29,8 +30,7 @@ func (s *Store) CreateStage(st Stage) (Stage, error) { // GetStagesByProjectID returns all stages for a given project. func (s *Store) GetStagesByProjectID(projectID string) ([]Stage, error) { rows, err := s.db.Query( - `SELECT id, project_id, name, tag_pattern, auto_deploy, max_instances, confirm, promote_from, subdomain, created_at, updated_at - FROM stages WHERE project_id = ? ORDER BY name`, projectID, + `SELECT `+stageColumns+` FROM stages WHERE project_id = ? ORDER BY name`, projectID, ) if err != nil { return nil, fmt.Errorf("query stages: %w", err) @@ -51,12 +51,11 @@ func (s *Store) GetStagesByProjectID(projectID string) ([]Stage, error) { // GetStageByID returns a single stage by its ID. func (s *Store) GetStageByID(id string) (Stage, error) { var st Stage - var autoDeploy, confirm int + var autoDeploy, confirm, enableProxy int err := s.db.QueryRow( - `SELECT id, project_id, name, tag_pattern, auto_deploy, max_instances, confirm, promote_from, subdomain, created_at, updated_at - FROM stages WHERE id = ?`, id, + `SELECT `+stageColumns+` FROM stages WHERE id = ?`, id, ).Scan(&st.ID, &st.ProjectID, &st.Name, &st.TagPattern, &autoDeploy, &st.MaxInstances, - &confirm, &st.PromoteFrom, &st.Subdomain, &st.CreatedAt, &st.UpdatedAt) + &confirm, &enableProxy, &st.PromoteFrom, &st.Subdomain, &st.CreatedAt, &st.UpdatedAt) if errors.Is(err, sql.ErrNoRows) { return Stage{}, fmt.Errorf("stage %s: %w", id, ErrNotFound) } @@ -65,6 +64,7 @@ func (s *Store) GetStageByID(id string) (Stage, error) { } st.AutoDeploy = autoDeploy != 0 st.Confirm = confirm != 0 + st.EnableProxy = enableProxy != 0 return st, nil } @@ -72,10 +72,10 @@ func (s *Store) GetStageByID(id string) (Stage, error) { func (s *Store) UpdateStage(st Stage) error { st.UpdatedAt = Now() result, err := s.db.Exec( - `UPDATE stages SET name=?, tag_pattern=?, auto_deploy=?, max_instances=?, confirm=?, promote_from=?, subdomain=?, updated_at=? + `UPDATE stages SET name=?, tag_pattern=?, auto_deploy=?, max_instances=?, confirm=?, enable_proxy=?, promote_from=?, subdomain=?, updated_at=? WHERE id=?`, st.Name, st.TagPattern, BoolToInt(st.AutoDeploy), st.MaxInstances, - BoolToInt(st.Confirm), st.PromoteFrom, st.Subdomain, st.UpdatedAt, st.ID, + BoolToInt(st.Confirm), BoolToInt(st.EnableProxy), st.PromoteFrom, st.Subdomain, st.UpdatedAt, st.ID, ) if err != nil { return fmt.Errorf("update stage: %w", err) @@ -111,13 +111,14 @@ func BoolToInt(b bool) int { // scanStage scans a stage row from a *sql.Rows cursor. func scanStage(rows *sql.Rows) (Stage, error) { var st Stage - var autoDeploy, confirm int + var autoDeploy, confirm, enableProxy int err := rows.Scan(&st.ID, &st.ProjectID, &st.Name, &st.TagPattern, &autoDeploy, &st.MaxInstances, - &confirm, &st.PromoteFrom, &st.Subdomain, &st.CreatedAt, &st.UpdatedAt) + &confirm, &enableProxy, &st.PromoteFrom, &st.Subdomain, &st.CreatedAt, &st.UpdatedAt) if err != nil { return Stage{}, fmt.Errorf("scan stage: %w", err) } st.AutoDeploy = autoDeploy != 0 st.Confirm = confirm != 0 + st.EnableProxy = enableProxy != 0 return st, nil } diff --git a/internal/store/store.go b/internal/store/store.go index 872f366..38041c9 100644 --- a/internal/store/store.go +++ b/internal/store/store.go @@ -77,6 +77,8 @@ func (s *Store) runMigrations() error { `ALTER TABLE registries ADD COLUMN owner TEXT NOT NULL DEFAULT ''`, // Add base_volume_path to settings (2026-03-28). `ALTER TABLE settings ADD COLUMN base_volume_path TEXT NOT NULL DEFAULT ''`, + // Add enable_proxy to stages (2026-03-29). Default true for backwards compat. + `ALTER TABLE stages ADD COLUMN enable_proxy INTEGER NOT NULL DEFAULT 1`, } for _, m := range migrations { @@ -126,6 +128,7 @@ CREATE TABLE IF NOT EXISTS stages ( auto_deploy INTEGER NOT NULL DEFAULT 0, max_instances INTEGER NOT NULL DEFAULT 1, confirm INTEGER NOT NULL DEFAULT 0, + enable_proxy INTEGER NOT NULL DEFAULT 1, promote_from TEXT NOT NULL DEFAULT '', subdomain TEXT NOT NULL DEFAULT '', created_at TEXT NOT NULL DEFAULT (datetime('now')), diff --git a/web/src/lib/types.ts b/web/src/lib/types.ts index 9cc7f2a..245e788 100644 --- a/web/src/lib/types.ts +++ b/web/src/lib/types.ts @@ -21,6 +21,7 @@ export interface Stage { auto_deploy: boolean; max_instances: number; confirm: boolean; + enable_proxy: boolean; promote_from: string; subdomain: string; created_at: string; diff --git a/web/src/routes/projects/[id]/+page.svelte b/web/src/routes/projects/[id]/+page.svelte index 38a856e..a4dbdc7 100644 --- a/web/src/routes/projects/[id]/+page.svelte +++ b/web/src/routes/projects/[id]/+page.svelte @@ -32,6 +32,7 @@ let stageName = $state(''); let stageTagPattern = $state('*'); let stageAutoDeploy = $state(true); + let stageEnableProxy = $state(true); let stageMaxInstances = $state('1'); let addingStage = $state(false); @@ -43,10 +44,11 @@ name: stageName.trim(), tag_pattern: stageTagPattern.trim() || '*', auto_deploy: stageAutoDeploy, + enable_proxy: stageEnableProxy, max_instances: parseInt(stageMaxInstances) || 1, }); toasts.success(`Stage "${stageName}" created`); - stageName = ''; stageTagPattern = '*'; stageAutoDeploy = true; stageMaxInstances = '1'; + stageName = ''; stageTagPattern = '*'; stageAutoDeploy = true; stageEnableProxy = true; stageMaxInstances = '1'; showAddStage = false; await loadProject(); } catch (e) { @@ -338,7 +340,7 @@ {#if showAddStage}