248 lines
8.5 KiB
Go
248 lines
8.5 KiB
Go
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)
|
|
}
|
|
catalog.HydrateAnalyzeArtifacts(paths, m, configured)
|
|
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:")
|
|
origins := config.ArtifactFamilies(cfg.Pipeline).Members
|
|
for _, entry := range catalog.ListConfigured() {
|
|
state := "unavailable"
|
|
if entry.Available {
|
|
state = "available"
|
|
}
|
|
writeConfiguredArtifactLine(out, entry.SourceID, state, entry.Provenance, lockSet, origins)
|
|
}
|
|
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 writeConfiguredArtifactLine(out io.Writer, source, state, provenance string, lockSet map[string]config.PublishLockRule, origins map[string]config.ArtifactFamilyMemberOrigin) {
|
|
parts := []string{source, "planned", state}
|
|
if key, ok := artifactpolicy.ParseConfiguredSource(source); ok {
|
|
if origin, family := origins[key]; family {
|
|
parts = append(parts, "family="+origin.Family, "character_id="+origin.CharacterID)
|
|
}
|
|
}
|
|
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 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 unresolvedRemotePublishedOutputAvailability(cfg *config.Config, catalog *artifacts.ArtifactCatalog, state string) map[string]string {
|
|
out := map[string]string{}
|
|
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
|
|
}
|
|
out[publishedOutputRemoteStateKey(source, dest)] = state
|
|
}
|
|
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 unresolvedRemotePublishedOutputAvailability(cfg, catalog, "remote=missing")
|
|
}
|
|
out := map[string]string{}
|
|
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
|
|
}
|
|
declaration, ok := committedPublishedOutput(current.Commit, source)
|
|
if !ok {
|
|
out[publishedOutputRemoteStateKey(source, dest)] = "remote=missing"
|
|
continue
|
|
}
|
|
identity, err := artifacts.ParseImmutableRunObjectKey(current.SessionPrefix, declaration.DestinationKey)
|
|
if err != nil {
|
|
out[publishedOutputRemoteStateKey(source, dest)] = "remote=error"
|
|
continue
|
|
}
|
|
exists, err := store.Exists(ctx, declaration.DestinationKey)
|
|
if err != nil {
|
|
out[publishedOutputRemoteStateKey(source, dest)] = "remote=error"
|
|
continue
|
|
}
|
|
if !exists {
|
|
out[publishedOutputRemoteStateKey(source, dest)] = "remote=missing"
|
|
continue
|
|
}
|
|
state := "remote=published"
|
|
if identity.RunID != current.RunID {
|
|
state += " origin_run=" + identity.RunID
|
|
}
|
|
out[publishedOutputRemoteStateKey(source, dest)] = state
|
|
}
|
|
return out
|
|
}
|
|
|
|
func committedPublishedOutput(commit *artifacts.RemoteCommitManifest, source string) (artifacts.RemoteArtifact, bool) {
|
|
if commit == nil {
|
|
return artifacts.RemoteArtifact{}, false
|
|
}
|
|
for _, artifact := range commit.Artifacts {
|
|
if artifact.Type == artifacts.RemoteArtifactTypePublishedOutput && artifact.Source == source {
|
|
return artifact, true
|
|
}
|
|
}
|
|
return artifacts.RemoteArtifact{}, 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)
|
|
}
|