package app import ( "context" "fmt" "path/filepath" "strings" "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/manifest" ) var removeRunScopedDirFn = removeRunScopedDir func runPostPublishCleanup(ctx context.Context, env *Env, manifestPath string, m *manifest.Manifest, executed []string) error { if env == nil || env.Config == nil || env.Config.Pipeline == nil || m == nil { return nil } cleanup := m.PostPublishCleanup if cleanup == nil { var err error cleanup, err = createPostPublishCleanup(env.Config, m, executed) if err != nil { return err } if cleanup == nil { return nil } m.PostPublishCleanup = cleanup if err := env.ManifestStore.Save(ctx, manifestPath, m); err != nil { return fmt.Errorf("persist post-publish cleanup obligation %q: %w", manifestPath, err) } } for index := range cleanup.Targets { target := &cleanup.Targets[index] if target.Completed { continue } if err := removeRunScopedDirFn(target.Root, target.Path, target.Policy); err != nil { return fmt.Errorf("complete post-publish cleanup for %q: %w", target.Path, err) } target.Completed = true if err := env.ManifestStore.Save(ctx, manifestPath, m); err != nil { target.Completed = false return fmt.Errorf("persist post-publish cleanup completion %q: %w", manifestPath, err) } } return nil } func createPostPublishCleanup(cfg *config.Config, m *manifest.Manifest, executed []string) (*manifest.PostPublishCleanup, error) { spoolRequested := cfg.Pipeline.Spool.DeleteAudioAfterPublish workRequested := cfg.Pipeline.Workspace.CleanupAfterPublish if !spoolRequested && !workRequested { return nil, nil } sr := publishStageRecordForCleanup(m) publishedRunID, eligible, _ := publishCleanupEligible(cfg, m, sr, executed) if !eligible { return nil, nil } spoolDir := strings.TrimSpace(m.LocalSpoolDir) if spoolDir == "" { spoolDir = artifacts.SessionSpoolAudioDir( cfg.Pipeline.Spool.Root, strings.TrimSpace(m.Campaign), strings.TrimSpace(m.SessionID), publishedRunID, ) } workDir := strings.TrimSpace(m.LocalWorkDir) if workDir == "" { workDir = artifacts.SessionRunRootForCampaign( cfg.Pipeline.Workspace.Root, strings.TrimSpace(m.Campaign), strings.TrimSpace(m.SessionID), publishedRunID, ) } cleanup := &manifest.PostPublishCleanup{ CommittedRunID: publishedRunID, RemoteCommitKey: strings.TrimSpace(asString(sr.Metadata["remote_commit_key"])), CurrentCommitPointerKey: strings.TrimSpace(asString(sr.Metadata["current_commit_pointer_key"])), } if spoolRequested { cleanup.Targets = append(cleanup.Targets, manifest.CleanupTarget{ Policy: "pipeline.spool.delete_audio_after_publish", Root: strings.TrimSpace(cfg.Pipeline.Spool.Root), Path: filepath.Clean(spoolDir), }) } if workRequested { cleanup.Targets = append(cleanup.Targets, manifest.CleanupTarget{ Policy: "pipeline.workspace.cleanup_after_publish", Root: strings.TrimSpace(cfg.Pipeline.Workspace.Root), Path: filepath.Clean(workDir), }) } return cleanup, nil } func publishStageRecordForCleanup(m *manifest.Manifest) *manifest.StageRecord { if m == nil { return nil } sr := m.Stages["publish"] if sr == nil || sr.Status != manifest.StatusSucceeded { return nil } return sr } func publishCleanupEligible(cfg *config.Config, m *manifest.Manifest, sr *manifest.StageRecord, executed []string) (string, bool, string) { if cfg == nil || cfg.Pipeline == nil || cfg.Pipeline.Publish == nil { return "", false, "publish configuration is missing" } enabled := true if cfg.Pipeline.Publish.Enabled != nil { enabled = *cfg.Pipeline.Publish.Enabled } if !enabled { return "", false, "publish.enabled is false" } uploadRun := true if cfg.Pipeline.Publish.UploadRun != nil { uploadRun = *cfg.Pipeline.Publish.UploadRun } if !uploadRun { return "", false, "publish.upload_run is false" } if sr == nil || sr.Metadata == nil { return "", false, "publish metadata is missing" } if skipped, _ := sr.Metadata["skipped"].(bool); skipped { return "", false, "publish stage was skipped" } if uploaded, _ := sr.Metadata["uploaded"].(bool); !uploaded { return "", false, "publish did not upload run record" } if strings.TrimSpace(asString(sr.Metadata["remote_commit_key"])) == "" { return "", false, "publish remote commit key is missing" } if strings.TrimSpace(asString(sr.Metadata["current_commit_pointer_key"])) == "" { return "", false, "publish current commit pointer key is missing" } publishedRunID := strings.TrimSpace(asString(sr.Metadata["published_run_id"])) if publishedRunID == "" && containsStage(executed, "publish") && m != nil { publishedRunID = strings.TrimSpace(m.RunID) } if publishedRunID == "" { return "", false, "publish run id is missing" } return publishedRunID, true, "" } func containsStage(names []string, target string) bool { for _, name := range names { if name == target { return true } } return false } type scopedDir struct { RootAbs string TargetAbs string Exists bool } func removeRunScopedDir(root, target, policy string) error { dir, err := validateScopedDir(root, target, policy) if err != nil { return err } if !dir.Exists { return nil } if err := fileops.RemoveAllUnderRoot(dir.RootAbs, dir.TargetAbs); err != nil { return fmt.Errorf("cleanup policy %s: remove %q: %w", policy, dir.TargetAbs, err) } return nil } func validateScopedDir(root, target, policy string) (scopedDir, error) { return validateScopedTarget(root, target, policy, true) } func asString(v any) string { s, _ := v.(string) return s }