summaryrefslogtreecommitdiff
path: root/internal/executor
diff options
context:
space:
mode:
authorPeter Stone <thepeterstone@gmail.com>2026-03-24 23:01:22 +0000
committerPeter Stone <thepeterstone@gmail.com>2026-03-24 23:01:22 +0000
commit4a47ec318c92cc899ee7392bb200cf9ee482e0da (patch)
treee2c2d7f37c6a971f344f11344e9df53e271df290 /internal/executor
parentf6041b8d1c1bf7e776973e2cc6ddac8ecaab3cfa (diff)
feat: merge story branch to master before deploy, add doot project to registry
- triggerStoryDeploy: fetch/checkout/merge --no-ff/push before running deploy script (ADR-007) - executor_test: TestPool_StoryDeploy_MergesStoryBranch proves merge happens - seed.go: add doot project with deploy script; wire claudomator deploy script Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Diffstat (limited to 'internal/executor')
-rw-r--r--internal/executor/executor.go16
-rw-r--r--internal/executor/executor_test.go73
2 files changed, 89 insertions, 0 deletions
diff --git a/internal/executor/executor.go b/internal/executor/executor.go
index 4183ab0..7213b34 100644
--- a/internal/executor/executor.go
+++ b/internal/executor/executor.go
@@ -512,6 +512,22 @@ func (p *Pool) triggerStoryDeploy(ctx context.Context, storyID string) {
if proj.DeployScript == "" {
return
}
+ // Merge story branch to master before deploying (ADR-007).
+ if story.BranchName != "" && proj.LocalPath != "" {
+ mergeSteps := [][]string{
+ {"git", "-C", proj.LocalPath, "fetch", "origin"},
+ {"git", "-C", proj.LocalPath, "checkout", "master"},
+ {"git", "-C", proj.LocalPath, "merge", "--no-ff", story.BranchName, "-m", "Merge " + story.BranchName},
+ {"git", "-C", proj.LocalPath, "push", "origin", "master"},
+ }
+ for _, args := range mergeSteps {
+ if mergeOut, mergeErr := exec.CommandContext(ctx, args[0], args[1:]...).CombinedOutput(); mergeErr != nil {
+ p.logger.Error("triggerStoryDeploy: merge failed", "cmd", args, "output", string(mergeOut), "error", mergeErr)
+ return
+ }
+ }
+ p.logger.Info("story branch merged to master", "storyID", storyID, "branch", story.BranchName)
+ }
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)
diff --git a/internal/executor/executor_test.go b/internal/executor/executor_test.go
index 44fa7b5..c68d37b 100644
--- a/internal/executor/executor_test.go
+++ b/internal/executor/executor_test.go
@@ -5,6 +5,7 @@ import (
"fmt"
"log/slog"
"os"
+ "os/exec"
"path/filepath"
"strings"
"sync"
@@ -1809,6 +1810,78 @@ func TestPool_StoryDeploy_RunsDeployScript(t *testing.T) {
}
}
+func runGit(t *testing.T, dir string, args ...string) {
+ t.Helper()
+ cmd := exec.Command("git", args...)
+ if dir != "" {
+ cmd.Dir = dir
+ }
+ if out, err := cmd.CombinedOutput(); err != nil {
+ t.Fatalf("git %v: %v\n%s", args, err, out)
+ }
+}
+
+func TestPool_StoryDeploy_MergesStoryBranch(t *testing.T) {
+ tmpDir := t.TempDir()
+
+ // Set up bare repo + working copy with a story branch.
+ bareDir := filepath.Join(tmpDir, "bare.git")
+ localDir := filepath.Join(tmpDir, "local")
+ runGit(t, "", "init", "--bare", bareDir)
+ runGit(t, "", "clone", bareDir, localDir)
+ runGit(t, localDir, "config", "user.email", "test@test.com")
+ runGit(t, localDir, "config", "user.name", "Test")
+
+ // Initial commit on master.
+ runGit(t, localDir, "checkout", "-b", "master")
+ os.WriteFile(filepath.Join(localDir, "README.md"), []byte("initial"), 0644)
+ runGit(t, localDir, "add", ".")
+ runGit(t, localDir, "commit", "-m", "initial")
+ runGit(t, localDir, "push", "-u", "origin", "master")
+
+ // Story branch with a feature commit.
+ runGit(t, localDir, "checkout", "-b", "story/test-feature")
+ os.WriteFile(filepath.Join(localDir, "feature.go"), []byte("package main"), 0644)
+ runGit(t, localDir, "add", ".")
+ runGit(t, localDir, "commit", "-m", "feature work")
+ runGit(t, localDir, "push", "origin", "story/test-feature")
+ runGit(t, localDir, "checkout", "master")
+
+ store := testStore(t)
+ logger := slog.New(slog.NewTextHandler(os.Stderr, &slog.HandlerOptions{Level: slog.LevelError}))
+ pool := NewPool(2, map[string]Runner{"claude": &mockRunner{}}, store, logger)
+
+ scriptPath := filepath.Join(tmpDir, "deploy.sh")
+ os.WriteFile(scriptPath, []byte("#!/bin/sh\nexit 0\n"), 0755)
+
+ proj := &task.Project{
+ ID: "proj-merge-1", Name: "Merge Test",
+ LocalPath: localDir, DeployScript: scriptPath,
+ }
+ if err := store.CreateProject(proj); err != nil {
+ t.Fatalf("create project: %v", err)
+ }
+ story := &task.Story{
+ ID: "story-merge-1", Name: "Merge Test Story",
+ ProjectID: proj.ID, BranchName: "story/test-feature",
+ Status: task.StoryShippable,
+ }
+ if err := store.CreateStory(story); err != nil {
+ t.Fatalf("create story: %v", err)
+ }
+
+ pool.triggerStoryDeploy(context.Background(), story.ID)
+
+ // feature.go should now be on master in the working copy.
+ if _, err := os.Stat(filepath.Join(localDir, "feature.go")); os.IsNotExist(err) {
+ t.Error("story branch was not merged to master: feature.go missing")
+ }
+ got, _ := store.GetStory(story.ID)
+ if got.Status != task.StoryDeployed {
+ t.Errorf("story status: want DEPLOYED, got %q", got.Status)
+ }
+}
+
func TestPool_PostDeploy_CreatesValidationTask(t *testing.T) {
store := testStore(t)
logger := slog.New(slog.NewTextHandler(os.Stderr, &slog.HandlerOptions{Level: slog.LevelError}))