From 63285421feab74726089417c57519a1e6ec2e9d6 Mon Sep 17 00:00:00 2001 From: Cory O'Daniel Date: Fri, 6 Feb 2026 16:23:03 -0800 Subject: [PATCH] adding exp artdef massdriver.yaml support --- pkg/definition/build.go | 128 ++++++++++++++ pkg/definition/build_test.go | 158 ++++++++++++++++++ pkg/definition/publish_test.go | 12 +- pkg/definition/read.go | 23 +++ .../exports/config.yaml.liquid | 6 + .../instructions/cli.md | 7 + .../instructions/console.md | 7 + .../massdriver-yaml-artifact/massdriver.yaml | 41 +++++ .../massdriver-yaml-simple/massdriver.yaml | 19 +++ 9 files changed, 400 insertions(+), 1 deletion(-) create mode 100644 pkg/definition/build.go create mode 100644 pkg/definition/build_test.go create mode 100644 pkg/definition/testdata/massdriver-yaml-artifact/exports/config.yaml.liquid create mode 100644 pkg/definition/testdata/massdriver-yaml-artifact/instructions/cli.md create mode 100644 pkg/definition/testdata/massdriver-yaml-artifact/instructions/console.md create mode 100644 pkg/definition/testdata/massdriver-yaml-artifact/massdriver.yaml create mode 100644 pkg/definition/testdata/massdriver-yaml-simple/massdriver.yaml diff --git a/pkg/definition/build.go b/pkg/definition/build.go new file mode 100644 index 0000000..658ba33 --- /dev/null +++ b/pkg/definition/build.go @@ -0,0 +1,128 @@ +package definition + +import ( + "fmt" + "os" + "path/filepath" + + "gopkg.in/yaml.v3" +) + +// MassdriverYAML represents the structure of a massdriver.yaml artifact definition file. +// This is an experimental format that provides a more ergonomic authoring experience. +type MassdriverYAML struct { + Name string `yaml:"name"` + Label string `yaml:"label"` + Icon string `yaml:"icon"` + UI *UIConfig `yaml:"ui"` + Exports []ExportConfig `yaml:"exports"` + Schema map[string]any `yaml:"schema"` +} + +// UIConfig represents the UI configuration section +type UIConfig struct { + ConnectionOrientation string `yaml:"connectionOrientation"` + EnvironmentDefaultGroup string `yaml:"environmentDefaultGroup"` + Instructions []InstructionConfig `yaml:"instructions"` +} + +// InstructionConfig represents an instruction file reference +type InstructionConfig struct { + Label string `yaml:"label"` + Path string `yaml:"path"` +} + +// ExportConfig represents an export template configuration +type ExportConfig struct { + DownloadButtonText string `yaml:"downloadButtonText"` + FileFormat string `yaml:"fileFormat"` + TemplatePath string `yaml:"templatePath"` + TemplateLang string `yaml:"templateLang"` +} + +// Build reads a massdriver.yaml file and builds it into the artifact definition +// format expected by the Massdriver API. +func Build(path string) (map[string]any, error) { + content, err := os.ReadFile(path) + if err != nil { + return nil, fmt.Errorf("failed to read massdriver.yaml: %w", err) + } + + var config MassdriverYAML + if err := yaml.Unmarshal(content, &config); err != nil { + return nil, fmt.Errorf("failed to parse massdriver.yaml: %w", err) + } + + baseDir := filepath.Dir(path) + + // Build the $md block + mdBlock := map[string]any{ + "name": config.Name, + "label": config.Label, + "icon": config.Icon, + } + + // Process UI configuration + if config.UI != nil { + uiBlock := map[string]any{} + + if config.UI.ConnectionOrientation != "" { + uiBlock["connectionOrientation"] = config.UI.ConnectionOrientation + } + if config.UI.EnvironmentDefaultGroup != "" { + uiBlock["environmentDefaultGroup"] = config.UI.EnvironmentDefaultGroup + } + + // Process instructions + instructions := []map[string]any{} + for _, instruction := range config.UI.Instructions { + instructionPath := filepath.Join(baseDir, instruction.Path) + instructionContent, err := os.ReadFile(instructionPath) + if err != nil { + return nil, fmt.Errorf("failed to read instruction file %s: %w", instruction.Path, err) + } + instructions = append(instructions, map[string]any{ + "label": instruction.Label, + "content": string(instructionContent), + }) + } + uiBlock["instructions"] = instructions + + mdBlock["ui"] = uiBlock + } + + // Process exports + exports := []map[string]any{} + for _, export := range config.Exports { + templatePath := filepath.Join(baseDir, export.TemplatePath) + templateContent, err := os.ReadFile(templatePath) + if err != nil { + return nil, fmt.Errorf("failed to read export template %s: %w", export.TemplatePath, err) + } + exports = append(exports, map[string]any{ + "downloadButtonText": export.DownloadButtonText, + "fileFormat": export.FileFormat, + "template": string(templateContent), + "templateLang": export.TemplateLang, + }) + } + mdBlock["export"] = exports + + // Build the final structure: merge $md with schema + result := map[string]any{ + "$md": mdBlock, + } + + // Merge schema into result + for key, value := range config.Schema { + result[key] = value + } + + return result, nil +} + +// IsMassdriverYAMLArtifactDefinition checks if the given path is a massdriver.yaml +// file that should be treated as an artifact definition in the experimental format. +func IsMassdriverYAMLArtifactDefinition(path string) bool { + return filepath.Base(path) == "massdriver.yaml" +} diff --git a/pkg/definition/build_test.go b/pkg/definition/build_test.go new file mode 100644 index 0000000..197fb97 --- /dev/null +++ b/pkg/definition/build_test.go @@ -0,0 +1,158 @@ +package definition_test + +import ( + "path/filepath" + "testing" + + "github.com/massdriver-cloud/mass/pkg/definition" +) + +func TestIsMassdriverYAMLArtifactDefinition(t *testing.T) { + tests := []struct { + name string + path string + want bool + }{ + { + name: "massdriver.yaml file", + path: "some/path/massdriver.yaml", + want: true, + }, + { + name: "massdriver.yaml in root", + path: "massdriver.yaml", + want: true, + }, + { + name: "json artifact definition", + path: "some/path/artifact.json", + want: false, + }, + { + name: "other yaml file", + path: "some/path/artifact.yaml", + want: false, + }, + { + name: "yml file", + path: "some/path/artifact.yml", + want: false, + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + got := definition.IsMassdriverYAMLArtifactDefinition(tc.path) + if got != tc.want { + t.Errorf("IsMassdriverYAMLArtifactDefinition(%q) = %v, want %v", tc.path, got, tc.want) + } + }) + } +} + +func TestBuild(t *testing.T) { + tests := []struct { + name string + path string + wantErr bool + check func(t *testing.T, result map[string]any) + }{ + { + name: "builds massdriver.yaml with instructions and exports", + path: filepath.Join("testdata", "massdriver-yaml-artifact", "massdriver.yaml"), + wantErr: false, + check: func(t *testing.T, result map[string]any) { + // Check $md block + md, ok := result["$md"].(map[string]any) + if !ok { + t.Fatal("expected $md to be a map") + } + + if md["name"] != "test-artifact" { + t.Errorf("expected name to be test-artifact, got %v", md["name"]) + } + if md["label"] != "Test Artifact Definition" { + t.Errorf("expected label to be Test Artifact Definition, got %v", md["label"]) + } + if md["icon"] != "https://example.com/icon.png" { + t.Errorf("expected icon to be https://example.com/icon.png, got %v", md["icon"]) + } + + // Check UI block + ui, ok := md["ui"].(map[string]any) + if !ok { + t.Fatal("expected ui to be a map") + } + if ui["connectionOrientation"] != "environmentDefault" { + t.Errorf("expected connectionOrientation to be environmentDefault, got %v", ui["connectionOrientation"]) + } + if ui["environmentDefaultGroup"] != "credentials" { + t.Errorf("expected environmentDefaultGroup to be credentials, got %v", ui["environmentDefaultGroup"]) + } + + // Check instructions + instructions, ok := ui["instructions"].([]map[string]any) + if !ok { + t.Fatal("expected instructions to be a slice of maps") + } + if len(instructions) != 2 { + t.Errorf("expected 2 instructions, got %d", len(instructions)) + } + if instructions[0]["label"] != "CLI Setup" { + t.Errorf("expected first instruction label to be CLI Setup, got %v", instructions[0]["label"]) + } + // Check that content was read + content, ok := instructions[0]["content"].(string) + if !ok || content == "" { + t.Error("expected instruction content to be a non-empty string") + } + + // Check exports + exports, ok := md["export"].([]map[string]any) + if !ok { + t.Fatal("expected export to be a slice of maps") + } + if len(exports) != 1 { + t.Errorf("expected 1 export, got %d", len(exports)) + } + if exports[0]["downloadButtonText"] != "Download Config" { + t.Errorf("expected downloadButtonText to be Download Config, got %v", exports[0]["downloadButtonText"]) + } + if exports[0]["fileFormat"] != "yaml" { + t.Errorf("expected fileFormat to be yaml, got %v", exports[0]["fileFormat"]) + } + if exports[0]["templateLang"] != "liquid" { + t.Errorf("expected templateLang to be liquid, got %v", exports[0]["templateLang"]) + } + // Check that template was read + template, ok := exports[0]["template"].(string) + if !ok || template == "" { + t.Error("expected export template to be a non-empty string") + } + + // Check schema fields are merged at top level + if result["$schema"] != "http://json-schema.org/draft-07/schema" { + t.Errorf("expected $schema at top level, got %v", result["$schema"]) + } + if result["title"] != "Test Artifact" { + t.Errorf("expected title to be Test Artifact, got %v", result["title"]) + } + if result["type"] != "object" { + t.Errorf("expected type to be object, got %v", result["type"]) + } + }, + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + result, err := definition.Build(tc.path) + if (err != nil) != tc.wantErr { + t.Fatalf("Build() error = %v, wantErr %v", err, tc.wantErr) + } + if tc.check != nil { + tc.check(t, result) + } + }) + } +} diff --git a/pkg/definition/publish_test.go b/pkg/definition/publish_test.go index 5fa90a8..8cdc621 100644 --- a/pkg/definition/publish_test.go +++ b/pkg/definition/publish_test.go @@ -22,10 +22,20 @@ func TestPublish(t *testing.T) { } tests := []test{ { - name: "simple", + name: "simple json", path: "testdata/simple-artifact.json", wantBody: `{"$schema":"http://json-schema.org/draft-07/schema","type":"object","title":"Test Artifact","properties":{"data":{"type":"object"}},"specs":{"type":"object"}}}`, }, + { + name: "massdriver.yaml format", + path: "testdata/massdriver-yaml-simple/massdriver.yaml", + wantBody: `{"$schema":"http://json-schema.org/draft-07/schema","type":"object","title":"Test Artifact"}`, + }, + { + name: "massdriver.yaml with instructions and exports", + path: "testdata/massdriver-yaml-artifact/massdriver.yaml", + wantBody: `{"$schema":"http://json-schema.org/draft-07/schema","type":"object","title":"Test Artifact"}`, + }, } for _, tc := range tests { diff --git a/pkg/definition/read.go b/pkg/definition/read.go index 30b9bef..0d6ee3b 100644 --- a/pkg/definition/read.go +++ b/pkg/definition/read.go @@ -12,6 +12,29 @@ import ( ) func Read(ctx context.Context, mdClient *client.Client, path string) (map[string]any, error) { + // Check if this is a massdriver.yaml file (experimental artifact definition format) + if IsMassdriverYAMLArtifactDefinition(path) { + built, buildErr := Build(path) + if buildErr != nil { + return nil, fmt.Errorf("failed to build massdriver.yaml artifact definition: %w", buildErr) + } + + // Dereference the built schema + opts := DereferenceOptions{ + Client: mdClient, + Cwd: filepath.Dir(path), + } + dereferencedAny, derefErr := DereferenceSchema(built, opts) + if derefErr != nil { + return nil, fmt.Errorf("failed to dereference artifact definition: %w", derefErr) + } + dereferenced, ok := dereferencedAny.(map[string]any) + if !ok { + return nil, fmt.Errorf("dereferenced artifact definition is not a map") + } + return dereferenced, nil + } + artdefBytes, readErr := os.ReadFile(path) if readErr != nil { return nil, fmt.Errorf("failed to read artifact definition: %w", readErr) diff --git a/pkg/definition/testdata/massdriver-yaml-artifact/exports/config.yaml.liquid b/pkg/definition/testdata/massdriver-yaml-artifact/exports/config.yaml.liquid new file mode 100644 index 0000000..01f5cf9 --- /dev/null +++ b/pkg/definition/testdata/massdriver-yaml-artifact/exports/config.yaml.liquid @@ -0,0 +1,6 @@ +apiVersion: v1 +kind: Config +metadata: + name: {{ artifact.id }} +credentials: + token: {{ artifact.token }} diff --git a/pkg/definition/testdata/massdriver-yaml-artifact/instructions/cli.md b/pkg/definition/testdata/massdriver-yaml-artifact/instructions/cli.md new file mode 100644 index 0000000..921a673 --- /dev/null +++ b/pkg/definition/testdata/massdriver-yaml-artifact/instructions/cli.md @@ -0,0 +1,7 @@ +# CLI Setup Guide + +Follow these steps to configure the CLI: + +1. Install the CLI tool +2. Run `tool configure` +3. Enter your credentials diff --git a/pkg/definition/testdata/massdriver-yaml-artifact/instructions/console.md b/pkg/definition/testdata/massdriver-yaml-artifact/instructions/console.md new file mode 100644 index 0000000..5785549 --- /dev/null +++ b/pkg/definition/testdata/massdriver-yaml-artifact/instructions/console.md @@ -0,0 +1,7 @@ +# Console Guide + +Access the console at https://console.example.com + +1. Navigate to Settings +2. Click on Credentials +3. Copy your token diff --git a/pkg/definition/testdata/massdriver-yaml-artifact/massdriver.yaml b/pkg/definition/testdata/massdriver-yaml-artifact/massdriver.yaml new file mode 100644 index 0000000..8178ed5 --- /dev/null +++ b/pkg/definition/testdata/massdriver-yaml-artifact/massdriver.yaml @@ -0,0 +1,41 @@ +name: test-artifact +label: Test Artifact Definition +icon: https://example.com/icon.png + +ui: + connectionOrientation: environmentDefault + environmentDefaultGroup: credentials + instructions: + - label: CLI Setup + path: ./instructions/cli.md + - label: Console Guide + path: ./instructions/console.md + +exports: + - downloadButtonText: Download Config + fileFormat: yaml + templatePath: ./exports/config.yaml.liquid + templateLang: liquid + +schema: + $schema: http://json-schema.org/draft-07/schema + title: Test Artifact + description: A test artifact for unit testing + type: object + required: + - id + - token + properties: + id: + title: ID + description: Unique identifier + type: string + token: + $md.sensitive: true + title: Token + description: Authentication token + type: string + data: + type: object + specs: + type: object diff --git a/pkg/definition/testdata/massdriver-yaml-simple/massdriver.yaml b/pkg/definition/testdata/massdriver-yaml-simple/massdriver.yaml new file mode 100644 index 0000000..cfbdfbc --- /dev/null +++ b/pkg/definition/testdata/massdriver-yaml-simple/massdriver.yaml @@ -0,0 +1,19 @@ +name: simple-yaml-artifact +label: Simple YAML Artifact +icon: https://example.com/icon.png + +ui: + environmentDefaultGroup: credentials + instructions: [] + +exports: [] + +schema: + $schema: http://json-schema.org/draft-07/schema + title: Test Artifact + type: object + properties: + data: + type: object + specs: + type: object