299 lines
9.7 KiB
Go
299 lines
9.7 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 []artifacts.RemoteLock, publishedRemoteState map[string]string) {
|
|
lockSet := remoteLockSourceSet(locks)
|
|
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]struct{}, 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]struct{}) {
|
|
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]struct{}) {
|
|
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]struct{}, 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 remoteLockSourceSet(locks []artifacts.RemoteLock) map[string]struct{} {
|
|
out := make(map[string]struct{}, len(locks))
|
|
for _, lock := range locks {
|
|
out[lock.Source] = struct{}{}
|
|
}
|
|
return out
|
|
}
|
|
|
|
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")
|
|
}
|
|
outcomes := committedPublishOutputOutcomes(current.Manifest)
|
|
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
|
|
}
|
|
outcome := outcomes[source]
|
|
state := ""
|
|
if outcome != "" {
|
|
state = "outcome=" + outcome + " "
|
|
}
|
|
declaration, ok := committedPublishedOutput(current.Commit, source)
|
|
if !ok {
|
|
out[publishedOutputRemoteStateKey(source, dest)] = state + "remote=missing"
|
|
continue
|
|
}
|
|
identity, err := artifacts.ParsePublishedOutputObjectKey(current.SessionPrefix, declaration.DestinationKey)
|
|
if err != nil {
|
|
out[publishedOutputRemoteStateKey(source, dest)] = state + "remote=error"
|
|
continue
|
|
}
|
|
exists, err := store.Exists(ctx, declaration.DestinationKey)
|
|
if err != nil {
|
|
out[publishedOutputRemoteStateKey(source, dest)] = state + "remote=error"
|
|
continue
|
|
}
|
|
if !exists {
|
|
out[publishedOutputRemoteStateKey(source, dest)] = state + "remote=missing"
|
|
continue
|
|
}
|
|
state += "remote=published"
|
|
if identity.RunID != current.RunID {
|
|
state += " origin_run=" + identity.RunID
|
|
}
|
|
out[publishedOutputRemoteStateKey(source, dest)] = state
|
|
}
|
|
return out
|
|
}
|
|
|
|
func committedPublishOutputOutcomes(m *manifest.Manifest) map[string]string {
|
|
out := map[string]string{}
|
|
if m == nil || m.Stages["publish"] == nil {
|
|
return out
|
|
}
|
|
raw := m.Stages["publish"].Metadata["publish_output_outcomes"]
|
|
appendOutcome := func(item map[string]any) {
|
|
source, sourceOK := item["source"].(string)
|
|
status, statusOK := item["status"].(string)
|
|
if sourceOK && statusOK && isPublishOutputOutcome(status) {
|
|
out[source] = status
|
|
}
|
|
}
|
|
switch items := raw.(type) {
|
|
case []any:
|
|
for _, rawItem := range items {
|
|
if item, ok := rawItem.(map[string]any); ok {
|
|
appendOutcome(item)
|
|
}
|
|
}
|
|
case []map[string]any:
|
|
for _, item := range items {
|
|
appendOutcome(item)
|
|
}
|
|
}
|
|
return out
|
|
}
|
|
|
|
func isPublishOutputOutcome(status string) bool {
|
|
switch status {
|
|
case "published", "carried_locked", "locked_unpublished", "optional_missing", "unselected":
|
|
return true
|
|
default:
|
|
return false
|
|
}
|
|
}
|
|
|
|
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)
|
|
}
|