Files
kandev/apps/backend/internal/agent/runtime/lifecycle/preparer_script.go
T

145 lines
4.5 KiB
Go

package lifecycle
import (
"context"
"fmt"
"path/filepath"
"strings"
"go.uber.org/zap"
"github.com/kandev/kandev/internal/agent/executor"
"github.com/kandev/kandev/internal/common/logger"
"github.com/kandev/kandev/internal/common/subproc"
"github.com/kandev/kandev/internal/scriptengine"
)
// prepareScriptLogger is a package-level logger for setup script resolution.
var prepareScriptLogger = logger.Default().WithFields(zap.String("component", "prepare-script"))
func resolvePreparerSetupScript(req *EnvPrepareRequest, workspacePath string) (string, error) {
script := strings.TrimSpace(req.SetupScript)
if script == "" {
script = defaultPreparerSetupScript(req)
}
if req.ContributionDestination != nil {
destinationScript, err := scriptengine.ContributionDestinationSetupScriptAt(req.ContributionDestination, workspacePath)
if err != nil {
return "", fmt.Errorf("resolve contribution destination setup: %w", err)
}
script += destinationScript
}
if script == "" {
return "", nil
}
metadata := map[string]any{
MetadataKeyRepositoryPath: req.RepositoryPath,
MetadataKeyBaseBranch: req.BaseBranch,
MetadataKeyRepoSetupScript: req.RepoSetupScript,
}
if req.WorktreeID != "" {
metadata[MetadataKeyWorktreeID] = req.WorktreeID
}
if req.WorktreeBranch != "" {
metadata[MetadataKeyWorktreeBranch] = req.WorktreeBranch
}
worktreeBasePath := ""
if workspacePath != "" {
worktreeBasePath = filepath.Dir(workspacePath)
}
resolver := scriptengine.NewResolver().
WithProvider(scriptengine.WorkspaceProvider(workspacePath)).
WithProvider(scriptengine.RepositoryProvider(metadata, req.Env, getGitRemoteURL, nil)).
WithProvider(scriptengine.WorktreeProvider(
worktreeBasePath,
workspacePath,
req.WorktreeID,
req.WorktreeBranch,
req.BaseBranch,
))
resolved := resolver.Resolve(script)
if isScriptEffectivelyEmpty(resolved) {
prepareScriptLogger.Warn("setup script is comment-only after resolution, skipping",
zap.String("task_id", req.TaskID),
zap.String("executor_type", string(req.ExecutorType)),
zap.Bool("use_worktree", req.UseWorktree),
zap.Bool("has_explicit_script", strings.TrimSpace(req.SetupScript) != ""))
return "", nil
}
return resolved, nil
}
// isScriptEffectivelyEmpty returns true when a script contains only a shebang,
// comments, and/or blank lines — i.e. no executable commands. This avoids
// running (and displaying) the default template scripts that are pure comments.
func isScriptEffectivelyEmpty(script string) bool {
for _, line := range strings.Split(script, "\n") {
trimmed := strings.TrimSpace(line)
if trimmed == "" || strings.HasPrefix(trimmed, "#") {
continue
}
return false
}
return true
}
func defaultPreparerSetupScript(req *EnvPrepareRequest) string {
if req.UseWorktree {
return DefaultPrepareScript("worktree")
}
execType := req.ExecutorType
switch execType {
case executor.NameStandalone, executor.NameLocal:
return DefaultPrepareScript("local")
default:
return ""
}
}
func getGitRemoteURL(repoPath string) (string, error) {
cmd := subproc.NewGitCommand(context.Background(), "-C", repoPath, "remote", "get-url", "origin")
out, err := subproc.RunGitOutputClass(context.Background(), subproc.GitLifecycle, cmd)
if err != nil {
return "", err
}
return strings.TrimSpace(string(out)), nil
}
// runSetupScriptStep executes the resolved setup script as a named prepare step,
// appends the completed step to steps, and returns the updated slice.
// Setup script failures are non-fatal: the error is recorded in the step and
// logged as a warning, but preparation continues.
func runSetupScriptStep(
ctx context.Context,
req *EnvPrepareRequest,
workspacePath, resolvedScript string,
stepIdx, totalSteps int,
onProgress PrepareProgressCallback,
steps []PrepareStep,
log *logger.Logger,
) []PrepareStep {
step := beginStep("Run setup script")
step.Command = setupScriptDisplayCommand(req)
reportProgress(onProgress, step, stepIdx, totalSteps)
output, err := runSetupScript(ctx, resolvedScript, workspacePath, req.Env, func(current string) {
step.Output = current
reportProgress(onProgress, step, stepIdx, totalSteps)
})
step.Output = output
if err != nil {
completeStepError(&step, err.Error())
steps = append(steps, step)
reportProgress(onProgress, step, stepIdx, totalSteps)
log.Warn("setup script failed", zap.String("task_id", req.TaskID), zap.Error(err))
} else {
completeStepSuccess(&step)
steps = append(steps, step)
reportProgress(onProgress, step, stepIdx, totalSteps)
}
return steps
}