package app import ( "context" "fmt" "io" "strings" "gitea.maximumdirect.net/eric/narratio/internal/adapters/storage" "gitea.maximumdirect.net/eric/narratio/internal/artifactpolicy" "gitea.maximumdirect.net/eric/narratio/internal/artifacts" "gitea.maximumdirect.net/eric/narratio/internal/config" "gitea.maximumdirect.net/eric/narratio/internal/manifest" ) func buildHelperArtifactCatalog(cfg *config.Config, m *manifest.Manifest) (*artifacts.ArtifactCatalog, error) { configured := artifacts.ConfiguredArtifactDefinitions(nil) if cfg.Pipeline.Scriptorium != nil { configured = artifacts.ConfiguredArtifactDefinitions(cfg.Pipeline.Scriptorium.Artifacts) } extractionDefinitions := artifacts.ExtractionDefinitionsFromConfig(cfg.Pipeline.Notarius) effective, err := artifacts.ResolveEffectiveArtifactSet(configured, nil) if err != nil { return nil, err } catalog, err := artifacts.BootstrapRuntimeCatalog(configured, effective, extractionDefinitions) if err != nil { return nil, err } paths := artifacts.NewLocalStore(cfg.Pipeline.Workspace.Root).SessionPathsFor(cfg.Session.Campaign, cfg.Session.SessionID) if cfg.Pipeline.Notarius != nil && cfg.Pipeline.Notarius.Enabled { catalog.HydrateExtractionArtifacts(paths, m, extractionDefinitions) } return catalog, nil } func writeArtifactList(out io.Writer, cfg *config.Config, catalog *artifacts.ArtifactCatalog, locks *effectiveLocks, publishedRemoteState map[string]string) { lockSet := lockSourceSet(locks.All) fmt.Fprintln(out, "Built-in:") for _, transcript := range artifacts.RuntimeTranscriptArtifacts() { writeArtifactLine(out, transcript.SourceID, lockSet) } writeArtifactLine(out, artifacts.ArtifactBoundsSession, lockSet) fmt.Fprintln(out, "Configured:") for _, entry := range catalog.ListConfigured() { writeArtifactLine(out, entry.SourceID, lockSet) } fmt.Fprintln(out, "Extraction:") for _, entry := range catalog.ListExtraction() { state := "unavailable" if entry.Available { state = "available" } writeExtractionArtifactLine(out, entry.SourceID, state, entry.Provenance, lockSet) } fmt.Fprintln(out, "Previous-session:") if effective, err := resolveEffectiveArtifacts(cfg, nil); err == nil { for _, req := range artifacts.CollectPreviousArtifactRequirements(configuredScriptoriumArtifacts(cfg), effective) { fmt.Fprintf(out, "- %s required=%t\n", artifactpolicy.PreviousSessionSourceID(req.Name), req.Required) } } fmt.Fprintln(out, "Published:") for _, rule := range cfg.Pipeline.Publish.Outputs { writePublishedOutputLine(out, rule, catalog, lockSet, publishedRemoteState) } } func writeExtractionArtifactLine(out io.Writer, source, state, provenance string, lockSet map[string]config.PublishLockRule) { parts := []string{source, "planned", state} if strings.TrimSpace(provenance) != "" { parts = append(parts, "provenance="+strings.TrimSpace(provenance)) } if _, ok := lockSet[source]; ok { parts = append(parts, "locked") } fmt.Fprintf(out, "- %s\n", strings.Join(parts, " ")) } func writeArtifactLine(out io.Writer, source string, lockSet map[string]config.PublishLockRule) { parts := []string{source} if _, ok := lockSet[source]; ok { parts = append(parts, "locked") } fmt.Fprintf(out, "- %s\n", strings.Join(parts, " ")) } func writePublishedOutputLine(out io.Writer, rule config.PublishOutputRule, catalog *artifacts.ArtifactCatalog, lockSet map[string]config.PublishLockRule, remoteState map[string]string) { source := strings.TrimSpace(rule.Source) parts := []string{source} if _, ok := lockSet[source]; ok { parts = append(parts, "locked") } dest, showDest, err := helperPublishedOutputDest(rule, catalog) if err != nil { parts = append(parts, "remote=error") fmt.Fprintf(out, "- %s\n", strings.Join(parts, " ")) return } if showDest { parts = append(parts, "dest="+dest) } if state := remoteState[publishedOutputRemoteStateKey(source, dest)]; state != "" { parts = append(parts, state) } fmt.Fprintf(out, "- %s\n", strings.Join(parts, " ")) } func remotePublishedOutputAvailability(ctx context.Context, cfg *config.Config, store storage.ObjectStore, catalog *artifacts.ArtifactCatalog) map[string]string { out := map[string]string{} sessionPrefix := artifacts.S3SessionPrefix(cfg.Pipeline.Storage.S3.RootPrefix, cfg.Session.Campaign, cfg.Session.SessionID) for _, rule := range cfg.Pipeline.Publish.Outputs { source := strings.TrimSpace(rule.Source) dest, _, err := helperPublishedOutputDest(rule, catalog) if err != nil { out[publishedOutputRemoteStateKey(source, "")] = "remote=error" continue } key := artifacts.S3PublishedOutputKey(sessionPrefix, dest) if exists, err := store.Exists(ctx, key); err == nil && exists { out[publishedOutputRemoteStateKey(source, dest)] = "remote=published" } else if err != nil { out[publishedOutputRemoteStateKey(source, dest)] = "remote=error" } else { out[publishedOutputRemoteStateKey(source, dest)] = "remote=missing" } } return out } func remotePublishedOutputAvailabilityForCurrent( ctx context.Context, cfg *config.Config, store storage.ObjectStore, catalog *artifacts.ArtifactCatalog, current *RemoteCurrentState, ) map[string]string { if current == nil || current.Commit == nil { return remotePublishedOutputAvailability(ctx, cfg, store, catalog) } out := map[string]string{} runPrefix := artifacts.S3RunPrefix(current.SessionPrefix, current.RunID) for _, rule := range cfg.Pipeline.Publish.Outputs { source := strings.TrimSpace(rule.Source) dest, _, err := helperPublishedOutputDest(rule, catalog) if err != nil { out[publishedOutputRemoteStateKey(source, "")] = "remote=error" continue } key := artifacts.S3RunRelativeDestinationKey(runPrefix, dest) if committedPublishedOutput(current.Commit, key) { out[publishedOutputRemoteStateKey(source, dest)] = "remote=published" } else { out[publishedOutputRemoteStateKey(source, dest)] = "remote=missing" } } return out } func committedPublishedOutput(commit *artifacts.RemoteCommitManifest, key string) bool { if commit == nil { return false } for _, artifact := range commit.Artifacts { if artifact.Type == artifacts.RemoteArtifactTypePublishedOutput && artifact.DestinationKey == key { return true } } return false } func helperPublishedOutputDest(rule config.PublishOutputRule, catalog *artifacts.ArtifactCatalog) (string, bool, error) { source := strings.TrimSpace(rule.Source) normalized, err := artifactpolicy.ResolvePublishedDestinationWithExtractions( source, rule.Dest, helperConfiguredOutputPathMap(catalog), helperExtractionOutputSet(catalog), ) if err != nil { return "", false, err } entry, ok := catalog.Lookup(source) showDest := !ok || strings.TrimSpace(entry.CanonicalRelPath) != normalized return normalized, showDest, nil } func helperExtractionOutputSet(catalog *artifacts.ArtifactCatalog) map[string]struct{} { out := map[string]struct{}{} if catalog == nil { return out } for _, entry := range catalog.ListExtraction() { if strings.TrimSpace(entry.ExtractionKey) != "" { out[entry.ExtractionKey] = struct{}{} } } return out } func helperConfiguredOutputPathMap(catalog *artifacts.ArtifactCatalog) map[string]string { out := map[string]string{} if catalog == nil { return out } for _, entry := range catalog.ListConfigured() { if strings.TrimSpace(entry.ConfiguredKey) == "" { continue } out[entry.ConfiguredKey] = strings.TrimSpace(entry.CanonicalRelPath) } return out } func publishedOutputRemoteStateKey(source, dest string) string { return strings.TrimSpace(source) + "\x00" + strings.TrimSpace(dest) }