go-build/pkg/build/workflow.go

527 lines
19 KiB
Go
Raw Permalink Normal View History

// Package build provides project type detection and cross-compilation for the Core build system.
// This file exposes the release workflow generator and its path-resolution helpers.
package build
import (
"embed"
"strings"
"dappco.re/go/core/build/internal/ax"
io_interface "dappco.re/go/core/io"
coreerr "dappco.re/go/core/log"
)
//go:embed templates/release.yml
var releaseWorkflowTemplate embed.FS
// DefaultReleaseWorkflowPath is the conventional output path for the release workflow.
//
// path := build.DefaultReleaseWorkflowPath // ".github/workflows/release.yml"
const DefaultReleaseWorkflowPath = ".github/workflows/release.yml"
// DefaultReleaseWorkflowFileName is the workflow filename used when a directory-style
// output path is supplied.
const DefaultReleaseWorkflowFileName = "release.yml"
// WriteReleaseWorkflow writes the embedded release workflow template to outputPath.
//
// 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, ".github/workflows") // writes .github/workflows/release.yml
// 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(filesystem io_interface.Medium, outputPath string) error {
if filesystem == nil {
return coreerr.E("build.WriteReleaseWorkflow", "filesystem medium is required", nil)
}
2026-04-01 23:00:40 +00:00
outputPath = cleanWorkflowInput(outputPath)
if outputPath == "" {
outputPath = DefaultReleaseWorkflowPath
}
if isWorkflowDirectoryInput(outputPath) || filesystem.IsDir(outputPath) {
outputPath = ax.Join(outputPath, DefaultReleaseWorkflowFileName)
}
content, err := releaseWorkflowTemplate.ReadFile("templates/release.yml")
if err != nil {
return coreerr.E("build.WriteReleaseWorkflow", "failed to read embedded workflow template", err)
}
if err := filesystem.EnsureDir(ax.Dir(outputPath)); err != nil {
return coreerr.E("build.WriteReleaseWorkflow", "failed to create release workflow directory", err)
}
if err := filesystem.Write(outputPath, string(content)); err != nil {
return coreerr.E("build.WriteReleaseWorkflow", "failed to write release workflow", err)
}
return nil
}
// ReleaseWorkflowPath joins a project directory with the conventional workflow path.
//
// build.ReleaseWorkflowPath("/home/user/project") // /home/user/project/.github/workflows/release.yml
func ReleaseWorkflowPath(projectDir string) string {
return ax.Join(projectDir, DefaultReleaseWorkflowPath)
}
// ResolveReleaseWorkflowOutputPathWithMedium resolves the workflow output path
// relative to the project directory and treats an existing directory as a
// workflow directory even when the caller omits a trailing slash.
//
// build.ResolveReleaseWorkflowOutputPathWithMedium(io.Local, "/tmp/project", "ci") // /tmp/project/ci/release.yml when /tmp/project/ci exists
// build.ResolveReleaseWorkflowOutputPathWithMedium(io.Local, "/tmp/project", ".github/workflows") // /tmp/project/.github/workflows/release.yml
func ResolveReleaseWorkflowOutputPathWithMedium(filesystem io_interface.Medium, projectDir, outputPath string) string {
outputPath = cleanWorkflowInput(outputPath)
if outputPath == "" {
return ReleaseWorkflowPath(projectDir)
}
resolved := ResolveReleaseWorkflowPath(projectDir, outputPath)
if filesystem != nil && filesystem.IsDir(resolved) {
return ax.Join(resolved, DefaultReleaseWorkflowFileName)
}
return resolved
}
// ResolveReleaseWorkflowPath resolves the workflow output path relative to the
// 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", ".github/workflows") // /tmp/project/.github/workflows/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
func ResolveReleaseWorkflowPath(projectDir, outputPath string) string {
2026-04-01 23:00:40 +00:00
outputPath = cleanWorkflowInput(outputPath)
if outputPath == "" {
return ReleaseWorkflowPath(projectDir)
}
if isWorkflowDirectoryPath(outputPath) || isWorkflowDirectoryInput(outputPath) {
if ax.IsAbs(outputPath) {
return ax.Join(outputPath, DefaultReleaseWorkflowFileName)
}
return ax.Join(projectDir, outputPath, DefaultReleaseWorkflowFileName)
}
if !ax.IsAbs(outputPath) {
return ax.Join(projectDir, outputPath)
}
return outputPath
}
// ResolveReleaseWorkflowInputPath resolves a workflow target from the CLI/API
// `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
func ResolveReleaseWorkflowInputPath(projectDir, pathInput, outputPathInput string) (string, error) {
return resolveReleaseWorkflowInputPathPair(
pathInput,
outputPathInput,
func(input string) string {
return resolveReleaseWorkflowInputPath(projectDir, input, nil)
},
"build.ResolveReleaseWorkflowInputPath",
)
}
2026-04-01 22:05:27 +00:00
// ResolveReleaseWorkflowInputPathWithMedium resolves the workflow path and
// treats an existing directory as a directory even when the caller omits a
// 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(filesystem io_interface.Medium, projectDir, pathInput, outputPathInput string) (string, error) {
return resolveReleaseWorkflowInputPathPair(
pathInput,
outputPathInput,
func(input string) string {
return resolveReleaseWorkflowInputPath(projectDir, input, filesystem)
},
"build.ResolveReleaseWorkflowInputPathWithMedium",
)
}
2026-04-01 22:05:27 +00:00
2026-04-02 00:49:27 +00:00
// ResolveReleaseWorkflowInputPathAliases resolves the workflow path across the
// public path aliases and treats an existing directory as a directory even
// when the caller omits a trailing slash.
//
// build.ResolveReleaseWorkflowInputPathAliases(io.Local, "/tmp/project", "ci", "", "", "") // /tmp/project/ci/release.yml
// build.ResolveReleaseWorkflowInputPathAliases(io.Local, "/tmp/project", "", "ci", "", "") // /tmp/project/ci/release.yml
// build.ResolveReleaseWorkflowInputPathAliases(io.Local, "/tmp/project", "", "", "ci", "") // /tmp/project/ci/release.yml
// build.ResolveReleaseWorkflowInputPathAliases(io.Local, "/tmp/project", "", "", "", "ci") // /tmp/project/ci/release.yml
func ResolveReleaseWorkflowInputPathAliases(filesystem io_interface.Medium, projectDir, pathInput, workflowPathInput, workflowPathSnakeInput, workflowPathHyphenInput string) (string, error) {
return resolveReleaseWorkflowInputPathAliasSet(
filesystem,
projectDir,
"path",
pathInput,
workflowPathInput,
workflowPathSnakeInput,
workflowPathHyphenInput,
"build.ResolveReleaseWorkflowInputPathAliases",
)
}
// ResolveReleaseWorkflowOutputPath("ci/release.yml", "", "") // "ci/release.yml"
// ResolveReleaseWorkflowOutputPath("", "ci/release.yml", "") // "ci/release.yml"
// ResolveReleaseWorkflowOutputPath("", "", "ci/release.yml") // "ci/release.yml"
// ResolveReleaseWorkflowOutputPath("ci/release.yml", "ops.yml", "") // error
func ResolveReleaseWorkflowOutputPath(outputPathInput, outputPathSnakeInput, legacyOutputInput string) (string, error) {
return ResolveReleaseWorkflowOutputPathAliases(
outputPathInput,
"",
outputPathSnakeInput,
legacyOutputInput,
"",
"",
"",
"",
"",
)
}
// ResolveReleaseWorkflowOutputPathAliases resolves every public workflow output
// alias across the CLI, API, and UI layers.
//
// build.ResolveReleaseWorkflowOutputPathAliases("ci/release.yml", "", "", "", "", "", "", "", "") // "ci/release.yml"
// build.ResolveReleaseWorkflowOutputPathAliases("", "ci/release.yml", "", "", "", "", "", "", "") // "ci/release.yml"
// build.ResolveReleaseWorkflowOutputPathAliases("", "", "", "", "ci/release.yml", "", "", "", "") // "ci/release.yml"
// build.ResolveReleaseWorkflowOutputPathAliases("", "", "", "", "", "ci/release.yml", "", "", "") // "ci/release.yml"
func ResolveReleaseWorkflowOutputPathAliases(
outputPathInput,
outputPathHyphenInput,
outputPathSnakeInput,
legacyOutputInput,
workflowOutputPathInput,
workflowOutputSnakeInput,
workflowOutputHyphenInput,
workflowOutputPathSnakeInput,
workflowOutputPathHyphenInput string,
) (string, error) {
return resolveReleaseWorkflowOutputAliasSet(
outputPathInput,
outputPathHyphenInput,
outputPathSnakeInput,
legacyOutputInput,
workflowOutputPathInput,
workflowOutputSnakeInput,
workflowOutputHyphenInput,
workflowOutputPathSnakeInput,
workflowOutputPathHyphenInput,
"build.ResolveReleaseWorkflowOutputPathAliases",
)
}
// ResolveReleaseWorkflowOutputPathAliasesInProject resolves the workflow output
// aliases relative to a project directory before checking for conflicts.
//
// build.ResolveReleaseWorkflowOutputPathAliasesInProject("/tmp/project", "ci/release.yml", "", "", "", "", "", "", "") // "/tmp/project/ci/release.yml"
// build.ResolveReleaseWorkflowOutputPathAliasesInProject("/tmp/project", "", "", "", "", "/tmp/project/ci/release.yml", "", "", "") // "/tmp/project/ci/release.yml"
func ResolveReleaseWorkflowOutputPathAliasesInProject(
projectDir,
outputPathInput,
outputPathHyphenInput,
outputPathSnakeInput,
legacyOutputInput,
workflowOutputPathInput,
workflowOutputSnakeInput,
workflowOutputHyphenInput,
workflowOutputPathSnakeInput,
workflowOutputPathHyphenInput string,
) (string, error) {
return ResolveReleaseWorkflowOutputPathAliasesInProjectWithMedium(
nil,
projectDir,
outputPathInput,
outputPathHyphenInput,
outputPathSnakeInput,
legacyOutputInput,
workflowOutputPathInput,
workflowOutputSnakeInput,
workflowOutputHyphenInput,
workflowOutputPathSnakeInput,
workflowOutputPathHyphenInput,
)
}
// ResolveReleaseWorkflowOutputPathAliasesInProjectWithMedium resolves the
// workflow output aliases relative to a project directory and uses the
// provided filesystem medium to treat existing directories as workflow
// directories even when callers omit a trailing separator.
//
// build.ResolveReleaseWorkflowOutputPathAliasesInProjectWithMedium(io.Local, "/tmp/project", "", "", "", "", "/tmp/project/ci", "", "", "") // "/tmp/project/ci/release.yml"
func ResolveReleaseWorkflowOutputPathAliasesInProjectWithMedium(
filesystem io_interface.Medium,
projectDir,
outputPathInput,
outputPathHyphenInput,
outputPathSnakeInput,
legacyOutputInput,
workflowOutputPathInput,
workflowOutputSnakeInput,
workflowOutputHyphenInput,
workflowOutputPathSnakeInput,
workflowOutputPathHyphenInput string,
) (string, error) {
return resolveReleaseWorkflowOutputAliasSetInProject(
filesystem,
projectDir,
outputPathInput,
outputPathHyphenInput,
outputPathSnakeInput,
legacyOutputInput,
workflowOutputPathInput,
workflowOutputSnakeInput,
workflowOutputHyphenInput,
workflowOutputPathSnakeInput,
workflowOutputPathHyphenInput,
"build.ResolveReleaseWorkflowOutputPathAliasesInProject",
)
}
// resolveReleaseWorkflowInputPathPair resolves the workflow path from the path
// and output aliases, rejecting conflicting values and preferring explicit
// inputs over the default.
func resolveReleaseWorkflowInputPathPair(pathInput, outputPathInput string, resolve func(string) string, errorName string) (string, error) {
pathInput = cleanWorkflowInput(pathInput)
outputPathInput = cleanWorkflowInput(outputPathInput)
if pathInput != "" && outputPathInput != "" {
resolvedPath := resolve(pathInput)
resolvedOutput := resolve(outputPathInput)
2026-04-01 22:05:27 +00:00
if resolvedPath != resolvedOutput {
return "", coreerr.E(errorName, "path and output specify different locations", nil)
2026-04-01 22:05:27 +00:00
}
return resolvedPath, nil
}
if pathInput != "" {
return resolve(pathInput), nil
2026-04-01 22:05:27 +00:00
}
if outputPathInput != "" {
return resolve(outputPathInput), nil
2026-04-01 22:05:27 +00:00
}
return resolve(""), nil
}
// resolveReleaseWorkflowOutputAliasSet resolves a workflow output alias set by
// trimming whitespace, rejecting conflicts, and returning the first non-empty
// value when aliases agree.
func resolveReleaseWorkflowOutputAliasSet(
outputPathInput,
outputPathHyphenInput,
outputPathSnakeInput,
legacyOutputInput,
workflowOutputPathInput,
workflowOutputSnakeInput,
workflowOutputHyphenInput,
workflowOutputPathSnakeInput,
workflowOutputPathHyphenInput,
errorName string,
) (string, error) {
values := []string{
normalizeWorkflowOutputAlias(outputPathInput),
normalizeWorkflowOutputAlias(outputPathHyphenInput),
normalizeWorkflowOutputAlias(outputPathSnakeInput),
normalizeWorkflowOutputAlias(legacyOutputInput),
normalizeWorkflowOutputAlias(workflowOutputPathInput),
normalizeWorkflowOutputAlias(workflowOutputSnakeInput),
normalizeWorkflowOutputAlias(workflowOutputHyphenInput),
normalizeWorkflowOutputAlias(workflowOutputPathSnakeInput),
normalizeWorkflowOutputAlias(workflowOutputPathHyphenInput),
}
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
}
// resolveReleaseWorkflowOutputAliasSetInProject resolves workflow output aliases
// against a project directory so relative and absolute paths can be compared.
func resolveReleaseWorkflowOutputAliasSetInProject(
filesystem io_interface.Medium,
projectDir,
outputPathInput,
outputPathHyphenInput,
outputPathSnakeInput,
legacyOutputInput,
workflowOutputPathInput,
workflowOutputSnakeInput,
workflowOutputHyphenInput,
workflowOutputPathSnakeInput,
workflowOutputPathHyphenInput,
errorName string,
) (string, error) {
values := []string{
cleanWorkflowInput(outputPathInput),
cleanWorkflowInput(outputPathHyphenInput),
cleanWorkflowInput(outputPathSnakeInput),
cleanWorkflowInput(legacyOutputInput),
cleanWorkflowInput(workflowOutputPathInput),
cleanWorkflowInput(workflowOutputSnakeInput),
cleanWorkflowInput(workflowOutputHyphenInput),
cleanWorkflowInput(workflowOutputPathSnakeInput),
cleanWorkflowInput(workflowOutputPathHyphenInput),
}
var resolved string
for _, value := range values {
if value == "" {
continue
}
candidate := ResolveReleaseWorkflowOutputPathWithMedium(filesystem, projectDir, value)
if resolved == "" {
resolved = candidate
continue
}
if resolved != candidate {
return "", coreerr.E(errorName, "output aliases specify different locations", nil)
}
}
return resolved, nil
}
// normalizeWorkflowOutputAlias canonicalises a workflow output alias for comparison.
func normalizeWorkflowOutputAlias(path string) string {
path = cleanWorkflowInput(path)
if path == "" {
return ""
}
return ax.Clean(path)
}
// resolveReleaseWorkflowInputPath resolves one workflow input into a file path.
//
// resolveReleaseWorkflowInputPath("/tmp/project", "ci", io.Local) // /tmp/project/ci/release.yml
func resolveReleaseWorkflowInputPath(projectDir, input string, medium io_interface.Medium) string {
2026-04-01 23:00:40 +00:00
input = cleanWorkflowInput(input)
if input == "" {
return ReleaseWorkflowPath(projectDir)
}
if isWorkflowDirectoryInput(input) {
if ax.IsAbs(input) {
return ax.Join(input, DefaultReleaseWorkflowFileName)
}
return ax.Join(projectDir, input, DefaultReleaseWorkflowFileName)
}
resolved := ResolveReleaseWorkflowPath(projectDir, input)
if medium != nil && medium.IsDir(resolved) {
return ax.Join(resolved, DefaultReleaseWorkflowFileName)
}
return resolved
}
2026-04-02 00:49:27 +00:00
// resolveReleaseWorkflowInputPathAliasSet resolves a workflow path from a set
// of aliases and rejects conflicting values.
func resolveReleaseWorkflowInputPathAliasSet(filesystem io_interface.Medium, projectDir, fieldLabel, primaryInput, secondaryInput, tertiaryInput, quaternaryInput, errorName string) (string, error) {
2026-04-02 00:49:27 +00:00
values := []string{
cleanWorkflowInput(primaryInput),
cleanWorkflowInput(secondaryInput),
cleanWorkflowInput(tertiaryInput),
cleanWorkflowInput(quaternaryInput),
}
var resolved string
for _, value := range values {
if value == "" {
continue
}
candidate := resolveReleaseWorkflowInputPath(projectDir, value, filesystem)
if resolved == "" {
resolved = candidate
continue
}
if resolved != candidate {
return "", coreerr.E(errorName, fieldLabel+" aliases specify different locations", nil)
2026-04-02 00:49:27 +00:00
}
}
return resolved, nil
}
// isWorkflowDirectoryPath reports whether a workflow path is explicitly marked
// as a directory with a trailing separator.
func isWorkflowDirectoryPath(path string) bool {
2026-04-01 23:00:40 +00:00
path = cleanWorkflowInput(path)
if path == "" {
return false
}
if path == "." || path == "./" || path == ".\\" {
return true
}
last := path[len(path)-1]
return last == '/' || last == '\\'
}
// 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, plus current-directory-prefixed
// directory targets like "./ci" and the conventional ".github/workflows" path.
func isWorkflowDirectoryInput(path string) bool {
2026-04-01 23:00:40 +00:00
path = cleanWorkflowInput(path)
if isWorkflowDirectoryPath(path) {
return true
}
if path == "" || ax.Ext(path) != "" {
return false
}
if !strings.ContainsAny(path, "/\\") {
return true
}
if ax.Base(path) == "workflows" {
return true
}
if strings.HasPrefix(path, "./") || strings.HasPrefix(path, ".\\") {
trimmed := strings.TrimPrefix(strings.TrimPrefix(path, "./"), ".\\")
if trimmed == "" {
return false
}
if ax.Base(trimmed) == "workflows" {
return true
}
return !strings.ContainsAny(trimmed, "/\\")
}
return false
}
2026-04-01 23:00:40 +00:00
// cleanWorkflowInput trims surrounding whitespace from a workflow path input.
func cleanWorkflowInput(path string) string {
return strings.TrimSpace(path)
}