summaryrefslogtreecommitdiff
path: root/internal/executor/executor.go
diff options
context:
space:
mode:
authorPeter Stone <thepeterstone@gmail.com>2026-04-04 09:30:13 +0000
committerPeter Stone <thepeterstone@gmail.com>2026-04-04 09:30:13 +0000
commit940a5bab031bfe81cea9c90d64e6ebc804c366f9 (patch)
tree34f41c4675f0e238c5acd9665790d256ea39a041 /internal/executor/executor.go
parent2917c580ae3eab093e9e655ccdf210030b7b9d1f (diff)
feat: story ship gate — explicit POST /api/stories/{id}/ship; remove auto-deploy
- checkStoryCompletion now guards against re-running on already-SHIPPABLE stories and no longer auto-triggers triggerStoryDeploy on completion - New Pool.ShipStory method validates SHIPPABLE state then fires triggerStoryDeploy - POST /api/stories/{id}/ship route registered and handleShipStory handler added - Two new tests: 202 for SHIPPABLE story, 409 for non-SHIPPABLE story Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Diffstat (limited to 'internal/executor/executor.go')
-rw-r--r--internal/executor/executor.go22
1 files changed, 22 insertions, 0 deletions
diff --git a/internal/executor/executor.go b/internal/executor/executor.go
index a60a771..ad79a84 100644
--- a/internal/executor/executor.go
+++ b/internal/executor/executor.go
@@ -546,6 +546,14 @@ func (p *Pool) handleRunResult(ctx context.Context, t *task.Task, exec *storage.
// Subtasks are intentionally excluded — a parent task reaching READY/COMPLETED
// already accounts for its subtasks.
func (p *Pool) checkStoryCompletion(ctx context.Context, storyID string) {
+ story, err := p.store.GetStory(storyID)
+ if err != nil {
+ p.logger.Error("checkStoryCompletion: failed to get story", "storyID", storyID, "error", err)
+ return
+ }
+ if story.Status != task.StoryInProgress {
+ return // already SHIPPABLE or beyond — nothing to do
+ }
tasks, err := p.store.ListTasksByStory(storyID)
if err != nil {
p.logger.Error("checkStoryCompletion: failed to list tasks", "storyID", storyID, "error", err)
@@ -572,7 +580,21 @@ func (p *Pool) checkStoryCompletion(ctx context.Context, storyID string) {
return
}
p.logger.Info("story transitioned to SHIPPABLE", "storyID", storyID)
+ // Deploy is now triggered explicitly by the human via POST /api/stories/{id}/ship.
+}
+
+// ShipStory merges the story branch and runs the deploy script.
+// Returns an error if the story is not in SHIPPABLE state.
+func (p *Pool) ShipStory(ctx context.Context, storyID string) error {
+ story, err := p.store.GetStory(storyID)
+ if err != nil {
+ return fmt.Errorf("story not found: %w", err)
+ }
+ if story.Status != task.StoryShippable {
+ return fmt.Errorf("story is not SHIPPABLE (current status: %s)", story.Status)
+ }
go p.triggerStoryDeploy(ctx, storyID)
+ return nil
}
// spawnCheckerTask creates and submits a checker task for the given completed task.