186 lines
6.1 KiB
Go
186 lines
6.1 KiB
Go
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 := 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) ensureRunWorkspaceFor(selected stage.Stage) error {
|
|
if policy, ok := selected.(stage.RunWorkspacePolicy); ok && !policy.RequiresRunWorkspace() {
|
|
return nil
|
|
}
|
|
if err := fileops.EnsureWorkspaceDirectory(runtime.identity.LocalWorkDir); err != nil {
|
|
return fmt.Errorf("create run directory %q: %w", runtime.identity.LocalWorkDir, err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (runtime *invocationRuntime) persistTerminalFailure(err error) error {
|
|
return persistSessionTerminalFailure(
|
|
runtime.ctx,
|
|
runtime.manifestStore,
|
|
runtime.manifestPath,
|
|
runtime.manifest,
|
|
err,
|
|
runtime.dependencies.now(),
|
|
)
|
|
}
|