From ad339148be084b425ded24bb21ad7435c59d3072 Mon Sep 17 00:00:00 2001 From: Claudomator Agent Date: Tue, 24 Mar 2026 22:19:13 +0000 Subject: feat: trigger deploy script on SHIPPABLE → DEPLOYED (ADR-007) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add triggerStoryDeploy to Pool: fetches story's project, runs its DeployScript via exec.CommandContext, and advances story to DEPLOYED on success. Wire into checkStoryCompletion with go p.triggerStoryDeploy after the SHIPPABLE transition. Covered by TestPool_StoryDeploy_RunsDeployScript. Co-Authored-By: Claude Sonnet 4.6 --- internal/executor/executor.go | 33 +++++++++++++++++++++++++ internal/executor/executor_test.go | 50 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 83 insertions(+) diff --git a/internal/executor/executor.go b/internal/executor/executor.go index 22273d9..2ab17a7 100644 --- a/internal/executor/executor.go +++ b/internal/executor/executor.go @@ -6,6 +6,7 @@ import ( "errors" "fmt" "log/slog" + "os/exec" "path/filepath" "strings" "sync" @@ -457,6 +458,38 @@ func (p *Pool) checkStoryCompletion(ctx context.Context, storyID string) { return } p.logger.Info("story transitioned to SHIPPABLE", "storyID", storyID) + go p.triggerStoryDeploy(ctx, storyID) +} + +// triggerStoryDeploy runs the project deploy script for a SHIPPABLE story +// and advances it to DEPLOYED on success. +func (p *Pool) triggerStoryDeploy(ctx context.Context, storyID string) { + story, err := p.store.GetStory(storyID) + if err != nil { + p.logger.Error("triggerStoryDeploy: failed to get story", "storyID", storyID, "error", err) + return + } + if story.ProjectID == "" { + return + } + proj, err := p.store.GetProject(story.ProjectID) + if err != nil { + p.logger.Error("triggerStoryDeploy: failed to get project", "storyID", storyID, "projectID", story.ProjectID, "error", err) + return + } + if proj.DeployScript == "" { + return + } + out, err := exec.CommandContext(ctx, proj.DeployScript).CombinedOutput() + if err != nil { + p.logger.Error("triggerStoryDeploy: deploy script failed", "storyID", storyID, "script", proj.DeployScript, "output", string(out), "error", err) + return + } + if err := p.store.UpdateStoryStatus(storyID, task.StoryDeployed); err != nil { + p.logger.Error("triggerStoryDeploy: failed to update story status", "storyID", storyID, "error", err) + return + } + p.logger.Info("story transitioned to DEPLOYED", "storyID", storyID) } // UndrainingAgent resets the drain state and failure counter for the given agent type. diff --git a/internal/executor/executor_test.go b/internal/executor/executor_test.go index 1e92093..9a36c50 100644 --- a/internal/executor/executor_test.go +++ b/internal/executor/executor_test.go @@ -1757,3 +1757,53 @@ func TestPool_Undrain_ResumesExecution(t *testing.T) { t.Fatal("timed out waiting for task after undrain") } } + +func TestPool_StoryDeploy_RunsDeployScript(t *testing.T) { + store := testStore(t) + runner := &mockRunner{} + runners := map[string]Runner{"claude": runner} + logger := slog.New(slog.NewTextHandler(os.Stderr, &slog.HandlerOptions{Level: slog.LevelError})) + pool := NewPool(2, runners, store, logger) + + // Create a deploy script that writes a marker file. + tmpDir := t.TempDir() + markerFile := filepath.Join(tmpDir, "deployed.marker") + scriptPath := filepath.Join(tmpDir, "deploy.sh") + scriptContent := "#!/bin/sh\ntouch " + markerFile + "\n" + if err := os.WriteFile(scriptPath, []byte(scriptContent), 0755); err != nil { + t.Fatalf("write deploy script: %v", err) + } + + proj := &task.Project{ + ID: "proj-deploy-1", + Name: "Deploy Test Project", + DeployScript: scriptPath, + } + if err := store.CreateProject(proj); err != nil { + t.Fatalf("create project: %v", err) + } + + story := &task.Story{ + ID: "story-deploy-1", + Name: "Deploy Test Story", + ProjectID: proj.ID, + Status: task.StoryShippable, + } + if err := store.CreateStory(story); err != nil { + t.Fatalf("create story: %v", err) + } + + pool.triggerStoryDeploy(context.Background(), story.ID) + + if _, err := os.Stat(markerFile); os.IsNotExist(err) { + t.Error("deploy script did not run: marker file not found") + } + + got, err := store.GetStory(story.ID) + if err != nil { + t.Fatalf("get story: %v", err) + } + if got.Status != task.StoryDeployed { + t.Errorf("story status: want DEPLOYED, got %q", got.Status) + } +} -- cgit v1.2.3