Invalidate downstream results when stages are replaced

This commit is contained in:
2026-08-10 01:38:24 +00:00
parent df40cbec6e
commit 0d6f2dd0ce
7 changed files with 509 additions and 528 deletions

View File

@@ -3,6 +3,7 @@ package app
import (
"context"
"fmt"
"strings"
"time"
"gitea.maximumdirect.net/eric/narratio/internal/artifacts"
@@ -23,22 +24,40 @@ type stageDecision struct {
Action stageAction
}
const (
staleReasonForcedReplacement = "upstream stage was force-run"
staleReasonChangedResult = "upstream stage result changed"
staleReasonFailure = "upstream stage failed"
staleReasonSelfSkip = "upstream stage self-skipped"
staleReasonNotResumable = "upstream stage result was not resumable"
)
type priorStageOutcome struct {
exists bool
status manifest.StageStatus
skipReason string
outputs int
}
func decideStageActions(stages []stage.Stage, m *manifest.Manifest, force bool) []stageDecision {
out := make([]stageDecision, 0, len(stages))
for _, s := range stages {
action := stageActionRun
// TODO: incorporate stale detection once checksum/input change tracking is implemented.
if !force && stageSucceeded(m, s.Name()) {
action = stageActionSkip
}
out = append(out, stageDecision{
Stage: s,
Action: action,
Action: decideStageAction(s, m, force),
})
}
return out
}
func decideStageAction(s stage.Stage, m *manifest.Manifest, force bool) stageAction {
// TODO: incorporate stale detection once checksum/input change tracking is implemented.
if !force && stageSucceeded(m, s.Name()) {
return stageActionSkip
}
return stageActionRun
}
func stageSucceeded(m *manifest.Manifest, name string) bool {
if m == nil || m.Stages == nil {
return false
@@ -47,6 +66,29 @@ func stageSucceeded(m *manifest.Manifest, name string) bool {
return sr != nil && sr.Status == manifest.StatusSucceeded
}
func capturePriorStageOutcome(m *manifest.Manifest, name string) priorStageOutcome {
if m == nil || m.Stages == nil || m.Stages[name] == nil {
return priorStageOutcome{}
}
record := m.Stages[name]
outcome := priorStageOutcome{
exists: true,
status: record.Status,
outputs: len(record.Outputs),
}
if record.Error != nil && record.Error.Code == "skipped" {
outcome.skipReason = record.Error.Message
}
return outcome
}
func (o priorStageOutcome) isSameSelfSkip(reason string) bool {
return o.exists &&
o.status == manifest.StatusSkipped &&
o.outputs == 0 &&
o.skipReason == strings.TrimSpace(reason)
}
func loadManifestIfPresent(ctx context.Context, cfg *config.Config) (*manifest.Manifest, error) {
path := artifacts.SessionManifestPathForCampaign(
cfg.Pipeline.Workspace.Root,
@@ -91,10 +133,6 @@ func downstreamStageNames(stageName string) []string {
return nil
}
func invalidateDownstreamSucceededStages(m *manifest.Manifest, upstreamStage string, at time.Time) []string {
return invalidateDownstreamSucceededStagesWithReason(m, upstreamStage, at, "upstream stage rerun with force")
}
func invalidateDownstreamSucceededStagesWithReason(m *manifest.Manifest, upstreamStage string, at time.Time, reason string) []string {
if m == nil || m.Stages == nil {
return nil