package app import ( "context" "flag" "fmt" "io" "gitea.maximumdirect.net/eric/narratio/internal/artifacts" "gitea.maximumdirect.net/eric/narratio/internal/config" "gitea.maximumdirect.net/eric/narratio/internal/manifest" ) // Resume continues execution from the first non-succeeded stage in the manifest. func Resume(ctx context.Context, args []string, out io.Writer) error { fs := flag.NewFlagSet("resume", flag.ContinueOnError) fs.SetOutput(io.Discard) var flags commonConfigFlags var force bool var selectedArtifacts artifactSelectionFlag addCommonConfigFlags(fs, &flags) fs.BoolVar(&force, "force", false, "force stage execution") fs.Var(&selectedArtifacts, "artifacts", "configured artifact names to execute and publish (comma-separated or repeatable)") if err := parseSessionAwareFlags("resume", fs, args, &flags.sessionID); err != nil { return err } if flags.sessionID == "" { return fmt.Errorf("resume: session_id is required") } cfg, err := loadCommandConfig(ctx, flags.pipelinePath, flags.campaignPath, flags.campaignFilePath, flags.sessionPath, flags.sessionOptions()) if err != nil { return fmt.Errorf("resume: %w", err) } if err := config.Validate(cfg); err != nil { return fmt.Errorf("resume: %w", err) } normalizedArtifacts, err := selectedArtifacts.Normalize() if err != nil { return fmt.Errorf("resume: invalid --artifacts: %w", err) } if err := validateSelectedArtifacts(cfg, normalizedArtifacts); err != nil { return fmt.Errorf("resume: %w", err) } full := BuildFullPlan() selected := full if !force { m, err := loadManifestIfPresent(ctx, cfg) if err != nil { return fmt.Errorf("resume: %w", err) } if m != nil { start := firstNonSucceededIndex(full, m) if start >= len(full) { _, err := fmt.Fprintf(out, "narratio resume: session %s has no remaining stages\n", cfg.Session.SessionID) return err } selected = full[start:] } } summary, err := executeStagesFn(ctx, cfg, selected, RunOptions{ Force: force, SelectedArtifacts: normalizedArtifacts, }) if err != nil { return fmt.Errorf("resume: %w", err) } _, err = fmt.Fprintf( out, "narratio resume: session %s; executed=%d skipped=%d; manifest=%s\n", summary.SessionID, len(summary.Executed), len(summary.Skipped), summary.ManifestPath, ) return err } func loadManifestIfPresent(ctx context.Context, cfg *config.Config) (*manifest.Manifest, error) { path := artifacts.SessionManifestPathForCampaign( cfg.Pipeline.Workspace.Root, cfg.Session.Campaign, cfg.Session.SessionID, ) exists, err := fileExists(path) if err != nil { return nil, fmt.Errorf("check manifest %q: %w", path, err) } if !exists { return nil, nil } store := &manifest.LocalStore{} m, err := store.Load(ctx, path) if err != nil { return nil, fmt.Errorf("load manifest %q: %w", path, err) } return m, nil }