Files
poimen-workflows/action/git.go
T
Story Crater Bot d74644d197
ci / test (push) Failing after 35s
fix(action): configure git user in worktree before commit
Worktrees don't inherit git config from main repo, causing 'git commit'
to fail with exit status 128 when user.name/user.email are not set.
Configure with poimen agent identity before each commit.
2026-08-22 00:57:57 -07:00

224 lines
6.7 KiB
Go

package action
import (
"context"
"fmt"
"os"
"os/exec"
"path/filepath"
"github.com/rockliang/poimen/workflows/internal/lock"
)
// CloneRepoInput is input to CloneRepoActivity.
type CloneRepoInput struct {
RemoteURL string
TargetRepoPath string
}
// CloneRepoActivity clones a repo if it doesn't exist, or fetches if it does.
func CloneRepoActivity(ctx context.Context, in CloneRepoInput) error {
// Check if repo already exists
gitDir := filepath.Join(in.TargetRepoPath, ".git")
if _, err := os.Stat(gitDir); err == nil {
// Repo exists, fetch latest
cmd := exec.CommandContext(ctx, "git", "-C", in.TargetRepoPath, "fetch", "origin")
if err := cmd.Run(); err != nil {
return fmt.Errorf("git fetch failed: %w", err)
}
return nil
}
// Repo doesn't exist, clone it
cmd := exec.CommandContext(ctx, "git", "clone", in.RemoteURL, in.TargetRepoPath)
if err := cmd.Run(); err != nil {
return fmt.Errorf("git clone failed: %w", err)
}
return nil
}
// GitWorktreeAddInput is input to GitWorktreeAddActivity.
type GitWorktreeAddInput struct {
RepoPath string
TaskID string
}
// GitWorktreeAddActivity creates a new git worktree for a task.
func GitWorktreeAddActivity(ctx context.Context, in GitWorktreeAddInput) (string, error) {
// Acquire lock to synchronize worktree creation
lockPath := filepath.Join(in.RepoPath, "orchestrator.lock")
if err := lock.Acquire(lockPath); err != nil {
return "", fmt.Errorf("failed to acquire lock: %w", err)
}
defer lock.Release(lockPath)
// Create worktrees directory if it doesn't exist
worktreesDir := filepath.Join(in.RepoPath, "worktrees")
if err := os.MkdirAll(worktreesDir, 0755); err != nil {
return "", fmt.Errorf("failed to create worktrees dir: %w", err)
}
worktreePath := filepath.Join(worktreesDir, in.TaskID)
branch := "task/" + in.TaskID
// Create worktree
cmd := exec.CommandContext(ctx, "git", "-C", in.RepoPath, "worktree", "add", "-b", branch, worktreePath, "origin/main")
if err := cmd.Run(); err != nil {
return "", fmt.Errorf("git worktree add failed: %w", err)
}
return worktreePath, nil
}
// GitCommitInput is input to GitCommitActivity.
type GitCommitInput struct {
WorktreePath string
Message string
}
// GitCommitActivity commits changes in a worktree.
func GitCommitActivity(ctx context.Context, in GitCommitInput) error {
// Configure git user for commits if not already configured
// (worktrees don't inherit config from main repo)
exec.CommandContext(ctx, "git", "-C", in.WorktreePath, "config", "user.email", "[email protected]").Run()
exec.CommandContext(ctx, "git", "-C", in.WorktreePath, "config", "user.name", "Poimen Agent").Run()
// Stage all changes
cmd := exec.CommandContext(ctx, "git", "-C", in.WorktreePath, "add", "-A")
if err := cmd.Run(); err != nil {
return fmt.Errorf("git add failed: %w", err)
}
// Commit
cmd = exec.CommandContext(ctx, "git", "-C", in.WorktreePath, "commit", "-m", in.Message)
if err := cmd.Run(); err != nil {
return fmt.Errorf("git commit failed: %w", err)
}
return nil
}
// GitPushInput is input to GitPushActivity.
type GitPushInput struct {
RepoPath string
}
// GitPushActivity pushes changes to origin.
func GitPushActivity(ctx context.Context, in GitPushInput) error {
// Acquire lock to synchronize push
lockPath := filepath.Join(in.RepoPath, "orchestrator.lock")
if err := lock.Acquire(lockPath); err != nil {
return fmt.Errorf("failed to acquire lock: %w", err)
}
defer lock.Release(lockPath)
cmd := exec.CommandContext(ctx, "git", "-C", in.RepoPath, "push", "origin", "main")
if err := cmd.Run(); err != nil {
return fmt.Errorf("git push failed: %w", err)
}
return nil
}
// GitSquashMergeInput is input to GitSquashMergeActivity.
type GitSquashMergeInput struct {
RepoPath string
Branches []string
Message string
}
// GitSquashMergeActivity performs a squash merge of multiple branches into main.
func GitSquashMergeActivity(ctx context.Context, in GitSquashMergeInput) error {
// Acquire lock to synchronize merge
lockPath := filepath.Join(in.RepoPath, "orchestrator.lock")
if err := lock.Acquire(lockPath); err != nil {
return fmt.Errorf("failed to acquire lock: %w", err)
}
defer lock.Release(lockPath)
// Fetch origin main
cmd := exec.CommandContext(ctx, "git", "-C", in.RepoPath, "fetch", "origin", "main")
if err := cmd.Run(); err != nil {
return fmt.Errorf("git fetch failed: %w", err)
}
// Checkout main and pull with ff-only
cmd = exec.CommandContext(ctx, "git", "-C", in.RepoPath, "checkout", "main")
if err := cmd.Run(); err != nil {
return fmt.Errorf("git checkout main failed: %w", err)
}
cmd = exec.CommandContext(ctx, "git", "-C", in.RepoPath, "pull", "--ff-only", "origin", "main")
if err := cmd.Run(); err != nil {
return fmt.Errorf("git pull failed: %w", err)
}
// Squash merge each branch
for _, branch := range in.Branches {
cmd = exec.CommandContext(ctx, "git", "-C", in.RepoPath, "merge", "--squash", branch)
if err := cmd.Run(); err != nil {
return fmt.Errorf("git merge --squash %s failed: %w", branch, err)
}
}
// Commit squashed changes
cmd = exec.CommandContext(ctx, "git", "-C", in.RepoPath, "commit", "-m", in.Message)
if err := cmd.Run(); err != nil {
return fmt.Errorf("git commit failed: %w", err)
}
// Push to origin
cmd = exec.CommandContext(ctx, "git", "-C", in.RepoPath, "push", "origin", "main")
if err := cmd.Run(); err != nil {
return fmt.Errorf("git push failed: %w", err)
}
// Clean up worktrees and branches
for _, branch := range in.Branches {
taskID := branch[len("task/"):]
worktreePath := filepath.Join(in.RepoPath, "worktrees", taskID)
// Remove worktree
cmd = exec.CommandContext(ctx, "git", "-C", in.RepoPath, "worktree", "remove", worktreePath, "--force")
if err := cmd.Run(); err != nil {
// Log error but continue cleanup
fmt.Printf("warning: failed to remove worktree %s: %v\n", worktreePath, err)
}
// Delete branch
cmd = exec.CommandContext(ctx, "git", "-C", in.RepoPath, "branch", "-D", branch)
if err := cmd.Run(); err != nil {
// Log error but continue cleanup
fmt.Printf("warning: failed to delete branch %s: %v\n", branch, err)
}
}
return nil
}
// GitDiffInput is input to GitDiffActivity.
type GitDiffInput struct {
WorktreePath string
}
// GitDiffOutput is output of GitDiffActivity.
type GitDiffOutput struct {
Diff string
}
// GitDiffActivity gets git diff for a worktree.
func GitDiffActivity(ctx context.Context, in GitDiffInput) (GitDiffOutput, error) {
out := GitDiffOutput{Diff: ""}
// Get diff from worktree against main branch
cmd := exec.CommandContext(ctx, "git", "-C", in.WorktreePath, "diff", "main")
output, err := cmd.CombinedOutput()
if err != nil {
// Diff can fail if branch doesn't exist, treat as no changes
return out, nil
}
out.Diff = string(output)
return out, nil
}