Reconcile removed analysis artifacts
This commit is contained in:
@@ -71,25 +71,19 @@ func (analyzeStage) Run(ctx context.Context, env *Env, m *manifest.Manifest) (*S
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("analyze: resolve run-stage layout: %w", err)
|
return nil, fmt.Errorf("analyze: resolve run-stage layout: %w", err)
|
||||||
}
|
}
|
||||||
if env.Config.Pipeline.Scriptorium == nil {
|
scriptoriumCfg := env.Config.Pipeline.Scriptorium
|
||||||
return &StageResult{Metadata: map[string]any{
|
noWorkReason := ""
|
||||||
"stage": "analyze",
|
if scriptoriumCfg == nil {
|
||||||
"skipped": true,
|
noWorkReason = "pipeline.scriptorium is not configured"
|
||||||
"reason": "pipeline.scriptorium is not configured",
|
scriptoriumCfg = &config.ScriptoriumConfig{}
|
||||||
}}, nil
|
} else if len(scriptoriumCfg.Artifacts) == 0 {
|
||||||
}
|
noWorkReason = "no scriptorium artifacts configured"
|
||||||
if len(env.Config.Pipeline.Scriptorium.Artifacts) == 0 {
|
|
||||||
return &StageResult{Metadata: map[string]any{
|
|
||||||
"stage": "analyze",
|
|
||||||
"skipped": true,
|
|
||||||
"reason": "no scriptorium artifacts configured",
|
|
||||||
}}, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
effective := env.EffectiveArtifacts
|
effective := env.EffectiveArtifacts
|
||||||
if !effective.Resolved() {
|
if !effective.Resolved() {
|
||||||
effective, err = artifacts.ResolveEffectiveArtifactSet(
|
effective, err = artifacts.ResolveEffectiveArtifactSet(
|
||||||
artifacts.ConfiguredArtifactDefinitions(env.Config.Pipeline.Scriptorium.Artifacts),
|
artifacts.ConfiguredArtifactDefinitions(scriptoriumCfg.Artifacts),
|
||||||
env.SelectedArtifactKeys,
|
env.SelectedArtifactKeys,
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -100,7 +94,7 @@ func (analyzeStage) Run(ctx context.Context, env *Env, m *manifest.Manifest) (*S
|
|||||||
runtimeCatalog, err := buildAnalyzeRuntimeArtifactCatalog(
|
runtimeCatalog, err := buildAnalyzeRuntimeArtifactCatalog(
|
||||||
paths,
|
paths,
|
||||||
m,
|
m,
|
||||||
env.Config.Pipeline.Scriptorium,
|
scriptoriumCfg,
|
||||||
env.Config.Pipeline.Notarius,
|
env.Config.Pipeline.Notarius,
|
||||||
effective,
|
effective,
|
||||||
)
|
)
|
||||||
@@ -108,12 +102,8 @@ func (analyzeStage) Run(ctx context.Context, env *Env, m *manifest.Manifest) (*S
|
|||||||
return nil, fmt.Errorf("analyze: build runtime artifact catalog: %w", err)
|
return nil, fmt.Errorf("analyze: build runtime artifact catalog: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(effective.Keys()) == 0 {
|
if len(effective.Keys()) == 0 && noWorkReason == "" {
|
||||||
return &StageResult{Metadata: map[string]any{
|
noWorkReason = "no selected scriptorium artifacts to execute"
|
||||||
"stage": "analyze",
|
|
||||||
"skipped": true,
|
|
||||||
"reason": "no selected scriptorium artifacts to execute",
|
|
||||||
}}, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
execution := analyzeExecutionContext{
|
execution := analyzeExecutionContext{
|
||||||
@@ -125,12 +115,12 @@ func (analyzeStage) Run(ctx context.Context, env *Env, m *manifest.Manifest) (*S
|
|||||||
TranscriptRefs: discoverAnalyzeTranscriptRefs(m, paths),
|
TranscriptRefs: discoverAnalyzeTranscriptRefs(m, paths),
|
||||||
Catalog: runtimeCatalog,
|
Catalog: runtimeCatalog,
|
||||||
}
|
}
|
||||||
reconciliation, err := reconcileAnalyzeArtifacts(env.Config.Pipeline.Scriptorium, execution)
|
reconciliation, err := reconcileAnalyzeArtifacts(scriptoriumCfg, execution)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("analyze: reconcile configured artifacts: %w", err)
|
return nil, fmt.Errorf("analyze: reconcile configured artifacts: %w", err)
|
||||||
}
|
}
|
||||||
workPlan, err := planAnalyzeWork(
|
workPlan, err := planAnalyzeWork(
|
||||||
env.Config.Pipeline.Scriptorium,
|
scriptoriumCfg,
|
||||||
env.SelectedArtifactKeys,
|
env.SelectedArtifactKeys,
|
||||||
env.Force,
|
env.Force,
|
||||||
reconciliation,
|
reconciliation,
|
||||||
@@ -148,10 +138,10 @@ func (analyzeStage) Run(ctx context.Context, env *Env, m *manifest.Manifest) (*S
|
|||||||
}
|
}
|
||||||
|
|
||||||
for _, item := range workPlan.ExecutionOrder {
|
for _, item := range workPlan.ExecutionOrder {
|
||||||
artifactCfg := env.Config.Pipeline.Scriptorium.Artifacts[item.Key]
|
artifactCfg := scriptoriumCfg.Artifacts[item.Key]
|
||||||
fingerprint, _, _, err := computeAnalyzeArtifactFingerprint(
|
fingerprint, _, _, err := computeAnalyzeArtifactFingerprint(
|
||||||
item.Key,
|
item.Key,
|
||||||
env.Config.Pipeline.Scriptorium,
|
scriptoriumCfg,
|
||||||
execution,
|
execution,
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -167,7 +157,12 @@ func (analyzeStage) Run(ctx context.Context, env *Env, m *manifest.Manifest) (*S
|
|||||||
return state.failedResult(item, artifactCfg, fingerprint, err), err
|
return state.failedResult(item, artifactCfg, fingerprint, err), err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return state.successResult(), nil
|
result := state.successResult()
|
||||||
|
if noWorkReason != "" {
|
||||||
|
result.Metadata["skipped"] = true
|
||||||
|
result.Metadata["reason"] = noWorkReason
|
||||||
|
}
|
||||||
|
return result, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func analyzeArtifactOrigin(execution analyzeExecutionContext, key string) (config.ArtifactFamilyMemberOrigin, bool) {
|
func analyzeArtifactOrigin(execution analyzeExecutionContext, key string) (config.ArtifactFamilyMemberOrigin, bool) {
|
||||||
|
|||||||
@@ -15,8 +15,8 @@ func (analyzeStage) ValidateResume(_ context.Context, env *Env, m *manifest.Mani
|
|||||||
return ResumeValidation{}, fmt.Errorf("analyze resume: resolved stage environment config is required")
|
return ResumeValidation{}, fmt.Errorf("analyze resume: resolved stage environment config is required")
|
||||||
}
|
}
|
||||||
cfg := env.Config.Pipeline.Scriptorium
|
cfg := env.Config.Pipeline.Scriptorium
|
||||||
if cfg == nil || len(cfg.Artifacts) == 0 {
|
if cfg == nil {
|
||||||
return ResumeValidation{Resumable: true, Analyze: &AnalyzeResumeSummary{}}, nil
|
cfg = &config.ScriptoriumConfig{}
|
||||||
}
|
}
|
||||||
sessionID := ""
|
sessionID := ""
|
||||||
if m != nil {
|
if m != nil {
|
||||||
@@ -39,9 +39,6 @@ func (analyzeStage) ValidateResume(_ context.Context, env *Env, m *manifest.Mani
|
|||||||
return ResumeValidation{}, fmt.Errorf("analyze resume: resolve effective artifacts: %w", err)
|
return ResumeValidation{}, fmt.Errorf("analyze resume: resolve effective artifacts: %w", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if len(effective.Keys()) == 0 {
|
|
||||||
return ResumeValidation{Resumable: true, Analyze: &AnalyzeResumeSummary{}}, nil
|
|
||||||
}
|
|
||||||
paths := sessionPathsForEnv(env, sessionID)
|
paths := sessionPathsForEnv(env, sessionID)
|
||||||
catalog, err := buildAnalyzeRuntimeArtifactCatalog(
|
catalog, err := buildAnalyzeRuntimeArtifactCatalog(
|
||||||
paths, m, cfg, env.Config.Pipeline.Notarius, effective,
|
paths, m, cfg, env.Config.Pipeline.Notarius, effective,
|
||||||
@@ -62,9 +59,16 @@ func (analyzeStage) ValidateResume(_ context.Context, env *Env, m *manifest.Mani
|
|||||||
return ResumeValidation{}, fmt.Errorf("analyze resume: plan configured artifacts: %w", err)
|
return ResumeValidation{}, fmt.Errorf("analyze resume: plan configured artifacts: %w", err)
|
||||||
}
|
}
|
||||||
summary := analyzeResumeSummary(plan, env.Config.Pipeline)
|
summary := analyzeResumeSummary(plan, env.Config.Pipeline)
|
||||||
if len(plan.ExecutionOrder) == 0 {
|
if len(plan.ExecutionOrder) == 0 && len(plan.Removed) == 0 {
|
||||||
return ResumeValidation{Resumable: true, Analyze: summary}, nil
|
return ResumeValidation{Resumable: true, Analyze: summary}, nil
|
||||||
}
|
}
|
||||||
|
if len(plan.ExecutionOrder) == 0 {
|
||||||
|
keys := analyzePlanKeysForMetadata(plan.Removed)
|
||||||
|
return ResumeValidation{
|
||||||
|
Reason: "analysis artifact state requires reconciliation: " + strings.Join(keys, ", "),
|
||||||
|
Analyze: summary,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
keys := analyzePlanKeysForMetadata(plan.ExecutionOrder)
|
keys := analyzePlanKeysForMetadata(plan.ExecutionOrder)
|
||||||
return ResumeValidation{
|
return ResumeValidation{
|
||||||
Reason: "analysis artifacts require execution: " + strings.Join(keys, ", "),
|
Reason: "analysis artifacts require execution: " + strings.Join(keys, ", "),
|
||||||
|
|||||||
@@ -4,8 +4,11 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"reflect"
|
"reflect"
|
||||||
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
"gitea.maximumdirect.net/eric/narratio/internal/adapters/scriptorium"
|
||||||
|
"gitea.maximumdirect.net/eric/narratio/internal/artifacts"
|
||||||
"gitea.maximumdirect.net/eric/narratio/internal/config"
|
"gitea.maximumdirect.net/eric/narratio/internal/config"
|
||||||
"gitea.maximumdirect.net/eric/narratio/internal/manifest"
|
"gitea.maximumdirect.net/eric/narratio/internal/manifest"
|
||||||
)
|
)
|
||||||
@@ -37,6 +40,69 @@ func TestAnalyzeResumeValidationHonorsFullAndPartialSelections(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestAnalyzeRemovedConfigurationRequiresAndPersistsReconciliation(t *testing.T) {
|
||||||
|
for _, test := range []struct {
|
||||||
|
name string
|
||||||
|
remove func(*config.PipelineConfig)
|
||||||
|
reason string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "scriptorium omitted",
|
||||||
|
remove: func(pipeline *config.PipelineConfig) {
|
||||||
|
pipeline.Scriptorium = nil
|
||||||
|
},
|
||||||
|
reason: "pipeline.scriptorium is not configured",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "artifact map empty",
|
||||||
|
remove: func(pipeline *config.PipelineConfig) {
|
||||||
|
pipeline.Scriptorium.Artifacts = map[string]config.ScriptoriumArtifactConfig{}
|
||||||
|
},
|
||||||
|
reason: "no scriptorium artifacts configured",
|
||||||
|
},
|
||||||
|
} {
|
||||||
|
t.Run(test.name, func(t *testing.T) {
|
||||||
|
env, m, initialRequests := currentAnalyzeFixture(t, false)
|
||||||
|
test.remove(env.Config.Pipeline)
|
||||||
|
|
||||||
|
validation, err := (analyzeStage{}).ValidateResume(context.Background(), env, m)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if validation.Resumable || !strings.Contains(validation.Reason, "session_recap") {
|
||||||
|
t.Fatalf("validation = %#v, want reconciliation of removed session_recap", validation)
|
||||||
|
}
|
||||||
|
|
||||||
|
result, err := (analyzeStage{}).Run(context.Background(), env, m)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if result.Metadata["skipped"] != true || result.Metadata["reason"] != test.reason {
|
||||||
|
t.Fatalf("metadata = %#v, want skipped reason %q", result.Metadata, test.reason)
|
||||||
|
}
|
||||||
|
if len(result.Outputs) != 0 || len(m.Stages["analyze"].AnalyzeArtifacts) != 0 {
|
||||||
|
t.Fatalf("result outputs = %#v; analyze records = %#v, want both empty", result.Outputs, m.Stages["analyze"].AnalyzeArtifacts)
|
||||||
|
}
|
||||||
|
fake := env.Scriptorium.(*scriptorium.FakeRunner)
|
||||||
|
if len(fake.RunRequests) != initialRequests {
|
||||||
|
t.Fatalf("adapter requests = %d, want unchanged count %d", len(fake.RunRequests), initialRequests)
|
||||||
|
}
|
||||||
|
|
||||||
|
paths := sessionPathsForEnv(env, m.SessionID)
|
||||||
|
stored, err := env.ManifestStore.Load(
|
||||||
|
context.Background(),
|
||||||
|
artifacts.SessionManifestPathForCampaign(paths.WorkspaceRoot, m.Campaign, m.SessionID),
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if len(stored.Stages["analyze"].AnalyzeArtifacts) != 0 {
|
||||||
|
t.Fatalf("durable analyze records = %#v, want empty", stored.Stages["analyze"].AnalyzeArtifacts)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestAnalyzeResumeValidationReportsForceAndCurrentPrerequisites(t *testing.T) {
|
func TestAnalyzeResumeValidationReportsForceAndCurrentPrerequisites(t *testing.T) {
|
||||||
env, m, _ := currentAnalyzeFixture(t, true)
|
env, m, _ := currentAnalyzeFixture(t, true)
|
||||||
env.SelectedArtifactKeys = []string{"session_recap"}
|
env.SelectedArtifactKeys = []string{"session_recap"}
|
||||||
|
|||||||
Reference in New Issue
Block a user