refactor(build): centralise workflow output alias resolution

Co-Authored-By: Virgil <virgil@lethean.io>
This commit is contained in:
Virgil 2026-04-01 23:59:13 +00:00
parent 83b5f12ce1
commit dbe7c69378
5 changed files with 89 additions and 67 deletions

View file

@ -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

View file

@ -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")
}

View file

@ -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) {

View file

@ -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

View file

@ -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")
}