Establish explicit runner lifecycle state

This commit is contained in:
2026-09-04 01:05:44 +00:00
parent ca1b694aa9
commit e0f19833a1
4 changed files with 240 additions and 118 deletions

View File

@@ -4,7 +4,6 @@ import (
"context"
"errors"
"fmt"
"log/slog"
"os"
"strings"
@@ -17,8 +16,6 @@ import (
"gitea.maximumdirect.net/eric/narratio/internal/artifactmodel"
"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"
)
@@ -42,74 +39,20 @@ type RunSummary struct {
var executeStagesFn = executePlan
func executePlan(ctx context.Context, cfg *config.Config, plan BoundedPlan, opts RunOptions) (summary *RunSummary, resultErr error) {
stages := plan.Stages()
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 := artifacts.NewRunID()
runtime, err := prepareInvocation(ctx, cfg, plan, opts)
if err != nil {
return nil, fmt.Errorf("generate run id: %w", err)
return nil, err
}
identity, err := resolveInvocationIdentity(cfg, runID)
if err != nil {
return nil, fmt.Errorf("resolve invocation identity: %w", err)
if err := prepareInvocationLayout(runtime); err != nil {
return nil, err
}
env := opts.Env
if env == nil {
env = &Env{}
}
// The resolved configuration is the invocation's single source of truth.
// Injected environments supply collaborators, never an alternate config.
env.Config = cfg
env.SelectedArtifactKeys = append([]string(nil), opts.SelectedArtifacts...)
env.EffectiveArtifacts = effectiveArtifacts
if env.ManifestStore == nil {
env.ManifestStore = &manifest.LocalStore{}
}
manifestPath := artifacts.SessionManifestPathForCampaign(
cfg.Pipeline.Workspace.Root,
identity.Campaign,
identity.SessionID,
)
if env.ArtifactStore == nil {
env.ArtifactStore = artifacts.NewLocalStore(cfg.Pipeline.Workspace.Root)
}
if env.Logger == nil {
env.Logger = logging.NewLogger(os.Stderr, slog.LevelInfo)
}
artifactStore := env.ArtifactStore
paths, err := artifactStore.EnsureLayoutFor(identity.Campaign, identity.SessionID)
if err != nil {
return nil, fmt.Errorf("prepare workdir: %w", err)
}
lock, err := artifactStore.AcquireSessionLockForContext(ctx, identity.Campaign, identity.SessionID)
lock, err := runtime.artifactStore.AcquireSessionLockForContext(ctx, runtime.identity.Campaign, runtime.identity.SessionID)
if err != nil {
return nil, fmt.Errorf("acquire session lock: %w", err)
}
defer func() {
if releaseErr := artifactStore.ReleaseSessionLock(lock); releaseErr != nil {
if releaseErr := runtime.artifactStore.ReleaseSessionLock(lock); releaseErr != nil {
if resultErr == nil {
resultErr = fmt.Errorf("release session lock: %w", releaseErr)
} else {
@@ -118,50 +61,17 @@ func executePlan(ctx context.Context, cfg *config.Config, plan BoundedPlan, opts
}
}()
if paths.ManifestPath != manifestPath {
return nil, fmt.Errorf("prepared manifest path %q does not match resolved manifest path %q", paths.ManifestPath, manifestPath)
}
if err := requireCompleteRestore(paths); err != nil {
if err := initializeLockedInvocation(runtime); err != nil {
return nil, err
}
m, present, err := loadManifestAtPathIfPresent(ctx, env.ManifestStore, manifestPath)
if err != nil {
return nil, err
}
if present {
if err := identity.validateSessionManifest(m); err != nil {
return nil, err
}
} else {
m, err = env.ManifestStore.Create(ctx, identity.SessionID)
if err != nil {
return nil, fmt.Errorf("create manifest: %w", err)
}
}
// The preflight prerequisite inspection avoids creating run state for an
// already-invalid request. Recheck the manifest protected by the session
// lock because another invocation may have changed prerequisite state while
// this invocation waited to acquire the lock.
if err := validateBoundedPrerequisites(plan, m); err != nil {
return nil, fmt.Errorf("validate bounded run prerequisites under session lock: %w", err)
}
identity.applyToSessionManifest(m)
applyEffectiveConfigProvenance(m, cfg)
if err := env.ManifestStore.Save(ctx, manifestPath, m); err != nil {
return nil, fmt.Errorf("save manifest identity %q: %w", manifestPath, err)
}
if err := fileops.EnsureWorkspaceDirectory(identity.LocalWorkDir); err != nil {
return nil, persistSessionTerminalFailure(
ctx, env.ManifestStore, manifestPath, m,
fmt.Errorf("create run directory %q: %w", identity.LocalWorkDir, err),
)
}
if _, err := loadSecretsFromConfig(env.Config, env.Logger); err != nil {
return nil, persistSessionTerminalFailure(
ctx, env.ManifestStore, manifestPath, m,
fmt.Errorf("load secrets from files: %w", err),
)
}
stages := runtime.stages
effectiveArtifacts := runtime.effectiveArtifacts
identity := runtime.identity
env := runtime.env
manifestPath := runtime.manifestPath
m := runtime.manifest
runID := identity.RunID
if env.WhisperX == nil && stagesContainAny(stages, "transcribe") {
client, err := buildDefaultWhisperXClient(env.Config)
if err != nil {
@@ -231,12 +141,9 @@ func executePlan(ctx context.Context, cfg *config.Config, plan BoundedPlan, opts
stageEnv := env
runNames := make([]string, 0, len(stages))
executed := make([]string, 0, len(stages))
skipped := make([]string, 0, len(stages))
for _, s := range stages {
stageEnv.Force = opts.Force
runNames = append(runNames, s.Name())
runtime.stageNames = append(runtime.stageNames, s.Name())
semanticConfig, err := currentStageSemanticConfig(s, stageEnv)
if err != nil {
return nil, persistSessionTerminalFailure(
@@ -274,11 +181,11 @@ func executePlan(ctx context.Context, cfg *config.Config, plan BoundedPlan, opts
}
if decision.action == stageActionSkip {
skipped = append(skipped, s.Name())
runtime.skipped = append(runtime.skipped, s.Name())
env.Logger.Info("skipping stage", "stage", s.Name(), "reason", "already_succeeded", "force", opts.Force)
continue
}
executed = append(executed, s.Name())
runtime.executed = append(runtime.executed, s.Name())
priorOutcome := capturePriorStageOutcome(m, s.Name())
priorAnalyzeState := captureAnalyzeState(m, s.Name())
@@ -332,7 +239,7 @@ func executePlan(ctx context.Context, cfg *config.Config, plan BoundedPlan, opts
)
}
if result != nil && result.Disposition == stage.StageDispositionSkipped {
skipped = append(skipped, s.Name())
runtime.skipped = append(runtime.skipped, s.Name())
skippedAt := nowUTC()
if _, err := applyCompletedStageOutcome(m, s.Name(), result, nil, semanticConfig, priorOutcome, skippedAt); err != nil {
return nil, persistSessionTerminalFailure(
@@ -383,8 +290,8 @@ func executePlan(ctx context.Context, cfg *config.Config, plan BoundedPlan, opts
env.Logger.Info("stage succeeded", "stage", s.Name())
}
if containsStage(executed, "publish") {
if err := runPostPublishCleanup(ctx, env, manifestPath, m, executed); err != nil {
if containsStage(runtime.executed, "publish") {
if err := runPostPublishCleanup(ctx, env, manifestPath, m, runtime.executed); err != nil {
return nil, persistPostPublishCleanupFailure(
ctx, env.ManifestStore, manifestPath, m,
fmt.Errorf("post-publish cleanup incomplete: %w", err),
@@ -396,9 +303,9 @@ func executePlan(ctx context.Context, cfg *config.Config, plan BoundedPlan, opts
SessionID: cfg.Session.SessionID,
RunID: runID,
ManifestPath: manifestPath,
StageNames: runNames,
Executed: executed,
Skipped: skipped,
StageNames: runtime.stageNames,
Executed: runtime.executed,
Skipped: runtime.skipped,
}, nil
}