package app import ( "context" "fmt" "log/slog" "os" "gitea.maximumdirect.net/eric/narratio/internal/artifacts" "gitea.maximumdirect.net/eric/narratio/internal/config" "gitea.maximumdirect.net/eric/narratio/internal/fileops" "gitea.maximumdirect.net/eric/narratio/internal/logging" "gitea.maximumdirect.net/eric/narratio/internal/manifest" "gitea.maximumdirect.net/eric/narratio/internal/stage" ) type invocationRuntime struct { ctx context.Context cfg *config.Config plan BoundedPlan stages []stage.Stage opts RunOptions effectiveArtifacts artifacts.EffectiveArtifactSet identity invocationIdentity paths artifacts.SessionPaths env *stage.Env manifestStore manifest.Store artifactStore artifacts.Store manifestPath string manifest *manifest.Manifest stageNames []string executed []string skipped []string dependencies invocationDependencies } func prepareInvocation(ctx context.Context, cfg *config.Config, plan BoundedPlan, opts RunOptions, dependencies invocationDependencies) (*invocationRuntime, error) { var prerequisiteStore manifest.Store if opts.Env != nil { prerequisiteStore = opts.Env.ManifestStore } if err := inspectBoundedPrerequisites(ctx, cfg, plan, prerequisiteStore); err != nil { return nil, fmt.Errorf("validate bounded run prerequisites: %w", err) } effectiveArtifacts := opts.EffectiveArtifacts if !effectiveArtifacts.Resolved() && cfg != nil && cfg.Pipeline != nil && cfg.Pipeline.Scriptorium != nil { var err error effectiveArtifacts, err = resolveEffectiveArtifacts(cfg, nil) if err != nil { return nil, fmt.Errorf("resolve effective artifacts: %w", err) } } if !effectiveArtifacts.Resolved() { var err error effectiveArtifacts, err = artifacts.ResolveEffectiveArtifactSet(nil, nil) if err != nil { return nil, fmt.Errorf("resolve default effective artifacts: %w", err) } } runID, err := dependencies.newRunID() if err != nil { return nil, fmt.Errorf("generate run id: %w", err) } identity, err := resolveInvocationIdentity(cfg, runID) if err != nil { return nil, fmt.Errorf("resolve invocation identity: %w", err) } env := opts.Env if env == nil { env = &stage.Env{} } // The resolved configuration is the invocation's single source of truth. // Injected environments supply collaborators, never alternate invocation data. env.Config = cfg env.SelectedArtifactKeys = append([]string(nil), opts.SelectedArtifacts...) env.EffectiveArtifacts = effectiveArtifacts if env.ManifestStore == nil { env.ManifestStore = &manifest.LocalStore{} } if env.ArtifactStore == nil { env.ArtifactStore = artifacts.NewLocalStore(cfg.Pipeline.Workspace.Root) } if env.Logger == nil { env.Logger = logging.NewLogger(os.Stderr, slog.LevelInfo) } normalizedOpts := opts normalizedOpts.SelectedArtifacts = append([]string(nil), opts.SelectedArtifacts...) normalizedOpts.EffectiveArtifacts = effectiveArtifacts normalizedOpts.Env = env stages := plan.Stages() return &invocationRuntime{ ctx: ctx, cfg: cfg, plan: plan, stages: stages, opts: normalizedOpts, effectiveArtifacts: effectiveArtifacts, identity: identity, env: env, manifestStore: env.ManifestStore, artifactStore: env.ArtifactStore, manifestPath: artifacts.SessionManifestPathForCampaign( cfg.Pipeline.Workspace.Root, identity.Campaign, identity.SessionID, ), stageNames: make([]string, 0, len(stages)), executed: make([]string, 0, len(stages)), skipped: make([]string, 0, len(stages)), dependencies: dependencies, }, nil } func prepareInvocationLayout(runtime *invocationRuntime) error { paths, err := runtime.artifactStore.EnsureLayoutFor(runtime.identity.Campaign, runtime.identity.SessionID) if err != nil { return fmt.Errorf("prepare workdir: %w", err) } if paths.ManifestPath != runtime.manifestPath { return fmt.Errorf("prepared manifest path %q does not match resolved manifest path %q", paths.ManifestPath, runtime.manifestPath) } runtime.paths = paths return nil } func initializeLockedInvocation(runtime *invocationRuntime) error { if err := requireCompleteRestore(runtime.paths); err != nil { return err } m, present, err := loadManifestAtPathIfPresent(runtime.ctx, runtime.manifestStore, runtime.manifestPath) if err != nil { return err } if present { if err := runtime.identity.validateSessionManifest(m); err != nil { return err } } else { m, err = runtime.manifestStore.Create(runtime.ctx, runtime.identity.SessionID) if err != nil { return fmt.Errorf("create manifest: %w", err) } } runtime.manifest = m // The unlocked inspection avoids creating invocation state for an invalid // request. Recheck while holding the session lock in case prior state changed. if err := validateBoundedPrerequisites(runtime.plan, m); err != nil { return fmt.Errorf("validate bounded run prerequisites under session lock: %w", err) } runtime.identity.applyToSessionManifest(m) applyEffectiveConfigProvenance(m, runtime.cfg) if err := runtime.manifestStore.Save(runtime.ctx, runtime.manifestPath, m); err != nil { return fmt.Errorf("save manifest identity %q: %w", runtime.manifestPath, err) } if err := fileops.EnsureWorkspaceDirectory(runtime.identity.LocalWorkDir); err != nil { return runtime.persistTerminalFailure(fmt.Errorf("create run directory %q: %w", runtime.identity.LocalWorkDir, err)) } if _, err := loadSecretsFromConfig(runtime.env.Config, runtime.env.Logger); err != nil { return runtime.persistTerminalFailure(fmt.Errorf("load secrets from files: %w", err)) } return nil } func (runtime *invocationRuntime) persistTerminalFailure(err error) error { return persistSessionTerminalFailure( runtime.ctx, runtime.manifestStore, runtime.manifestPath, runtime.manifest, err, runtime.dependencies.now(), ) }