Implement idempotent cleanup behavior
This commit is contained in:
101
internal/stage/cleanup.go
Normal file
101
internal/stage/cleanup.go
Normal file
@@ -0,0 +1,101 @@
|
||||
package stage
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"gitea.maximumdirect.net/eric/narratio/internal/fileops"
|
||||
"gitea.maximumdirect.net/eric/narratio/internal/manifest"
|
||||
)
|
||||
|
||||
const (
|
||||
cleanupSkipReason = "cleanup policies are disabled"
|
||||
cleanupSemanticConfigVersion = 1
|
||||
)
|
||||
|
||||
type cleanupStage struct{}
|
||||
|
||||
type cleanupSemanticConfig struct {
|
||||
DeleteSpoolAudio bool `json:"delete_spool_audio"`
|
||||
SpoolRoot string `json:"spool_root"`
|
||||
DeleteWorkspace bool `json:"delete_workspace"`
|
||||
WorkspaceRoot string `json:"workspace_root"`
|
||||
}
|
||||
|
||||
func (cleanupStage) Name() string { return "cleanup" }
|
||||
|
||||
func (cleanupStage) Run(ctx context.Context, env *Env, m *manifest.Manifest) (*StageResult, error) {
|
||||
plan, err := buildCleanupPlan(env, m)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if !plan.enabled {
|
||||
return &StageResult{
|
||||
Disposition: StageDispositionSkipped,
|
||||
SkipReason: cleanupSkipReason,
|
||||
}, nil
|
||||
}
|
||||
|
||||
completedPolicies := make([]string, 0, len(plan.targets))
|
||||
for _, target := range plan.targets {
|
||||
if err := ctx.Err(); err != nil {
|
||||
return nil, fmt.Errorf("cleanup canceled before %s: %w", target.policy, err)
|
||||
}
|
||||
if err := fileops.RemoveAllUnderRoot(target.root, target.target); err != nil {
|
||||
return nil, fmt.Errorf("apply %s: %w", target.policy, err)
|
||||
}
|
||||
completedPolicies = append(completedPolicies, target.policy)
|
||||
}
|
||||
|
||||
return &StageResult{Metadata: map[string]any{
|
||||
"stage": "cleanup",
|
||||
"published_run_id": plan.publishedRunID,
|
||||
"completed_policies": completedPolicies,
|
||||
}}, nil
|
||||
}
|
||||
|
||||
func (cleanupStage) SemanticConfigFingerprint(env *Env) (manifest.SemanticConfigFingerprint, error) {
|
||||
if env == nil || env.Config == nil || env.Config.Pipeline == nil {
|
||||
return manifest.SemanticConfigFingerprint{}, fmt.Errorf("resolved pipeline configuration is required")
|
||||
}
|
||||
pipeline := env.Config.Pipeline
|
||||
return FingerprintSemanticConfig(cleanupSemanticConfigVersion, cleanupSemanticConfig{
|
||||
DeleteSpoolAudio: pipeline.Spool.DeleteAudioAfterPublish,
|
||||
SpoolRoot: normalizedCleanupRoot(pipeline.Spool.Root),
|
||||
DeleteWorkspace: pipeline.Workspace.CleanupAfterPublish,
|
||||
WorkspaceRoot: normalizedCleanupRoot(pipeline.Workspace.Root),
|
||||
})
|
||||
}
|
||||
|
||||
func (cleanupStage) ValidateResume(ctx context.Context, env *Env, m *manifest.Manifest) (ResumeValidation, error) {
|
||||
plan, err := buildCleanupPlan(env, m)
|
||||
if err != nil {
|
||||
return ResumeValidation{}, err
|
||||
}
|
||||
if !plan.enabled {
|
||||
return NonResumable(cleanupSkipReason), nil
|
||||
}
|
||||
for _, target := range plan.targets {
|
||||
if err := ctx.Err(); err != nil {
|
||||
return ResumeValidation{}, fmt.Errorf("validate cleanup resume for %s: %w", target.policy, err)
|
||||
}
|
||||
if _, err := os.Lstat(target.target); err == nil {
|
||||
return NonResumable(fmt.Sprintf("cleanup target for %s exists", target.policy)), nil
|
||||
} else if !errors.Is(err, os.ErrNotExist) {
|
||||
return ResumeValidation{}, fmt.Errorf("inspect cleanup target for %s: %w", target.policy, err)
|
||||
}
|
||||
}
|
||||
return Resumable(), nil
|
||||
}
|
||||
|
||||
func normalizedCleanupRoot(root string) string {
|
||||
root = strings.TrimSpace(root)
|
||||
if root == "" {
|
||||
return ""
|
||||
}
|
||||
return filepath.Clean(root)
|
||||
}
|
||||
Reference in New Issue
Block a user