feat(build): accept ./ workflow directory inputs

Co-Authored-By: Virgil <virgil@lethean.io>
This commit is contained in:
Virgil 2026-04-01 22:47:16 +00:00
parent 5690b9f15b
commit 7b937eebf9
4 changed files with 68 additions and 2 deletions

View file

@ -90,6 +90,16 @@ func TestBuildCmd_RunReleaseWorkflow_Good(t *testing.T) {
assert.Contains(t, content, "workflow_dispatch:")
})
t.Run("writes release.yml inside a current-directory-prefixed directory-style path", func(t *testing.T) {
err := runReleaseWorkflowInDir(projectDir, "./ci", "")
require.NoError(t, err)
content, err := io.Local.Read(ax.Join(projectDir, "ci", "release.yml"))
require.NoError(t, err)
assert.Contains(t, content, "workflow_call:")
assert.Contains(t, content, "workflow_dispatch:")
})
t.Run("writes to the output alias", func(t *testing.T) {
customPath := "ci/alias-release.yml"
err := runReleaseWorkflowInDir(projectDir, "", customPath)

View file

@ -281,6 +281,30 @@ func TestProvider_GenerateReleaseWorkflow_BareDirectoryPath_Good(t *testing.T) {
assert.Contains(t, content, "workflow_dispatch:")
}
func TestProvider_GenerateReleaseWorkflow_CurrentDirectoryPrefixedPath_Good(t *testing.T) {
gin.SetMode(gin.TestMode)
projectDir := t.TempDir()
p := NewProvider(projectDir, nil)
recorder := httptest.NewRecorder()
request := httptest.NewRequest(http.MethodPost, "/release/workflow", bytes.NewBufferString(`{"path":"./ci"}`))
request.Header.Set("Content-Type", "application/json")
ctx, _ := gin.CreateTestContext(recorder)
ctx.Request = request
p.generateReleaseWorkflow(ctx)
assert.Equal(t, http.StatusOK, recorder.Code)
path := ax.Join(projectDir, "ci", "release.yml")
content, err := io.Local.Read(path)
require.NoError(t, err)
assert.Contains(t, content, "workflow_call:")
assert.Contains(t, content, "workflow_dispatch:")
}
func TestProvider_GenerateReleaseWorkflow_ExistingDirectoryPath_Good(t *testing.T) {
gin.SetMode(gin.TestMode)

View file

@ -27,6 +27,7 @@ const DefaultReleaseWorkflowFileName = "release.yml"
//
// build.WriteReleaseWorkflow(io.Local, "") // writes .github/workflows/release.yml
// build.WriteReleaseWorkflow(io.Local, "ci") // writes ./ci/release.yml under the project root
// build.WriteReleaseWorkflow(io.Local, "./ci") // writes ./ci/release.yml under the project root
// build.WriteReleaseWorkflow(io.Local, "ci/release.yml") // writes ./ci/release.yml under the project root
// build.WriteReleaseWorkflow(io.Local, "/tmp/repo/.github/workflows/release.yml") // writes the absolute path unchanged
func WriteReleaseWorkflow(medium io_interface.Medium, outputPath string) error {
@ -69,6 +70,7 @@ func ReleaseWorkflowPath(projectDir string) string {
// project directory when the caller supplies a relative path.
//
// build.ResolveReleaseWorkflowPath("/tmp/project", "") // /tmp/project/.github/workflows/release.yml
// build.ResolveReleaseWorkflowPath("/tmp/project", "./ci") // /tmp/project/ci/release.yml
// build.ResolveReleaseWorkflowPath("/tmp/project", "ci/release.yml") // /tmp/project/ci/release.yml
// build.ResolveReleaseWorkflowPath("/tmp/project", "ci") // /tmp/project/ci/release.yml
// build.ResolveReleaseWorkflowPath("/tmp/project", "/tmp/release.yml") // /tmp/release.yml
@ -92,6 +94,7 @@ func ResolveReleaseWorkflowPath(projectDir, outputPath string) string {
// `path` field and its `output` alias.
//
// build.ResolveReleaseWorkflowInputPath("/tmp/project", "", "") // /tmp/project/.github/workflows/release.yml
// build.ResolveReleaseWorkflowInputPath("/tmp/project", "./ci", "") // /tmp/project/ci/release.yml
// build.ResolveReleaseWorkflowInputPath("/tmp/project", "ci/release.yml", "") // /tmp/project/ci/release.yml
// build.ResolveReleaseWorkflowInputPath("/tmp/project", "", "ci/release.yml") // /tmp/project/ci/release.yml
// build.ResolveReleaseWorkflowInputPath("/tmp/project", "ci/release.yml", "ci.yml") // error
@ -125,6 +128,7 @@ func ResolveReleaseWorkflowInputPath(projectDir, path, outputPath string) (strin
// trailing slash.
//
// build.ResolveReleaseWorkflowInputPathWithMedium(io.Local, "/tmp/project", "ci", "") // /tmp/project/ci/release.yml when /tmp/project/ci exists
// build.ResolveReleaseWorkflowInputPathWithMedium(io.Local, "/tmp/project", "./ci", "") // /tmp/project/ci/release.yml
func ResolveReleaseWorkflowInputPathWithMedium(medium io_interface.Medium, projectDir, path, outputPath string) (string, error) {
resolve := func(input string) string {
return resolveReleaseWorkflowInputPath(projectDir, input, medium)
@ -189,7 +193,8 @@ func isWorkflowDirectoryPath(path string) bool {
// isWorkflowDirectoryInput reports whether a workflow input should be treated
// as a directory target. This includes explicit directory paths and bare names
// without path separators or a file extension.
// without path separators or a file extension, plus current-directory-prefixed
// directory targets like "./ci".
func isWorkflowDirectoryInput(path string) bool {
if isWorkflowDirectoryPath(path) {
return true
@ -197,5 +202,14 @@ func isWorkflowDirectoryInput(path string) bool {
if path == "" || ax.Ext(path) != "" {
return false
}
return !strings.ContainsAny(path, "/\\")
if !strings.ContainsAny(path, "/\\") {
return true
}
if strings.HasPrefix(path, "./") || strings.HasPrefix(path, ".\\") {
trimmed := strings.TrimPrefix(strings.TrimPrefix(path, "./"), ".\\")
return trimmed != "" && !strings.ContainsAny(trimmed, "/\\")
}
return false
}

View file

@ -124,6 +124,10 @@ func TestWorkflow_ResolveReleaseWorkflowPath_Good(t *testing.T) {
assert.Equal(t, "/tmp/project/ci/release.yml", ResolveReleaseWorkflowPath("/tmp/project", "ci"))
})
t.Run("treats current-directory-prefixed directory names as directories", func(t *testing.T) {
assert.Equal(t, "/tmp/project/ci/release.yml", ResolveReleaseWorkflowPath("/tmp/project", "./ci"))
})
t.Run("keeps nested extensionless paths as files", func(t *testing.T) {
assert.Equal(t, "/tmp/project/ci/release", ResolveReleaseWorkflowPath("/tmp/project", "ci/release"))
})
@ -164,6 +168,12 @@ func TestWorkflow_ResolveReleaseWorkflowInputPath_Good(t *testing.T) {
assert.Equal(t, "/tmp/project/ci/release.yml", path)
})
t.Run("accepts current-directory-prefixed directory-style path as the primary input", func(t *testing.T) {
path, err := ResolveReleaseWorkflowInputPath("/tmp/project", "./ci", "")
require.NoError(t, err)
assert.Equal(t, "/tmp/project/ci/release.yml", path)
})
t.Run("keeps nested extensionless paths as files", func(t *testing.T) {
path, err := ResolveReleaseWorkflowInputPath("/tmp/project", "ci/release", "")
require.NoError(t, err)
@ -222,6 +232,14 @@ func TestWorkflow_ResolveReleaseWorkflowInputPathWithMedium_Good(t *testing.T) {
assert.Equal(t, "/tmp/project/ci/release.yml", path)
})
t.Run("treats a current-directory-prefixed directory-style path as a workflow directory", func(t *testing.T) {
fs := io.NewMockMedium()
path, err := ResolveReleaseWorkflowInputPathWithMedium(fs, "/tmp/project", "./ci", "")
require.NoError(t, err)
assert.Equal(t, "/tmp/project/ci/release.yml", path)
})
t.Run("keeps a file path unchanged when the target is not a directory", func(t *testing.T) {
fs := io.NewMockMedium()