From d809085406940536cf120f8c4cc29aeedee0151e Mon Sep 17 00:00:00 2001 From: Eric Rakestraw Date: Sun, 13 Sep 2026 16:51:46 +0000 Subject: [PATCH] Prepare ConfigKit validation and listing commands --- docs/roadmap/implementation.md | 2 + internal/cli/configkit_commands.go | 100 ++++++++++++++++++++++++ internal/cli/configkit_commands_test.go | 35 +++++++++ 3 files changed, 137 insertions(+) create mode 100644 internal/cli/configkit_commands.go create mode 100644 internal/cli/configkit_commands_test.go diff --git a/docs/roadmap/implementation.md b/docs/roadmap/implementation.md index 00bfea30..1dff1ed8 100644 --- a/docs/roadmap/implementation.md +++ b/docs/roadmap/implementation.md @@ -451,6 +451,8 @@ multi-error reporting, and profile selection. Use the real ConfigKit boundary. ## Stage 9 — Prepare Validation And Pipeline Listing Commands +**Status:** Completed 2026-09-13. + **Goal:** Finish the read-only command implementations before the shared production cutover. diff --git a/internal/cli/configkit_commands.go b/internal/cli/configkit_commands.go new file mode 100644 index 00000000..fe85f827 --- /dev/null +++ b/internal/cli/configkit_commands.go @@ -0,0 +1,100 @@ +package cli + +import ( + "context" + "encoding/json" + "flag" + "fmt" + "io" + "strings" + + "gitea.maximumdirect.net/eric/notarius/internal/core/config" +) + +func runConfigKitValidate(args []string, stdout, stderr io.Writer, opts Options) int { + fs := flag.NewFlagSet("config validate", flag.ContinueOnError) + fs.SetOutput(io.Discard) + configPath := fs.String("config", "", "config file path") + profile := fs.String("config-profile", "", "configuration profile") + pipelineID := fs.String("pipeline", "", "pipeline ID") + onlyRaw := fs.String("only", "", "comma-separated artifact lanes") + if err := fs.Parse(args); err != nil { + fmt.Fprintf(stderr, "notarius: %v\n", err) + return 2 + } + if fs.NArg() != 0 { + fmt.Fprintf(stderr, "notarius: unexpected argument %q\n", fs.Arg(0)) + return 2 + } + if strings.TrimSpace(*onlyRaw) != "" && strings.TrimSpace(*pipelineID) == "" { + fmt.Fprintln(stderr, "notarius: --only requires --pipeline") + return 2 + } + only, err := parseOnly(*onlyRaw) + if err != nil { + fmt.Fprintf(stderr, "notarius: %v\n", err) + return 2 + } + loaded, path, err := loadConfigKit(*configPath, *profile, nil, opts) + if err != nil { + writeConfigLoadError(stderr, err) + return 1 + } + if strings.TrimSpace(*pipelineID) == "" { + fmt.Fprintf(stdout, "config %q is valid\n", path) + return 0 + } + catalog, err := effectiveCatalog(opts) + if err != nil { + fmt.Fprintf(stderr, "notarius: %v\n", err) + return 1 + } + effective, err := loaded.Config.Resolve(config.ResolveInput{PipelineID: *pipelineID, Only: only, Catalog: catalog}) + if err != nil { + fmt.Fprintf(stderr, "notarius: %v\n", err) + return 1 + } + if err := validateExplicitPromptKitProfiles(context.Background(), effective.Config, effectiveLLMProfileIDs(effective.ResolvedPipeline), opts.promptKitAssets); err != nil { + fmt.Fprintf(stderr, "notarius: %v\n", err) + return 1 + } + fmt.Fprintf(stdout, "config %q is valid for pipeline %q\n", path, strings.TrimSpace(*pipelineID)) + return 0 +} + +func runConfigKitPipelinesList(args []string, stdout, stderr io.Writer, opts Options) int { + fs := flag.NewFlagSet("pipelines list", flag.ContinueOnError) + fs.SetOutput(io.Discard) + configPath := fs.String("config", "", "config file path") + profile := fs.String("config-profile", "", "configuration profile") + jsonOutput := fs.Bool("json", false, "write JSON output") + if err := fs.Parse(args); err != nil { + fmt.Fprintf(stderr, "notarius: %v\n", err) + return 2 + } + if fs.NArg() != 0 { + fmt.Fprintf(stderr, "notarius: unexpected argument %q\n", fs.Arg(0)) + return 2 + } + loaded, _, err := loadConfigKit(*configPath, *profile, nil, opts) + if err != nil { + writeConfigLoadError(stderr, err) + return 1 + } + ids := sortedPipelineIDs(loaded.Config) + if *jsonOutput { + payload, err := json.Marshal(struct { + Pipelines []string `json:"pipelines"` + }{Pipelines: ids}) + if err != nil { + fmt.Fprintf(stderr, "notarius: marshal pipeline list: %v\n", err) + return 1 + } + fmt.Fprintf(stdout, "%s\n", payload) + return 0 + } + for _, id := range ids { + fmt.Fprintln(stdout, id) + } + return 0 +} diff --git a/internal/cli/configkit_commands_test.go b/internal/cli/configkit_commands_test.go new file mode 100644 index 00000000..fd22acd3 --- /dev/null +++ b/internal/cli/configkit_commands_test.go @@ -0,0 +1,35 @@ +package cli + +import ( + "bytes" + "path/filepath" + "strings" + "testing" +) + +func TestConfigKitReadOnlyCommandsLoadImportsAndProfiles(t *testing.T) { + t.Parallel() + root := t.TempDir() + path := filepath.Join(root, "notarius.yml") + writeConfigKitFile(t, filepath.Join(root, "fragment.yml"), "pipelines:\n imported:\n input: {module: input}\n") + writeConfigKitFile(t, path, "version: 5\nconfigkit:\n imports:\n - path: fragment.yml\npipelines:\n base:\n input: {module: input}\n") + var stdout, stderr bytes.Buffer + if code := runConfigKitPipelinesList([]string{"--config", path}, &stdout, &stderr, commandContractOptions(t)); code != 0 || stdout.String() != "base\nimported\n" { + t.Fatalf("list = %d, %q, %q", code, stdout.String(), stderr.String()) + } + stdout.Reset() + stderr.Reset() + if code := runConfigKitValidate([]string{"--config", path}, &stdout, &stderr, commandContractOptions(t)); code != 0 || !strings.Contains(stdout.String(), "is valid") { + t.Fatalf("validate = %d, %q, %q", code, stdout.String(), stderr.String()) + } +} + +func TestConfigKitReadOnlyCommandsRejectVersionFour(t *testing.T) { + t.Parallel() + path := filepath.Join(t.TempDir(), "notarius.yml") + writeConfigKitFile(t, path, "version: 4\n") + var stdout, stderr bytes.Buffer + if code := runConfigKitPipelinesList([]string{"--config", path}, &stdout, &stderr, commandContractOptions(t)); code != 1 || !strings.Contains(stderr.String(), "version") { + t.Fatalf("list = %d, %q", code, stderr.String()) + } +}