All checks were successful
ci/woodpecker/tag/release Pipeline was successful
259 lines
9.5 KiB
Go
259 lines
9.5 KiB
Go
package artifactpolicy
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"regexp"
|
|
"strings"
|
|
|
|
"gitea.maximumdirect.net/eric/narratio/internal/artifactmodel"
|
|
"gitea.maximumdirect.net/eric/narratio/internal/pathsafe"
|
|
)
|
|
|
|
const (
|
|
SourceBoundsSession = "narratio.bounds.session"
|
|
|
|
SourceInputPlayers = "narratio.input.players"
|
|
SourceInputParty = "narratio.input.party"
|
|
SourceInputGlossary = "narratio.input.glossary"
|
|
|
|
configuredSourcePrefix = "narratio.artifact."
|
|
previousConfiguredSrcPrefix = "narratio.previous_session.artifact."
|
|
)
|
|
|
|
var configuredSourceRE = regexp.MustCompile(`^narratio\.artifact\.([a-z][a-z0-9_]*)$`)
|
|
var previousSourceRE = regexp.MustCompile(`^narratio\.previous_session\.artifact\.([a-z][a-z0-9_]*)$`)
|
|
|
|
var (
|
|
ErrUnsupportedScriptoriumInputSource = errors.New("unsupported scriptorium input source")
|
|
ErrInvalidPreviousSessionSource = errors.New("invalid previous-session source format")
|
|
)
|
|
|
|
type SourceKind string
|
|
|
|
const (
|
|
SourceKindBuiltIn SourceKind = "built_in"
|
|
SourceKindConfiguredArtifact SourceKind = "configured_artifact"
|
|
SourceKindPreviousArtifact SourceKind = "previous_session_configured_artifact"
|
|
SourceKindStableInput SourceKind = "stable_input"
|
|
)
|
|
|
|
// Source describes one normalized artifact source identifier.
|
|
type Source struct {
|
|
ID string
|
|
Kind SourceKind
|
|
ConfiguredKey string
|
|
}
|
|
|
|
// ScriptoriumInputSourceDescriptor describes one validated Scriptorium input source.
|
|
type ScriptoriumInputSourceDescriptor struct {
|
|
Source Source
|
|
PreviousSession *PreviousSessionSourceDescriptor
|
|
}
|
|
|
|
// PreviousSessionSourceDescriptor describes one canonical previous-session input source.
|
|
type PreviousSessionSourceDescriptor struct {
|
|
SourceID string
|
|
ConfiguredKey string
|
|
ConfiguredSourceID string
|
|
}
|
|
|
|
// UnknownConfiguredArtifactError reports a source that references an undefined configured artifact key.
|
|
type UnknownConfiguredArtifactError struct {
|
|
ConfiguredKey string
|
|
}
|
|
|
|
func (e *UnknownConfiguredArtifactError) Error() string {
|
|
return fmt.Sprintf("references unknown artifact %q", e.ConfiguredKey)
|
|
}
|
|
|
|
// ConfiguredSourceID converts a configured artifact key into source id form.
|
|
func ConfiguredSourceID(key string) string {
|
|
return configuredSourcePrefix + strings.TrimSpace(key)
|
|
}
|
|
|
|
// PreviousSessionSourceID converts a configured artifact key into previous-session source id form.
|
|
func PreviousSessionSourceID(key string) string {
|
|
return previousConfiguredSrcPrefix + strings.TrimSpace(key)
|
|
}
|
|
|
|
// ParseConfiguredSource extracts configured key from narratio.artifact.<key>.
|
|
func ParseConfiguredSource(source string) (string, bool) {
|
|
matches := configuredSourceRE.FindStringSubmatch(strings.TrimSpace(source))
|
|
if len(matches) != 2 {
|
|
return "", false
|
|
}
|
|
return matches[1], true
|
|
}
|
|
|
|
// ParsePreviousSessionSource extracts configured key from narratio.previous_session.artifact.<key>.
|
|
func ParsePreviousSessionSource(source string) (string, bool) {
|
|
matches := previousSourceRE.FindStringSubmatch(strings.TrimSpace(source))
|
|
if len(matches) != 2 {
|
|
return "", false
|
|
}
|
|
return matches[1], true
|
|
}
|
|
|
|
// ClassifySource classifies a source id as built-in, configured, or previous-session configured.
|
|
func ClassifySource(source string) (Source, error) {
|
|
trimmed := strings.TrimSpace(source)
|
|
if trimmed == "" {
|
|
return Source{}, fmt.Errorf("artifact source is required")
|
|
}
|
|
if _, ok := artifactmodel.LookupRuntimeTranscriptArtifact(trimmed); ok {
|
|
return Source{ID: trimmed, Kind: SourceKindBuiltIn}, nil
|
|
}
|
|
if trimmed == SourceBoundsSession {
|
|
return Source{ID: trimmed, Kind: SourceKindBuiltIn}, nil
|
|
}
|
|
if key, ok := ParseConfiguredSource(trimmed); ok {
|
|
return Source{ID: trimmed, Kind: SourceKindConfiguredArtifact, ConfiguredKey: key}, nil
|
|
}
|
|
if key, ok := ParsePreviousSessionSource(trimmed); ok {
|
|
return Source{ID: trimmed, Kind: SourceKindPreviousArtifact, ConfiguredKey: key}, nil
|
|
}
|
|
return Source{}, fmt.Errorf("unsupported artifact source %q", source)
|
|
}
|
|
|
|
// DescribeScriptoriumInputSource classifies one input source and returns descriptor
|
|
// metadata used by config validation, analyze input resolution, and previous-cache planning.
|
|
func DescribeScriptoriumInputSource(source string) (ScriptoriumInputSourceDescriptor, error) {
|
|
trimmed := strings.TrimSpace(source)
|
|
if trimmed == "" {
|
|
return ScriptoriumInputSourceDescriptor{}, ErrUnsupportedScriptoriumInputSource
|
|
}
|
|
if IsStableInputSource(trimmed) {
|
|
return ScriptoriumInputSourceDescriptor{
|
|
Source: Source{ID: trimmed, Kind: SourceKindStableInput},
|
|
}, nil
|
|
}
|
|
if strings.HasPrefix(trimmed, "narratio.previous_session.artifact") {
|
|
descriptor, err := DescribePreviousSessionSource(trimmed)
|
|
if err != nil {
|
|
return ScriptoriumInputSourceDescriptor{}, err
|
|
}
|
|
return ScriptoriumInputSourceDescriptor{
|
|
Source: Source{
|
|
ID: descriptor.SourceID,
|
|
Kind: SourceKindPreviousArtifact,
|
|
ConfiguredKey: descriptor.ConfiguredKey,
|
|
},
|
|
PreviousSession: &descriptor,
|
|
}, nil
|
|
}
|
|
|
|
classified, err := ClassifySource(trimmed)
|
|
if err != nil {
|
|
return ScriptoriumInputSourceDescriptor{}, ErrUnsupportedScriptoriumInputSource
|
|
}
|
|
return ScriptoriumInputSourceDescriptor{Source: classified}, nil
|
|
}
|
|
|
|
// IsStableInputSource reports whether source is a prepared stable input source
|
|
// available only to Scriptorium input resolution.
|
|
func IsStableInputSource(source string) bool {
|
|
switch strings.TrimSpace(source) {
|
|
case SourceInputPlayers, SourceInputParty, SourceInputGlossary:
|
|
return true
|
|
default:
|
|
return false
|
|
}
|
|
}
|
|
|
|
// DescribePreviousSessionSource validates a canonical previous-session source id
|
|
// and returns both previous and configured-source vocabulary descriptors.
|
|
func DescribePreviousSessionSource(source string) (PreviousSessionSourceDescriptor, error) {
|
|
configuredKey, ok := ParsePreviousSessionSource(source)
|
|
if !ok {
|
|
return PreviousSessionSourceDescriptor{}, ErrInvalidPreviousSessionSource
|
|
}
|
|
return PreviousSessionSourceDescriptor{
|
|
SourceID: PreviousSessionSourceID(configuredKey),
|
|
ConfiguredKey: configuredKey,
|
|
ConfiguredSourceID: ConfiguredSourceID(configuredKey),
|
|
}, nil
|
|
}
|
|
|
|
// PreviousSessionSourceDescriptorForConfiguredKey derives a previous-session source descriptor
|
|
// from a configured artifact key.
|
|
func PreviousSessionSourceDescriptorForConfiguredKey(configuredKey string) (PreviousSessionSourceDescriptor, error) {
|
|
return DescribePreviousSessionSource(PreviousSessionSourceID(configuredKey))
|
|
}
|
|
|
|
// ValidateInputConfiguredReference checks that configured/previous-session sources
|
|
// reference configured artifacts known to the current Scriptorium config.
|
|
func ValidateInputConfiguredReference(
|
|
descriptor ScriptoriumInputSourceDescriptor,
|
|
configured map[string]struct{},
|
|
) error {
|
|
switch descriptor.Source.Kind {
|
|
case SourceKindConfiguredArtifact, SourceKindPreviousArtifact:
|
|
if _, ok := configured[descriptor.Source.ConfiguredKey]; !ok {
|
|
return &UnknownConfiguredArtifactError{ConfiguredKey: descriptor.Source.ConfiguredKey}
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// ValidatePublishSource validates that a source is publish-compatible and references a known configured artifact.
|
|
func ValidatePublishSource(source string, configured map[string]string) (Source, error) {
|
|
classified, err := ClassifySource(source)
|
|
if err != nil {
|
|
return Source{}, fmt.Errorf("must be a built-in source id or narratio.artifact.<name>")
|
|
}
|
|
if classified.Kind == SourceKindPreviousArtifact {
|
|
return Source{}, fmt.Errorf("must be a built-in source id or narratio.artifact.<name>")
|
|
}
|
|
if classified.Kind == SourceKindConfiguredArtifact {
|
|
if configured == nil {
|
|
return Source{}, fmt.Errorf("configured artifact %q is not defined in pipeline.scriptorium.artifacts", classified.ConfiguredKey)
|
|
}
|
|
if _, ok := configured[classified.ConfiguredKey]; !ok {
|
|
return Source{}, fmt.Errorf("configured artifact %q is not defined in pipeline.scriptorium.artifacts", classified.ConfiguredKey)
|
|
}
|
|
}
|
|
return classified, nil
|
|
}
|
|
|
|
// DeriveDefaultPublishedDestination returns the default publish destination for one source.
|
|
func DeriveDefaultPublishedDestination(source Source, configured map[string]string) (string, error) {
|
|
switch source.Kind {
|
|
case SourceKindBuiltIn:
|
|
if spec, ok := artifactmodel.LookupRuntimeTranscriptArtifact(source.ID); ok {
|
|
return pathsafe.NormalizeRelativeDestination(spec.CanonicalRelPath)
|
|
}
|
|
if source.ID == SourceBoundsSession {
|
|
return pathsafe.NormalizeRelativeDestination("artifacts/session_bounds.json")
|
|
}
|
|
return "", fmt.Errorf("unsupported built-in source %q", source.ID)
|
|
case SourceKindConfiguredArtifact:
|
|
if configured == nil {
|
|
return "", fmt.Errorf("configured artifact %q is not defined in pipeline.scriptorium.artifacts", source.ConfiguredKey)
|
|
}
|
|
outputPath, ok := configured[source.ConfiguredKey]
|
|
if !ok {
|
|
return "", fmt.Errorf("configured artifact %q is not defined in pipeline.scriptorium.artifacts", source.ConfiguredKey)
|
|
}
|
|
if strings.TrimSpace(outputPath) == "" {
|
|
return "", fmt.Errorf("pipeline.scriptorium.artifacts.%s.output_path is empty", source.ConfiguredKey)
|
|
}
|
|
return pathsafe.NormalizeRelativeDestination(outputPath)
|
|
default:
|
|
return "", fmt.Errorf("publish destination cannot be derived from source %q", source.ID)
|
|
}
|
|
}
|
|
|
|
// ResolvePublishedDestination validates and normalizes an explicit destination,
|
|
// or derives one when omitted.
|
|
func ResolvePublishedDestination(sourceID, explicitDest string, configured map[string]string) (string, error) {
|
|
source, err := ValidatePublishSource(sourceID, configured)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
if strings.TrimSpace(explicitDest) != "" {
|
|
return pathsafe.NormalizeRelativeDestination(explicitDest)
|
|
}
|
|
return DeriveDefaultPublishedDestination(source, configured)
|
|
}
|