forked from Gitlink/gitlink-cli
176 lines
4.9 KiB
Go
176 lines
4.9 KiB
Go
package engine
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"os"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/gitlink-org/gitlink-cli/shortcuts/common"
|
|
wf "github.com/gitlink-org/gitlink-cli/shortcuts/workflow"
|
|
"github.com/gitlink-org/gitlink-cli/shortcuts/workflow/state"
|
|
)
|
|
|
|
// Run executes every step in a workflow sequentially.
|
|
func Run(ctx *common.RuntimeContext, wfDef *wf.WorkflowDef, dryRun bool) (*wf.WorkflowResult, error) {
|
|
return RunWithMode(ctx, wfDef, dryRun, "")
|
|
}
|
|
|
|
// RunWithMode executes a workflow with explicit AI mode control.
|
|
func RunWithMode(ctx *common.RuntimeContext, wfDef *wf.WorkflowDef, dryRun bool, aiMode string) (*wf.WorkflowResult, error) {
|
|
if aiMode != "" {
|
|
ctx.AIMode = aiMode
|
|
}
|
|
return run(ctx, wfDef, dryRun)
|
|
}
|
|
|
|
func run(ctx *common.RuntimeContext, wfDef *wf.WorkflowDef, dryRun bool) (*wf.WorkflowResult, error) {
|
|
if !shouldSkipOwnerRepoResolve(ctx, wfDef) {
|
|
if err := ctx.ResolveOwnerRepo(); err != nil {
|
|
return nil, fmt.Errorf("resolve repo: %w", err)
|
|
}
|
|
}
|
|
|
|
if ctx.Args == nil {
|
|
ctx.Args = make(map[string]string)
|
|
}
|
|
if _, ok := ctx.Args["dry_run"]; !ok && dryRun {
|
|
ctx.Args["dry_run"] = "true"
|
|
}
|
|
ctx.Args["__wf_name"] = wfDef.Name
|
|
|
|
if ctx.Owner != "" {
|
|
ctx.Args["_owner"] = ctx.Owner
|
|
}
|
|
if ctx.Repo != "" {
|
|
ctx.Args["_repo"] = ctx.Repo
|
|
}
|
|
|
|
var workflowState *state.WorkflowState
|
|
if !dryRun {
|
|
workflowState, _ = state.LoadState(wfDef.Name)
|
|
}
|
|
|
|
results := make([]wf.StepResult, 0, len(wfDef.Steps))
|
|
for _, step := range wfDef.Steps {
|
|
var upstreamHash string
|
|
if !dryRun && workflowState != nil {
|
|
upstream := collectUpstream(ctx, step)
|
|
upstreamHash = state.HashData(upstream)
|
|
}
|
|
|
|
isDaemon := ctx.Arg("daemon-loop") == "true"
|
|
if isDaemon && !dryRun && workflowState != nil && step.RunWhen != "" && step.RunWhen != wf.RunAlways {
|
|
shouldRun, skipReason := checkPhaseCondition(step, workflowState, upstreamHash)
|
|
if !shouldRun {
|
|
fmt.Fprintf(os.Stderr, "[%s] 跳过 %s: %s\n", wfDef.Name, step.Name, skipReason)
|
|
results = append(results, wf.StepResult{
|
|
Step: step.Name,
|
|
Purpose: step.Purpose,
|
|
Type: step.Type,
|
|
OK: true,
|
|
Skipped: true,
|
|
SkipReason: skipReason,
|
|
})
|
|
continue
|
|
}
|
|
}
|
|
|
|
sr := ExecuteStep(ctx, step, dryRun)
|
|
results = append(results, *sr)
|
|
|
|
if sr.OK && !sr.Skipped && step.Name == "init-scaffold" && ctx.Repo == "" {
|
|
if m, ok := sr.Data.(map[string]interface{}); ok {
|
|
if a, ok := m["analysis"].(map[string]interface{}); ok {
|
|
if r, ok := a["repo"].(string); ok && r != "" {
|
|
parts := strings.SplitN(r, "/", 2)
|
|
if len(parts) == 2 {
|
|
ctx.Repo = parts[1]
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if !dryRun && workflowState != nil && sr.OK && !sr.Skipped {
|
|
if workflowState.PhaseLastRun == nil {
|
|
workflowState.PhaseLastRun = make(map[string]string)
|
|
}
|
|
workflowState.PhaseLastRun[step.Name] = time.Now().Format(time.RFC3339)
|
|
if workflowState.PhaseUpstream == nil {
|
|
workflowState.PhaseUpstream = make(map[string]string)
|
|
}
|
|
workflowState.PhaseUpstream[step.Name] = upstreamHash
|
|
}
|
|
|
|
if sr.Data != nil {
|
|
raw, err := json.Marshal(sr.Data)
|
|
if err == nil {
|
|
ctx.Args[step.Name] = string(raw)
|
|
} else {
|
|
ctx.Args[step.Name] = fmt.Sprint(sr.Data)
|
|
}
|
|
} else if !sr.OK && sr.Error != "" {
|
|
ctx.Args[step.Name] = fmt.Sprintf(`{"_error": true, "_message": %q}`, sr.Error)
|
|
}
|
|
}
|
|
|
|
if !dryRun && workflowState != nil {
|
|
workflowState.Save()
|
|
}
|
|
|
|
return &wf.WorkflowResult{
|
|
Workflow: wfDef.Name,
|
|
Owner: ctx.Owner,
|
|
Repo: ctx.Repo,
|
|
Steps: results,
|
|
}, nil
|
|
}
|
|
|
|
func shouldSkipOwnerRepoResolve(ctx *common.RuntimeContext, wfDef *wf.WorkflowDef) bool {
|
|
if wfDef != nil && wfDef.Name == "project-init" {
|
|
return true
|
|
}
|
|
return isExplicitMultiRepoRun(ctx, wfDef)
|
|
}
|
|
|
|
// IsExplicitMultiRepoRun reports whether this is a multi-repo run with explicit
|
|
// --repos or --from flags (as opposed to resolving from the current directory).
|
|
func IsExplicitMultiRepoRun(ctx *common.RuntimeContext, wfDef *wf.WorkflowDef) bool {
|
|
return isExplicitMultiRepoRun(ctx, wfDef)
|
|
}
|
|
|
|
func isExplicitMultiRepoRun(ctx *common.RuntimeContext, wfDef *wf.WorkflowDef) bool {
|
|
if wfDef == nil || wfDef.Name != "multi-repo" {
|
|
return false
|
|
}
|
|
return ctx.Arg("repos") != "" || ctx.Arg("from") != ""
|
|
}
|
|
|
|
func checkPhaseCondition(step wf.StepDef, workflowState *state.WorkflowState, upstreamHash string) (bool, string) {
|
|
switch step.RunWhen {
|
|
case wf.RunWeekly:
|
|
last := workflowState.PhaseLastRun[step.Name]
|
|
if last == "" {
|
|
return true, ""
|
|
}
|
|
t, err := time.Parse(time.RFC3339, last)
|
|
if err != nil {
|
|
return true, ""
|
|
}
|
|
if time.Since(t) >= 7*24*time.Hour {
|
|
return true, ""
|
|
}
|
|
next := t.Add(7 * 24 * time.Hour)
|
|
return false, fmt.Sprintf("下次运行: %s", next.Format("01-02 15:04"))
|
|
case wf.RunOnChange:
|
|
if prev, ok := workflowState.PhaseUpstream[step.Name]; ok && prev == upstreamHash {
|
|
return false, "数据无变化"
|
|
}
|
|
return true, ""
|
|
default:
|
|
return true, ""
|
|
}
|
|
}
|