From dbe7c69378af3b6375d6281afa019a3ff90b8dca Mon Sep 17 00:00:00 2001 From: Virgil Date: Wed, 1 Apr 2026 23:59:13 +0000 Subject: [PATCH] refactor(build): centralise workflow output alias resolution Co-Authored-By: Virgil --- cmd/build/cmd_workflow.go | 34 +-------------------------- cmd/build/cmd_workflow_test.go | 10 ++++---- pkg/api/provider.go | 30 +----------------------- pkg/build/workflow.go | 43 ++++++++++++++++++++++++++++++++++ pkg/build/workflow_test.go | 39 ++++++++++++++++++++++++++++++ 5 files changed, 89 insertions(+), 67 deletions(-) diff --git a/cmd/build/cmd_workflow.go b/cmd/build/cmd_workflow.go index 4c1ef30..6a28fd1 100644 --- a/cmd/build/cmd_workflow.go +++ b/cmd/build/cmd_workflow.go @@ -4,7 +4,6 @@ package buildcmd import ( "context" - "strings" "dappco.re/go/core/build/internal/ax" "dappco.re/go/core/build/pkg/build" @@ -63,7 +62,7 @@ func AddWorkflowCommand(buildCmd *cli.Command) { // runReleaseWorkflow(ctx, "", "", "ci/release.yml", "") // uses the snake_case alias // runReleaseWorkflow(ctx, "", "", "", "ci/release.yml") // uses the legacy output alias func runReleaseWorkflow(_ context.Context, workflowPathInput, workflowOutputPathInput, workflowOutputPathSnakeInput, workflowOutputLegacyInput string) error { - resolvedOutputPathInput, err := resolveReleaseWorkflowOutputPathInput( + resolvedOutputPathInput, err := build.ResolveReleaseWorkflowOutputPath( workflowOutputPathInput, workflowOutputPathSnakeInput, workflowOutputLegacyInput, @@ -80,37 +79,6 @@ func runReleaseWorkflow(_ context.Context, workflowPathInput, workflowOutputPath return runReleaseWorkflowInDir(projectDir, workflowPathInput, resolvedOutputPathInput) } -// resolveReleaseWorkflowOutputPathInput chooses the workflow output path alias -// with deterministic precedence. -// -// resolveReleaseWorkflowOutputPathInput("ci/release.yml", "", "") // "ci/release.yml" -// resolveReleaseWorkflowOutputPathInput("", "ci/release.yml", "") // "ci/release.yml" -// resolveReleaseWorkflowOutputPathInput("", "", "ci/release.yml") // "ci/release.yml" -// resolveReleaseWorkflowOutputPathInput("ci/release.yml", "ops/release.yml", "") // error -func resolveReleaseWorkflowOutputPathInput(outputPathInput, outputPathSnakeInput, outputLegacyInput string) (string, error) { - values := []string{ - strings.TrimSpace(outputPathInput), - strings.TrimSpace(outputPathSnakeInput), - strings.TrimSpace(outputLegacyInput), - } - - var resolved string - for _, value := range values { - if value == "" { - continue - } - if resolved == "" { - resolved = value - continue - } - if resolved != value { - return "", coreerr.E("build.resolveReleaseWorkflowOutputPathInput", "output aliases specify different locations", nil) - } - } - - return resolved, nil -} - // runReleaseWorkflowInDir writes the embedded release workflow into projectDir. // // runReleaseWorkflowInDir("/tmp/project", "", "") // /tmp/project/.github/workflows/release.yml diff --git a/cmd/build/cmd_workflow_test.go b/cmd/build/cmd_workflow_test.go index 1bbc31e..58d863f 100644 --- a/cmd/build/cmd_workflow_test.go +++ b/cmd/build/cmd_workflow_test.go @@ -14,32 +14,32 @@ import ( func TestBuildCmd_resolveReleaseWorkflowOutputPathInput_Good(t *testing.T) { t.Run("accepts the preferred output path", func(t *testing.T) { - path, err := resolveReleaseWorkflowOutputPathInput("ci/release.yml", "", "") + path, err := build.ResolveReleaseWorkflowOutputPath("ci/release.yml", "", "") require.NoError(t, err) assert.Equal(t, "ci/release.yml", path) }) t.Run("accepts the snake_case output path alias", func(t *testing.T) { - path, err := resolveReleaseWorkflowOutputPathInput("", "ci/release.yml", "") + path, err := build.ResolveReleaseWorkflowOutputPath("", "ci/release.yml", "") require.NoError(t, err) assert.Equal(t, "ci/release.yml", path) }) t.Run("accepts the legacy output alias", func(t *testing.T) { - path, err := resolveReleaseWorkflowOutputPathInput("", "", "ci/release.yml") + path, err := build.ResolveReleaseWorkflowOutputPath("", "", "ci/release.yml") require.NoError(t, err) assert.Equal(t, "ci/release.yml", path) }) t.Run("accepts matching output aliases", func(t *testing.T) { - path, err := resolveReleaseWorkflowOutputPathInput("ci/release.yml", "ci/release.yml", "ci/release.yml") + path, err := build.ResolveReleaseWorkflowOutputPath("ci/release.yml", "ci/release.yml", "ci/release.yml") require.NoError(t, err) assert.Equal(t, "ci/release.yml", path) }) } func TestBuildCmd_resolveReleaseWorkflowOutputPathInput_Bad(t *testing.T) { - _, err := resolveReleaseWorkflowOutputPathInput("ci/release.yml", "ops/release.yml", "") + _, err := build.ResolveReleaseWorkflowOutputPath("ci/release.yml", "ops/release.yml", "") require.Error(t, err) assert.Contains(t, err.Error(), "output aliases specify different locations") } diff --git a/pkg/api/provider.go b/pkg/api/provider.go index 2773648..0c497d3 100644 --- a/pkg/api/provider.go +++ b/pkg/api/provider.go @@ -10,7 +10,6 @@ import ( stdio "io" "io/fs" "net/http" - "strings" "dappco.re/go/core/api" "dappco.re/go/core/api/pkg/provider" @@ -21,7 +20,6 @@ import ( "dappco.re/go/core/build/pkg/release" "dappco.re/go/core/build/pkg/sdk" "dappco.re/go/core/io" - coreerr "dappco.re/go/core/log" "dappco.re/go/core/ws" "github.com/gin-gonic/gin" ) @@ -556,33 +554,7 @@ type ReleaseWorkflowRequest struct { // resolvedOutputPath resolves the workflow output aliases with the same // conflict rules as the CLI. func (r ReleaseWorkflowRequest) resolvedOutputPath() (string, error) { - return resolveOutputPathInput(r.OutputPath, r.OutputPathSnake, r.LegacyOutputPath) -} - -// resolveOutputPathInput chooses the workflow output path alias with -// deterministic precedence. -func resolveOutputPathInput(outputPathInput, outputPathSnakeInput, outputLegacyInput string) (string, error) { - values := []string{ - strings.TrimSpace(outputPathInput), - strings.TrimSpace(outputPathSnakeInput), - strings.TrimSpace(outputLegacyInput), - } - - var resolved string - for _, value := range values { - if value == "" { - continue - } - if resolved == "" { - resolved = value - continue - } - if resolved != value { - return "", coreerr.E("api.resolveOutputPathInput", "output aliases specify different locations", nil) - } - } - - return resolved, nil + return build.ResolveReleaseWorkflowOutputPath(r.OutputPath, r.OutputPathSnake, r.LegacyOutputPath) } func (p *BuildProvider) generateReleaseWorkflow(c *gin.Context) { diff --git a/pkg/build/workflow.go b/pkg/build/workflow.go index 68fb4fb..a4e216d 100644 --- a/pkg/build/workflow.go +++ b/pkg/build/workflow.go @@ -130,6 +130,22 @@ func ResolveReleaseWorkflowInputPathWithMedium(filesystem io_interface.Medium, p ) } +// ResolveReleaseWorkflowOutputPath resolves the workflow output aliases used by +// the CLI, API, and UI layers. +// +// build.ResolveReleaseWorkflowOutputPath("ci/release.yml", "", "") // "ci/release.yml" +// build.ResolveReleaseWorkflowOutputPath("", "ci/release.yml", "") // "ci/release.yml" +// build.ResolveReleaseWorkflowOutputPath("", "", "ci/release.yml") // "ci/release.yml" +// build.ResolveReleaseWorkflowOutputPath("ci/release.yml", "ops.yml", "") // error +func ResolveReleaseWorkflowOutputPath(outputPathInput, outputPathSnakeInput, outputLegacyInput string) (string, error) { + return resolveReleaseWorkflowPathPair( + outputPathInput, + outputPathSnakeInput, + outputLegacyInput, + "build.ResolveReleaseWorkflowOutputPath", + ) +} + // resolveReleaseWorkflowInputPathPair resolves the workflow path from the path // and output aliases, rejecting conflicting values and preferring explicit // inputs over the default. @@ -157,6 +173,33 @@ func resolveReleaseWorkflowInputPathPair(pathInput, outputPathInput string, reso return resolve(""), nil } +// resolveReleaseWorkflowPathPair resolves any trio of workflow path aliases by +// trimming whitespace, rejecting conflicts, and returning the first non-empty +// value when aliases agree. +func resolveReleaseWorkflowPathPair(primaryInput, secondaryInput, tertiaryInput, errorName string) (string, error) { + values := []string{ + cleanWorkflowInput(primaryInput), + cleanWorkflowInput(secondaryInput), + cleanWorkflowInput(tertiaryInput), + } + + var resolved string + for _, value := range values { + if value == "" { + continue + } + if resolved == "" { + resolved = value + continue + } + if resolved != value { + return "", coreerr.E(errorName, "output aliases specify different locations", nil) + } + } + + return resolved, nil +} + // resolveReleaseWorkflowInputPath resolves one workflow input into a file path. // // resolveReleaseWorkflowInputPath("/tmp/project", "ci", io.Local) // /tmp/project/ci/release.yml diff --git a/pkg/build/workflow_test.go b/pkg/build/workflow_test.go index 6c4d053..566b21a 100644 --- a/pkg/build/workflow_test.go +++ b/pkg/build/workflow_test.go @@ -319,3 +319,42 @@ func TestWorkflow_ResolveReleaseWorkflowInputPathWithMedium_Good(t *testing.T) { assert.Equal(t, "/tmp/project/ci/release.yml", path) }) } + +func TestWorkflow_ResolveReleaseWorkflowOutputPath_Good(t *testing.T) { + t.Run("accepts the preferred output path", func(t *testing.T) { + path, err := ResolveReleaseWorkflowOutputPath("ci/release.yml", "", "") + require.NoError(t, err) + assert.Equal(t, "ci/release.yml", path) + }) + + t.Run("accepts the snake_case output path alias", func(t *testing.T) { + path, err := ResolveReleaseWorkflowOutputPath("", "ci/release.yml", "") + require.NoError(t, err) + assert.Equal(t, "ci/release.yml", path) + }) + + t.Run("accepts the legacy output alias", func(t *testing.T) { + path, err := ResolveReleaseWorkflowOutputPath("", "", "ci/release.yml") + require.NoError(t, err) + assert.Equal(t, "ci/release.yml", path) + }) + + t.Run("trims surrounding whitespace from aliases", func(t *testing.T) { + path, err := ResolveReleaseWorkflowOutputPath(" ci/release.yml ", " ", " ") + require.NoError(t, err) + assert.Equal(t, "ci/release.yml", path) + }) + + t.Run("accepts matching aliases", func(t *testing.T) { + path, err := ResolveReleaseWorkflowOutputPath("ci/release.yml", "ci/release.yml", "ci/release.yml") + require.NoError(t, err) + assert.Equal(t, "ci/release.yml", path) + }) +} + +func TestWorkflow_ResolveReleaseWorkflowOutputPath_Bad(t *testing.T) { + path, err := ResolveReleaseWorkflowOutputPath("ci/release.yml", "ops/release.yml", "") + assert.Error(t, err) + assert.Empty(t, path) + assert.Contains(t, err.Error(), "output aliases specify different locations") +}