package app import ( "context" "flag" "fmt" "io" "log/slog" "os" "gitea.maximumdirect.net/eric/narratio/internal/artifacts" "gitea.maximumdirect.net/eric/narratio/internal/config" "gitea.maximumdirect.net/eric/narratio/internal/logging" "gitea.maximumdirect.net/eric/narratio/internal/manifest" ) // Plan validates configuration, prepares the local workdir, and prints stage order. func Plan(ctx context.Context, args []string, out io.Writer) error { fs := flag.NewFlagSet("plan", flag.ContinueOnError) fs.SetOutput(io.Discard) var pipelinePath string var sessionPath string var sessionID string var force bool fs.StringVar(&pipelinePath, "config", "", "path to pipeline.yml (optional; defaults searched)") fs.StringVar(&sessionPath, "session", "", "path to session.yml") fs.StringVar(&sessionID, "session-id", "", "session identifier for session.yml templates") fs.BoolVar(&force, "force", false, "force stage execution (reserved for future behavior)") if err := fs.Parse(args); err != nil { return fmt.Errorf("plan: invalid flags: %w", err) } if fs.NArg() != 0 { return fmt.Errorf("plan: unexpected positional arguments") } resolvedPipelinePath, err := resolvePipelineConfigPath(pipelinePath) if err != nil { return fmt.Errorf("plan: %w", err) } resolvedSessionPath, err := resolveSessionConfigPath(sessionPath) if err != nil { return fmt.Errorf("plan: %w", err) } cfg, err := config.LoadWithSessionOptions(resolvedPipelinePath, resolvedSessionPath, config.SessionLoadOptions{ SessionID: sessionID, }) if err != nil { return fmt.Errorf("plan: %w", err) } if err := config.Validate(cfg); err != nil { return fmt.Errorf("plan: %w", err) } if _, err := loadSecretsFromConfig(cfg, logging.NewLogger(os.Stderr, slog.LevelInfo)); err != nil { return fmt.Errorf("plan: %w", err) } store := artifacts.NewLocalStore(cfg.Pipeline.Workspace.Root) paths, err := store.EnsureLayoutFor(cfg.Session.Campaign, cfg.Session.SessionID) if err != nil { return fmt.Errorf("plan: prepare workdir: %w", err) } stages := BuildFullPlan() var m *manifest.Manifest m, err = loadManifestIfPresent(ctx, cfg) if err != nil { return fmt.Errorf("plan: %w", err) } decisions := decideStageActions(stages, m, force) runCount := 0 skipCount := 0 if _, err := fmt.Fprintf(out, "narratio plan: workdir prepared at %s\n", paths.Root); err != nil { return err } for _, d := range decisions { if d.Action == stageActionRun { runCount++ } else { skipCount++ } if _, err := fmt.Fprintf(out, "%s: %s\n", d.Stage.Name(), d.Action); err != nil { return err } } if _, err := fmt.Fprintf(out, "totals: run=%d skip=%d\n", runCount, skipCount); err != nil { return err } return nil }