chore: apply AX experience-aligned code updates
This commit is contained in:
commit
007e30e869
93 changed files with 2704 additions and 2638 deletions
|
|
@ -140,6 +140,7 @@ func initBuildFlags() {
|
|||
}
|
||||
|
||||
// AddBuildCommands registers the 'build' command and all subcommands.
|
||||
// Usage example: call buildcmd.AddBuildCommands(...) from integrating code.
|
||||
func AddBuildCommands(root *cli.Command) {
|
||||
setBuildI18n()
|
||||
initBuildFlags()
|
||||
|
|
|
|||
|
|
@ -7,19 +7,17 @@ package buildcmd
|
|||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strings"
|
||||
|
||||
"dappco.re/go/core"
|
||||
"dappco.re/go/core/build/internal/ax"
|
||||
"dappco.re/go/core/build/pkg/build"
|
||||
"dappco.re/go/core/build/pkg/build/builders"
|
||||
"dappco.re/go/core/build/pkg/build/signing"
|
||||
"dappco.re/go/core/i18n"
|
||||
"dappco.re/go/core/io"
|
||||
coreerr "dappco.re/go/core/log"
|
||||
"forge.lthn.ai/core/cli/pkg/cli"
|
||||
)
|
||||
|
||||
// runProjectBuild handles the main `core build` command with auto-detection.
|
||||
|
|
@ -28,7 +26,7 @@ func runProjectBuild(ctx context.Context, buildType string, ciMode bool, targets
|
|||
fs := io.Local
|
||||
|
||||
// Get current working directory as project root
|
||||
projectDir, err := os.Getwd()
|
||||
projectDir, err := ax.Getwd()
|
||||
if err != nil {
|
||||
return coreerr.E("build.Run", "failed to get working directory", err)
|
||||
}
|
||||
|
|
@ -78,14 +76,14 @@ func runProjectBuild(ctx context.Context, buildType string, ciMode bool, targets
|
|||
if outputDir == "" {
|
||||
outputDir = "dist"
|
||||
}
|
||||
if !filepath.IsAbs(outputDir) {
|
||||
outputDir = filepath.Join(projectDir, outputDir)
|
||||
if !ax.IsAbs(outputDir) {
|
||||
outputDir = ax.Join(projectDir, outputDir)
|
||||
}
|
||||
outputDir = filepath.Clean(outputDir)
|
||||
outputDir = ax.Clean(outputDir)
|
||||
|
||||
// Ensure config path is absolute if provided
|
||||
if configPath != "" && !filepath.IsAbs(configPath) {
|
||||
configPath = filepath.Join(projectDir, configPath)
|
||||
if configPath != "" && !ax.IsAbs(configPath) {
|
||||
configPath = ax.Join(projectDir, configPath)
|
||||
}
|
||||
|
||||
// Determine binary name
|
||||
|
|
@ -94,17 +92,17 @@ func runProjectBuild(ctx context.Context, buildType string, ciMode bool, targets
|
|||
binaryName = buildCfg.Project.Name
|
||||
}
|
||||
if binaryName == "" {
|
||||
binaryName = filepath.Base(projectDir)
|
||||
binaryName = ax.Base(projectDir)
|
||||
}
|
||||
|
||||
// Print build info (verbose mode only)
|
||||
if verbose && !ciMode {
|
||||
fmt.Printf("%s %s\n", buildHeaderStyle.Render(i18n.T("cmd.build.label.build")), i18n.T("cmd.build.building_project"))
|
||||
fmt.Printf(" %s %s\n", i18n.T("cmd.build.label.type"), buildTargetStyle.Render(string(projectType)))
|
||||
fmt.Printf(" %s %s\n", i18n.T("cmd.build.label.output"), buildTargetStyle.Render(outputDir))
|
||||
fmt.Printf(" %s %s\n", i18n.T("cmd.build.label.binary"), buildTargetStyle.Render(binaryName))
|
||||
fmt.Printf(" %s %s\n", i18n.T("cmd.build.label.targets"), buildTargetStyle.Render(formatTargets(buildTargets)))
|
||||
fmt.Println()
|
||||
cli.Print("%s %s\n", buildHeaderStyle.Render(i18n.T("cmd.build.label.build")), i18n.T("cmd.build.building_project"))
|
||||
cli.Print(" %s %s\n", i18n.T("cmd.build.label.type"), buildTargetStyle.Render(string(projectType)))
|
||||
cli.Print(" %s %s\n", i18n.T("cmd.build.label.output"), buildTargetStyle.Render(outputDir))
|
||||
cli.Print(" %s %s\n", i18n.T("cmd.build.label.binary"), buildTargetStyle.Render(binaryName))
|
||||
cli.Print(" %s %s\n", i18n.T("cmd.build.label.targets"), buildTargetStyle.Render(formatTargets(buildTargets)))
|
||||
cli.Blank()
|
||||
}
|
||||
|
||||
// Get the appropriate builder
|
||||
|
|
@ -131,30 +129,30 @@ func runProjectBuild(ctx context.Context, buildType string, ciMode bool, targets
|
|||
|
||||
// Parse formats for LinuxKit
|
||||
if format != "" {
|
||||
cfg.Formats = strings.Split(format, ",")
|
||||
cfg.Formats = core.Split(format, ",")
|
||||
}
|
||||
|
||||
// Execute build
|
||||
artifacts, err := builder.Build(ctx, cfg, buildTargets)
|
||||
if err != nil {
|
||||
if !ciMode {
|
||||
fmt.Printf("%s %v\n", buildErrorStyle.Render(i18n.T("common.label.error")), err)
|
||||
cli.Print("%s %v\n", buildErrorStyle.Render(i18n.T("common.label.error")), err)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
if verbose && !ciMode {
|
||||
fmt.Printf("%s %s\n", buildSuccessStyle.Render(i18n.T("common.label.success")), i18n.T("cmd.build.built_artifacts", map[string]any{"Count": len(artifacts)}))
|
||||
fmt.Println()
|
||||
cli.Print("%s %s\n", buildSuccessStyle.Render(i18n.T("common.label.success")), i18n.T("cmd.build.built_artifacts", map[string]any{"Count": len(artifacts)}))
|
||||
cli.Blank()
|
||||
for _, artifact := range artifacts {
|
||||
relPath, err := filepath.Rel(projectDir, artifact.Path)
|
||||
relPath, err := ax.Rel(projectDir, artifact.Path)
|
||||
if err != nil {
|
||||
relPath = artifact.Path
|
||||
}
|
||||
fmt.Printf(" %s %s %s\n",
|
||||
cli.Print(" %s %s %s\n",
|
||||
buildSuccessStyle.Render("*"),
|
||||
buildTargetStyle.Render(relPath),
|
||||
buildDimStyle.Render(fmt.Sprintf("(%s/%s)", artifact.OS, artifact.Arch)),
|
||||
buildDimStyle.Render(core.Sprintf("(%s/%s)", artifact.OS, artifact.Arch)),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -170,8 +168,8 @@ func runProjectBuild(ctx context.Context, buildType string, ciMode bool, targets
|
|||
|
||||
if signCfg.Enabled && runtime.GOOS == "darwin" {
|
||||
if verbose && !ciMode {
|
||||
fmt.Println()
|
||||
fmt.Printf("%s %s\n", buildHeaderStyle.Render(i18n.T("cmd.build.label.sign")), i18n.T("cmd.build.signing_binaries"))
|
||||
cli.Blank()
|
||||
cli.Print("%s %s\n", buildHeaderStyle.Render(i18n.T("cmd.build.label.sign")), i18n.T("cmd.build.signing_binaries"))
|
||||
}
|
||||
|
||||
// Convert build.Artifact to signing.Artifact
|
||||
|
|
@ -182,7 +180,7 @@ func runProjectBuild(ctx context.Context, buildType string, ciMode bool, targets
|
|||
|
||||
if err := signing.SignBinaries(ctx, fs, signCfg, signingArtifacts); err != nil {
|
||||
if !ciMode {
|
||||
fmt.Printf("%s %s: %v\n", buildErrorStyle.Render(i18n.T("common.label.error")), i18n.T("cmd.build.error.signing_failed"), err)
|
||||
cli.Print("%s %s: %v\n", buildErrorStyle.Render(i18n.T("common.label.error")), i18n.T("cmd.build.error.signing_failed"), err)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
|
@ -190,7 +188,7 @@ func runProjectBuild(ctx context.Context, buildType string, ciMode bool, targets
|
|||
if signCfg.MacOS.Notarize {
|
||||
if err := signing.NotarizeBinaries(ctx, fs, signCfg, signingArtifacts); err != nil {
|
||||
if !ciMode {
|
||||
fmt.Printf("%s %s: %v\n", buildErrorStyle.Render(i18n.T("common.label.error")), i18n.T("cmd.build.error.notarization_failed"), err)
|
||||
cli.Print("%s %s: %v\n", buildErrorStyle.Render(i18n.T("common.label.error")), i18n.T("cmd.build.error.notarization_failed"), err)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
|
@ -201,28 +199,28 @@ func runProjectBuild(ctx context.Context, buildType string, ciMode bool, targets
|
|||
var archivedArtifacts []build.Artifact
|
||||
if doArchive && len(artifacts) > 0 {
|
||||
if verbose && !ciMode {
|
||||
fmt.Println()
|
||||
fmt.Printf("%s %s\n", buildHeaderStyle.Render(i18n.T("cmd.build.label.archive")), i18n.T("cmd.build.creating_archives"))
|
||||
cli.Blank()
|
||||
cli.Print("%s %s\n", buildHeaderStyle.Render(i18n.T("cmd.build.label.archive")), i18n.T("cmd.build.creating_archives"))
|
||||
}
|
||||
|
||||
archivedArtifacts, err = build.ArchiveAll(fs, artifacts)
|
||||
if err != nil {
|
||||
if !ciMode {
|
||||
fmt.Printf("%s %s: %v\n", buildErrorStyle.Render(i18n.T("common.label.error")), i18n.T("cmd.build.error.archive_failed"), err)
|
||||
cli.Print("%s %s: %v\n", buildErrorStyle.Render(i18n.T("common.label.error")), i18n.T("cmd.build.error.archive_failed"), err)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
if verbose && !ciMode {
|
||||
for _, artifact := range archivedArtifacts {
|
||||
relPath, err := filepath.Rel(projectDir, artifact.Path)
|
||||
relPath, err := ax.Rel(projectDir, artifact.Path)
|
||||
if err != nil {
|
||||
relPath = artifact.Path
|
||||
}
|
||||
fmt.Printf(" %s %s %s\n",
|
||||
cli.Print(" %s %s %s\n",
|
||||
buildSuccessStyle.Render("*"),
|
||||
buildTargetStyle.Render(relPath),
|
||||
buildDimStyle.Render(fmt.Sprintf("(%s/%s)", artifact.OS, artifact.Arch)),
|
||||
buildDimStyle.Render(core.Sprintf("(%s/%s)", artifact.OS, artifact.Arch)),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -256,17 +254,17 @@ func runProjectBuild(ctx context.Context, buildType string, ciMode bool, targets
|
|||
}
|
||||
|
||||
// JSON output for CI
|
||||
output, err := json.MarshalIndent(outputArtifacts, "", " ")
|
||||
output, err := ax.JSONMarshal(outputArtifacts)
|
||||
if err != nil {
|
||||
return coreerr.E("build.Run", "failed to marshal artifacts", err)
|
||||
}
|
||||
fmt.Println(string(output))
|
||||
cli.Print("%s\n", output)
|
||||
} else if !verbose {
|
||||
// Minimal output: just success with artifact count
|
||||
fmt.Printf("%s %s %s\n",
|
||||
cli.Print("%s %s %s\n",
|
||||
buildSuccessStyle.Render(i18n.T("common.label.success")),
|
||||
i18n.T("cmd.build.built_artifacts", map[string]any{"Count": len(artifacts)}),
|
||||
buildDimStyle.Render(fmt.Sprintf("(%s)", outputDir)),
|
||||
buildDimStyle.Render(core.Sprintf("(%s)", outputDir)),
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -277,23 +275,23 @@ func runProjectBuild(ctx context.Context, buildType string, ciMode bool, targets
|
|||
func computeAndWriteChecksums(ctx context.Context, projectDir, outputDir string, artifacts []build.Artifact, signCfg signing.SignConfig, ciMode bool, verbose bool) ([]build.Artifact, error) {
|
||||
fs := io.Local
|
||||
if verbose && !ciMode {
|
||||
fmt.Println()
|
||||
fmt.Printf("%s %s\n", buildHeaderStyle.Render(i18n.T("cmd.build.label.checksum")), i18n.T("cmd.build.computing_checksums"))
|
||||
cli.Blank()
|
||||
cli.Print("%s %s\n", buildHeaderStyle.Render(i18n.T("cmd.build.label.checksum")), i18n.T("cmd.build.computing_checksums"))
|
||||
}
|
||||
|
||||
checksummedArtifacts, err := build.ChecksumAll(fs, artifacts)
|
||||
if err != nil {
|
||||
if !ciMode {
|
||||
fmt.Printf("%s %s: %v\n", buildErrorStyle.Render(i18n.T("common.label.error")), i18n.T("cmd.build.error.checksum_failed"), err)
|
||||
cli.Print("%s %s: %v\n", buildErrorStyle.Render(i18n.T("common.label.error")), i18n.T("cmd.build.error.checksum_failed"), err)
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Write CHECKSUMS.txt
|
||||
checksumPath := filepath.Join(outputDir, "CHECKSUMS.txt")
|
||||
checksumPath := ax.Join(outputDir, "CHECKSUMS.txt")
|
||||
if err := build.WriteChecksumFile(fs, checksummedArtifacts, checksumPath); err != nil {
|
||||
if !ciMode {
|
||||
fmt.Printf("%s %s: %v\n", buildErrorStyle.Render(i18n.T("common.label.error")), i18n.T("common.error.failed", map[string]any{"Action": "write CHECKSUMS.txt"}), err)
|
||||
cli.Print("%s %s: %v\n", buildErrorStyle.Render(i18n.T("common.label.error")), i18n.T("common.error.failed", map[string]any{"Action": "write CHECKSUMS.txt"}), err)
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
|
|
@ -302,7 +300,7 @@ func computeAndWriteChecksums(ctx context.Context, projectDir, outputDir string,
|
|||
if signCfg.Enabled {
|
||||
if err := signing.SignChecksums(ctx, fs, signCfg, checksumPath); err != nil {
|
||||
if !ciMode {
|
||||
fmt.Printf("%s %s: %v\n", buildErrorStyle.Render(i18n.T("common.label.error")), i18n.T("cmd.build.error.gpg_signing_failed"), err)
|
||||
cli.Print("%s %s: %v\n", buildErrorStyle.Render(i18n.T("common.label.error")), i18n.T("cmd.build.error.gpg_signing_failed"), err)
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
|
|
@ -310,22 +308,22 @@ func computeAndWriteChecksums(ctx context.Context, projectDir, outputDir string,
|
|||
|
||||
if verbose && !ciMode {
|
||||
for _, artifact := range checksummedArtifacts {
|
||||
relPath, err := filepath.Rel(projectDir, artifact.Path)
|
||||
relPath, err := ax.Rel(projectDir, artifact.Path)
|
||||
if err != nil {
|
||||
relPath = artifact.Path
|
||||
}
|
||||
fmt.Printf(" %s %s\n",
|
||||
cli.Print(" %s %s\n",
|
||||
buildSuccessStyle.Render("*"),
|
||||
buildTargetStyle.Render(relPath),
|
||||
)
|
||||
fmt.Printf(" %s\n", buildDimStyle.Render(artifact.Checksum))
|
||||
cli.Print(" %s\n", buildDimStyle.Render(artifact.Checksum))
|
||||
}
|
||||
|
||||
relChecksumPath, err := filepath.Rel(projectDir, checksumPath)
|
||||
relChecksumPath, err := ax.Rel(projectDir, checksumPath)
|
||||
if err != nil {
|
||||
relChecksumPath = checksumPath
|
||||
}
|
||||
fmt.Printf(" %s %s\n",
|
||||
cli.Print(" %s %s\n",
|
||||
buildSuccessStyle.Render("*"),
|
||||
buildTargetStyle.Render(relChecksumPath),
|
||||
)
|
||||
|
|
@ -336,23 +334,23 @@ func computeAndWriteChecksums(ctx context.Context, projectDir, outputDir string,
|
|||
|
||||
// parseTargets parses a comma-separated list of OS/arch pairs.
|
||||
func parseTargets(targetsFlag string) ([]build.Target, error) {
|
||||
parts := strings.Split(targetsFlag, ",")
|
||||
parts := core.Split(targetsFlag, ",")
|
||||
var targets []build.Target
|
||||
|
||||
for _, part := range parts {
|
||||
part = strings.TrimSpace(part)
|
||||
part = core.Trim(part)
|
||||
if part == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
osArch := strings.Split(part, "/")
|
||||
osArch := core.Split(part, "/")
|
||||
if len(osArch) != 2 {
|
||||
return nil, coreerr.E("build.parseTargets", "invalid target format (expected os/arch): "+part, nil)
|
||||
}
|
||||
|
||||
targets = append(targets, build.Target{
|
||||
OS: strings.TrimSpace(osArch[0]),
|
||||
Arch: strings.TrimSpace(osArch[1]),
|
||||
OS: core.Trim(osArch[0]),
|
||||
Arch: core.Trim(osArch[1]),
|
||||
})
|
||||
}
|
||||
|
||||
|
|
@ -369,7 +367,7 @@ func formatTargets(targets []build.Target) string {
|
|||
for _, t := range targets {
|
||||
parts = append(parts, t.String())
|
||||
}
|
||||
return strings.Join(parts, ", ")
|
||||
return core.Join(", ", parts...)
|
||||
}
|
||||
|
||||
// getBuilder returns the appropriate builder for the project type.
|
||||
|
|
|
|||
|
|
@ -7,16 +7,13 @@
|
|||
package buildcmd
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"context"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"dappco.re/go/core"
|
||||
"dappco.re/go/core/build/internal/ax"
|
||||
"dappco.re/go/core/i18n"
|
||||
coreio "dappco.re/go/core/io"
|
||||
coreerr "dappco.re/go/core/log"
|
||||
|
|
@ -33,14 +30,14 @@ var (
|
|||
|
||||
// runPwaBuild downloads a PWA from URL and builds it.
|
||||
func runPwaBuild(pwaURL string) error {
|
||||
fmt.Printf("%s %s\n", i18n.T("cmd.build.pwa.starting"), pwaURL)
|
||||
core.Print(nil, "%s %s", i18n.T("cmd.build.pwa.starting"), pwaURL)
|
||||
|
||||
tempDir, err := os.MkdirTemp("", "core-pwa-build-*")
|
||||
tempDir, err := ax.TempDir("core-pwa-build-*")
|
||||
if err != nil {
|
||||
return coreerr.E("pwa.runPwaBuild", i18n.T("common.error.failed", map[string]any{"Action": "create temporary directory"}), err)
|
||||
}
|
||||
// defer os.RemoveAll(tempDir) // Keep temp dir for debugging
|
||||
fmt.Printf("%s %s\n", i18n.T("cmd.build.pwa.downloading_to"), tempDir)
|
||||
core.Print(nil, "%s %s", i18n.T("cmd.build.pwa.downloading_to"), tempDir)
|
||||
|
||||
if err := downloadPWA(pwaURL, tempDir); err != nil {
|
||||
return coreerr.E("pwa.runPwaBuild", i18n.T("common.error.failed", map[string]any{"Action": "download PWA"}), err)
|
||||
|
|
@ -67,14 +64,14 @@ func downloadPWA(baseURL, destDir string) error {
|
|||
manifestURL, err := findManifestURL(string(body), baseURL)
|
||||
if err != nil {
|
||||
// If no manifest, it's not a PWA, but we can still try to package it as a simple site.
|
||||
fmt.Printf("%s %s\n", i18n.T("common.label.warning"), i18n.T("cmd.build.pwa.no_manifest"))
|
||||
if err := coreio.Local.Write(filepath.Join(destDir, "index.html"), string(body)); err != nil {
|
||||
core.Print(nil, "%s %s", i18n.T("common.label.warning"), i18n.T("cmd.build.pwa.no_manifest"))
|
||||
if err := coreio.Local.Write(ax.Join(destDir, "index.html"), string(body)); err != nil {
|
||||
return coreerr.E("pwa.downloadPWA", i18n.T("common.error.failed", map[string]any{"Action": "write index.html"}), err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
fmt.Printf("%s %s\n", i18n.T("cmd.build.pwa.found_manifest"), manifestURL)
|
||||
core.Print(nil, "%s %s", i18n.T("cmd.build.pwa.found_manifest"), manifestURL)
|
||||
|
||||
// Fetch and parse the manifest
|
||||
manifest, err := fetchManifest(manifestURL)
|
||||
|
|
@ -86,22 +83,22 @@ func downloadPWA(baseURL, destDir string) error {
|
|||
assets := collectAssets(manifest, manifestURL)
|
||||
for _, assetURL := range assets {
|
||||
if err := downloadAsset(assetURL, destDir); err != nil {
|
||||
fmt.Printf("%s %s %s: %v\n", i18n.T("common.label.warning"), i18n.T("common.error.failed", map[string]any{"Action": "download asset"}), assetURL, err)
|
||||
core.Print(nil, "%s %s %s: %v", i18n.T("common.label.warning"), i18n.T("common.error.failed", map[string]any{"Action": "download asset"}), assetURL, err)
|
||||
}
|
||||
}
|
||||
|
||||
// Also save the root index.html
|
||||
if err := coreio.Local.Write(filepath.Join(destDir, "index.html"), string(body)); err != nil {
|
||||
if err := coreio.Local.Write(ax.Join(destDir, "index.html"), string(body)); err != nil {
|
||||
return coreerr.E("pwa.downloadPWA", i18n.T("common.error.failed", map[string]any{"Action": "write index.html"}), err)
|
||||
}
|
||||
|
||||
fmt.Println(i18n.T("cmd.build.pwa.download_complete"))
|
||||
core.Println(i18n.T("cmd.build.pwa.download_complete"))
|
||||
return nil
|
||||
}
|
||||
|
||||
// findManifestURL extracts the manifest URL from HTML content.
|
||||
func findManifestURL(htmlContent, baseURL string) (string, error) {
|
||||
doc, err := html.Parse(strings.NewReader(htmlContent))
|
||||
doc, err := html.Parse(core.NewReader(htmlContent))
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
|
@ -155,8 +152,13 @@ func fetchManifest(manifestURL string) (map[string]any, error) {
|
|||
}
|
||||
defer func() { _ = resp.Body.Close() }()
|
||||
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var manifest map[string]any
|
||||
if err := json.NewDecoder(resp.Body).Decode(&manifest); err != nil {
|
||||
if err := ax.JSONUnmarshal(body, &manifest); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return manifest, nil
|
||||
|
|
@ -203,12 +205,13 @@ func downloadAsset(assetURL, destDir string) error {
|
|||
return err
|
||||
}
|
||||
|
||||
path := filepath.Join(destDir, filepath.FromSlash(u.Path))
|
||||
if err := coreio.Local.EnsureDir(filepath.Dir(path)); err != nil {
|
||||
assetPath := core.TrimPrefix(ax.FromSlash(u.Path), ax.DS())
|
||||
path := ax.Join(destDir, assetPath)
|
||||
if err := coreio.Local.EnsureDir(ax.Dir(path)); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
out, err := os.Create(path)
|
||||
out, err := ax.Create(path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
|
@ -220,16 +223,16 @@ func downloadAsset(assetURL, destDir string) error {
|
|||
|
||||
// runBuild builds a desktop application from a local directory.
|
||||
func runBuild(fromPath string) error {
|
||||
fmt.Printf("%s %s\n", i18n.T("cmd.build.from_path.starting"), fromPath)
|
||||
core.Print(nil, "%s %s", i18n.T("cmd.build.from_path.starting"), fromPath)
|
||||
|
||||
if !coreio.Local.IsDir(fromPath) {
|
||||
return coreerr.E("pwa.runBuild", i18n.T("cmd.build.from_path.error.must_be_directory"), nil)
|
||||
}
|
||||
|
||||
buildDir := ".core/build/app"
|
||||
htmlDir := filepath.Join(buildDir, "html")
|
||||
appName := filepath.Base(fromPath)
|
||||
if strings.HasPrefix(appName, "core-pwa-build-") {
|
||||
htmlDir := ax.Join(buildDir, "html")
|
||||
appName := ax.Base(fromPath)
|
||||
if core.HasPrefix(appName, "core-pwa-build-") {
|
||||
appName = "pwa-app"
|
||||
}
|
||||
outputExe := appName
|
||||
|
|
@ -239,7 +242,7 @@ func runBuild(fromPath string) error {
|
|||
}
|
||||
|
||||
// 1. Generate the project from the embedded template
|
||||
fmt.Println(i18n.T("cmd.build.from_path.generating_template"))
|
||||
core.Println(i18n.T("cmd.build.from_path.generating_template"))
|
||||
templateFS, err := debme.FS(guiTemplate, "tmpl/gui")
|
||||
if err != nil {
|
||||
return coreerr.E("pwa.runBuild", i18n.T("common.error.failed", map[string]any{"Action": "anchor template filesystem"}), err)
|
||||
|
|
@ -255,67 +258,75 @@ func runBuild(fromPath string) error {
|
|||
}
|
||||
|
||||
// 2. Copy the user's web app files
|
||||
fmt.Println(i18n.T("cmd.build.from_path.copying_files"))
|
||||
core.Println(i18n.T("cmd.build.from_path.copying_files"))
|
||||
if err := copyDir(fromPath, htmlDir); err != nil {
|
||||
return coreerr.E("pwa.runBuild", i18n.T("common.error.failed", map[string]any{"Action": "copy application files"}), err)
|
||||
}
|
||||
|
||||
// 3. Compile the application
|
||||
fmt.Println(i18n.T("cmd.build.from_path.compiling"))
|
||||
core.Println(i18n.T("cmd.build.from_path.compiling"))
|
||||
|
||||
// Run go mod tidy
|
||||
cmd := exec.Command("go", "mod", "tidy")
|
||||
cmd.Dir = buildDir
|
||||
cmd.Stdout = os.Stdout
|
||||
cmd.Stderr = os.Stderr
|
||||
if err := cmd.Run(); err != nil {
|
||||
if err := ax.ExecDir(context.Background(), buildDir, "go", "mod", "tidy"); err != nil {
|
||||
return coreerr.E("pwa.runBuild", i18n.T("cmd.build.from_path.error.go_mod_tidy"), err)
|
||||
}
|
||||
|
||||
// Run go build
|
||||
cmd = exec.Command("go", "build", "-o", outputExe)
|
||||
cmd.Dir = buildDir
|
||||
cmd.Stdout = os.Stdout
|
||||
cmd.Stderr = os.Stderr
|
||||
if err := cmd.Run(); err != nil {
|
||||
if err := ax.ExecDir(context.Background(), buildDir, "go", "build", "-o", outputExe); err != nil {
|
||||
return coreerr.E("pwa.runBuild", i18n.T("cmd.build.from_path.error.go_build"), err)
|
||||
}
|
||||
|
||||
fmt.Printf("\n%s %s/%s\n", i18n.T("cmd.build.from_path.success"), buildDir, outputExe)
|
||||
core.Println()
|
||||
core.Print(nil, "%s %s/%s", i18n.T("cmd.build.from_path.success"), buildDir, outputExe)
|
||||
return nil
|
||||
}
|
||||
|
||||
// copyDir recursively copies a directory from src to dst.
|
||||
func copyDir(src, dst string) error {
|
||||
return filepath.Walk(src, func(path string, info os.FileInfo, err error) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
relPath, err := filepath.Rel(src, path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
dstPath := filepath.Join(dst, relPath)
|
||||
|
||||
if info.IsDir() {
|
||||
return coreio.Local.EnsureDir(dstPath)
|
||||
}
|
||||
|
||||
srcFile, err := os.Open(path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer func() { _ = srcFile.Close() }()
|
||||
|
||||
dstFile, err := os.Create(dstPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer func() { _ = dstFile.Close() }()
|
||||
|
||||
_, err = io.Copy(dstFile, srcFile)
|
||||
if err := coreio.Local.EnsureDir(dst); err != nil {
|
||||
return err
|
||||
})
|
||||
}
|
||||
|
||||
entries, err := coreio.Local.List(src)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, entry := range entries {
|
||||
srcPath := ax.Join(src, entry.Name())
|
||||
dstPath := ax.Join(dst, entry.Name())
|
||||
|
||||
if entry.IsDir() {
|
||||
if err := copyDir(srcPath, dstPath); err != nil {
|
||||
return err
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
srcFile, err := ax.Open(srcPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
dstFile, err := ax.Create(dstPath)
|
||||
if err != nil {
|
||||
_ = srcFile.Close()
|
||||
return err
|
||||
}
|
||||
|
||||
if _, err := io.Copy(dstFile, srcFile); err != nil {
|
||||
_ = srcFile.Close()
|
||||
_ = dstFile.Close()
|
||||
return err
|
||||
}
|
||||
if err := srcFile.Close(); err != nil {
|
||||
_ = dstFile.Close()
|
||||
return err
|
||||
}
|
||||
if err := dstFile.Close(); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,8 +4,8 @@ package buildcmd
|
|||
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
|
||||
"dappco.re/go/core/build/internal/ax"
|
||||
"dappco.re/go/core/build/pkg/release"
|
||||
"dappco.re/go/core/i18n"
|
||||
coreerr "dappco.re/go/core/log"
|
||||
|
|
@ -40,6 +40,7 @@ func initReleaseFlags() {
|
|||
}
|
||||
|
||||
// AddReleaseCommand adds the release subcommand to the build command.
|
||||
// Usage example: call buildcmd.AddReleaseCommand(...) from integrating code.
|
||||
func AddReleaseCommand(buildCmd *cli.Command) {
|
||||
setReleaseI18n()
|
||||
initReleaseFlags()
|
||||
|
|
@ -49,7 +50,7 @@ func AddReleaseCommand(buildCmd *cli.Command) {
|
|||
// runRelease executes the full release workflow: build + archive + checksum + publish.
|
||||
func runRelease(ctx context.Context, dryRun bool, version string, draft, prerelease bool) error {
|
||||
// Get current directory
|
||||
projectDir, err := os.Getwd()
|
||||
projectDir, err := ax.Getwd()
|
||||
if err != nil {
|
||||
return coreerr.E("release", "get working directory", err)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,20 +7,20 @@ package buildcmd
|
|||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"dappco.re/go/core"
|
||||
"dappco.re/go/core/build/internal/ax"
|
||||
"dappco.re/go/core/build/pkg/sdk"
|
||||
"dappco.re/go/core/i18n"
|
||||
coreerr "dappco.re/go/core/log"
|
||||
"forge.lthn.ai/core/cli/pkg/cli"
|
||||
)
|
||||
|
||||
// runBuildSDK handles the `core build sdk` command.
|
||||
func runBuildSDK(specPath, lang, version string, dryRun bool) error {
|
||||
ctx := context.Background()
|
||||
|
||||
projectDir, err := os.Getwd()
|
||||
projectDir, err := ax.Getwd()
|
||||
if err != nil {
|
||||
return coreerr.E("build.SDK", "failed to get working directory", err)
|
||||
}
|
||||
|
|
@ -36,48 +36,48 @@ func runBuildSDK(specPath, lang, version string, dryRun bool) error {
|
|||
s.SetVersion(version)
|
||||
}
|
||||
|
||||
fmt.Printf("%s %s\n", buildHeaderStyle.Render(i18n.T("cmd.build.sdk.label")), i18n.T("cmd.build.sdk.generating"))
|
||||
cli.Print("%s %s\n", buildHeaderStyle.Render(i18n.T("cmd.build.sdk.label")), i18n.T("cmd.build.sdk.generating"))
|
||||
if dryRun {
|
||||
fmt.Printf(" %s\n", buildDimStyle.Render(i18n.T("cmd.build.sdk.dry_run_mode")))
|
||||
cli.Print(" %s\n", buildDimStyle.Render(i18n.T("cmd.build.sdk.dry_run_mode")))
|
||||
}
|
||||
fmt.Println()
|
||||
cli.Blank()
|
||||
|
||||
// Detect spec
|
||||
detectedSpec, err := s.DetectSpec()
|
||||
if err != nil {
|
||||
fmt.Printf("%s %v\n", buildErrorStyle.Render(i18n.T("common.label.error")), err)
|
||||
cli.Print("%s %v\n", buildErrorStyle.Render(i18n.T("common.label.error")), err)
|
||||
return err
|
||||
}
|
||||
fmt.Printf(" %s %s\n", i18n.T("common.label.spec"), buildTargetStyle.Render(detectedSpec))
|
||||
cli.Print(" %s %s\n", i18n.T("common.label.spec"), buildTargetStyle.Render(detectedSpec))
|
||||
|
||||
if dryRun {
|
||||
if lang != "" {
|
||||
fmt.Printf(" %s %s\n", i18n.T("cmd.build.sdk.language_label"), buildTargetStyle.Render(lang))
|
||||
cli.Print(" %s %s\n", i18n.T("cmd.build.sdk.language_label"), buildTargetStyle.Render(lang))
|
||||
} else {
|
||||
fmt.Printf(" %s %s\n", i18n.T("cmd.build.sdk.languages_label"), buildTargetStyle.Render(strings.Join(config.Languages, ", ")))
|
||||
cli.Print(" %s %s\n", i18n.T("cmd.build.sdk.languages_label"), buildTargetStyle.Render(core.Join(", ", config.Languages...)))
|
||||
}
|
||||
fmt.Println()
|
||||
fmt.Printf("%s %s\n", buildSuccessStyle.Render(i18n.T("cmd.build.label.ok")), i18n.T("cmd.build.sdk.would_generate"))
|
||||
cli.Blank()
|
||||
cli.Print("%s %s\n", buildSuccessStyle.Render(i18n.T("cmd.build.label.ok")), i18n.T("cmd.build.sdk.would_generate"))
|
||||
return nil
|
||||
}
|
||||
|
||||
if lang != "" {
|
||||
// Generate single language
|
||||
if err := s.GenerateLanguage(ctx, lang); err != nil {
|
||||
fmt.Printf("%s %v\n", buildErrorStyle.Render(i18n.T("common.label.error")), err)
|
||||
cli.Print("%s %v\n", buildErrorStyle.Render(i18n.T("common.label.error")), err)
|
||||
return err
|
||||
}
|
||||
fmt.Printf(" %s %s\n", i18n.T("cmd.build.sdk.generated_label"), buildTargetStyle.Render(lang))
|
||||
cli.Print(" %s %s\n", i18n.T("cmd.build.sdk.generated_label"), buildTargetStyle.Render(lang))
|
||||
} else {
|
||||
// Generate all
|
||||
if err := s.Generate(ctx); err != nil {
|
||||
fmt.Printf("%s %v\n", buildErrorStyle.Render(i18n.T("common.label.error")), err)
|
||||
cli.Print("%s %v\n", buildErrorStyle.Render(i18n.T("common.label.error")), err)
|
||||
return err
|
||||
}
|
||||
fmt.Printf(" %s %s\n", i18n.T("cmd.build.sdk.generated_label"), buildTargetStyle.Render(strings.Join(config.Languages, ", ")))
|
||||
cli.Print(" %s %s\n", i18n.T("cmd.build.sdk.generated_label"), buildTargetStyle.Render(core.Join(", ", config.Languages...)))
|
||||
}
|
||||
|
||||
fmt.Println()
|
||||
fmt.Printf("%s %s\n", buildSuccessStyle.Render(i18n.T("common.label.success")), i18n.T("cmd.build.sdk.complete"))
|
||||
cli.Blank()
|
||||
cli.Print("%s %s\n", buildSuccessStyle.Render(i18n.T("common.label.success")), i18n.T("cmd.build.sdk.complete"))
|
||||
return nil
|
||||
}
|
||||
|
|
|
|||
19
cmd/ci/ci.go
19
cmd/ci/ci.go
|
|
@ -2,10 +2,9 @@ package ci
|
|||
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
"os/exec"
|
||||
"strings"
|
||||
|
||||
"dappco.re/go/core"
|
||||
"dappco.re/go/core/build/internal/ax"
|
||||
"dappco.re/go/core/build/pkg/release"
|
||||
"dappco.re/go/core/i18n"
|
||||
coreerr "dappco.re/go/core/log"
|
||||
|
|
@ -96,7 +95,7 @@ func initCIFlags() {
|
|||
func runCIPublish(dryRun bool, version string, draft, prerelease bool) error {
|
||||
ctx := context.Background()
|
||||
|
||||
projectDir, err := os.Getwd()
|
||||
projectDir, err := ax.Getwd()
|
||||
if err != nil {
|
||||
return cli.WrapVerb(err, "get", "working directory")
|
||||
}
|
||||
|
|
@ -155,7 +154,7 @@ func runCIPublish(dryRun bool, version string, draft, prerelease bool) error {
|
|||
|
||||
// runCIReleaseInit scaffolds a release config.
|
||||
func runCIReleaseInit() error {
|
||||
cwd, err := os.Getwd()
|
||||
cwd, err := ax.Getwd()
|
||||
if err != nil {
|
||||
return cli.Err("%s: %w", i18n.T("i18n.fail.get", "working directory"), err)
|
||||
}
|
||||
|
|
@ -184,7 +183,7 @@ func runCIReleaseInit() error {
|
|||
|
||||
// runChangelog generates a changelog between two git refs.
|
||||
func runChangelog(fromRef, toRef string) error {
|
||||
cwd, err := os.Getwd()
|
||||
cwd, err := ax.Getwd()
|
||||
if err != nil {
|
||||
return cli.Err("%s: %w", i18n.T("i18n.fail.get", "working directory"), err)
|
||||
}
|
||||
|
|
@ -217,7 +216,7 @@ func runChangelog(fromRef, toRef string) error {
|
|||
|
||||
// runCIReleaseVersion shows the determined version.
|
||||
func runCIReleaseVersion() error {
|
||||
projectDir, err := os.Getwd()
|
||||
projectDir, err := ax.Getwd()
|
||||
if err != nil {
|
||||
return cli.WrapVerb(err, "get", "working directory")
|
||||
}
|
||||
|
|
@ -232,11 +231,9 @@ func runCIReleaseVersion() error {
|
|||
}
|
||||
|
||||
func latestTag(dir string) (string, error) {
|
||||
cmd := exec.Command("git", "describe", "--tags", "--abbrev=0")
|
||||
cmd.Dir = dir
|
||||
out, err := cmd.Output()
|
||||
out, err := ax.RunDir(context.Background(), dir, "git", "describe", "--tags", "--abbrev=0")
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return strings.TrimSpace(string(out)), nil
|
||||
return core.Trim(out), nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@ func init() {
|
|||
}
|
||||
|
||||
// AddCICommands registers the 'ci' command and all subcommands.
|
||||
// Usage example: call ci.AddCICommands(...) from integrating code.
|
||||
func AddCICommands(root *cli.Command) {
|
||||
setCII18n()
|
||||
initCIFlags()
|
||||
|
|
|
|||
|
|
@ -8,8 +8,7 @@
|
|||
package sdkcmd
|
||||
|
||||
import (
|
||||
"os"
|
||||
|
||||
"dappco.re/go/core/build/internal/ax"
|
||||
"dappco.re/go/core/build/pkg/sdk"
|
||||
"dappco.re/go/core/i18n"
|
||||
coreerr "dappco.re/go/core/log"
|
||||
|
|
@ -61,6 +60,7 @@ func setSDKI18n() {
|
|||
}
|
||||
|
||||
// AddSDKCommands registers the 'sdk' command and all subcommands.
|
||||
// Usage example: call sdkcmd.AddSDKCommands(...) from integrating code.
|
||||
func AddSDKCommands(root *cli.Command) {
|
||||
setSDKI18n()
|
||||
|
||||
|
|
@ -79,7 +79,7 @@ func AddSDKCommands(root *cli.Command) {
|
|||
}
|
||||
|
||||
func runSDKDiff(basePath, specPath string) error {
|
||||
projectDir, err := os.Getwd()
|
||||
projectDir, err := ax.Getwd()
|
||||
if err != nil {
|
||||
return coreerr.E("sdk.Diff", "failed to get working directory", err)
|
||||
}
|
||||
|
|
@ -119,7 +119,7 @@ func runSDKDiff(basePath, specPath string) error {
|
|||
}
|
||||
|
||||
func runSDKValidate(specPath string) error {
|
||||
projectDir, err := os.Getwd()
|
||||
projectDir, err := ax.Getwd()
|
||||
if err != nil {
|
||||
return coreerr.E("sdk.Validate", "failed to get working directory", err)
|
||||
}
|
||||
|
|
|
|||
20
go.mod
20
go.mod
|
|
@ -3,11 +3,13 @@ module dappco.re/go/core/build
|
|||
go 1.26.0
|
||||
|
||||
require (
|
||||
dappco.re/go/core/api v0.1.5
|
||||
dappco.re/go/core/i18n v0.1.7
|
||||
dappco.re/go/core v0.8.0-alpha.1
|
||||
dappco.re/go/core/api v0.2.0
|
||||
dappco.re/go/core/i18n v0.2.0
|
||||
dappco.re/go/core/io v0.2.0
|
||||
dappco.re/go/core/log v0.1.0
|
||||
dappco.re/go/core/ws v0.2.5
|
||||
dappco.re/go/core/process v0.3.0
|
||||
dappco.re/go/core/ws v0.3.0
|
||||
forge.lthn.ai/core/cli v0.3.7
|
||||
github.com/Snider/Borg v0.2.0
|
||||
github.com/gin-gonic/gin v1.12.0
|
||||
|
|
@ -23,7 +25,6 @@ require (
|
|||
|
||||
require (
|
||||
cloud.google.com/go v0.123.0 // indirect
|
||||
dappco.re/go/core v0.5.0 // indirect
|
||||
forge.lthn.ai/core/go v0.3.2 // indirect
|
||||
forge.lthn.ai/core/go-i18n v0.1.7 // indirect
|
||||
forge.lthn.ai/core/go-inference v0.1.7 // indirect
|
||||
|
|
@ -155,14 +156,3 @@ require (
|
|||
golang.org/x/tools v0.43.0 // indirect
|
||||
google.golang.org/protobuf v1.36.11 // indirect
|
||||
)
|
||||
|
||||
replace (
|
||||
dappco.re/go/core => /Users/snider/Code/core/go
|
||||
dappco.re/go/core/api => /Users/snider/Code/core/api
|
||||
dappco.re/go/core/i18n => /Users/snider/Code/core/go-i18n
|
||||
dappco.re/go/core/io => /Users/snider/Code/core/go-io
|
||||
dappco.re/go/core/log => /Users/snider/Code/core/go-log
|
||||
dappco.re/go/core/ws => /Users/snider/Code/core/go-ws
|
||||
forge.lthn.ai/core/cli => /Users/snider/Code/core/cli
|
||||
forge.lthn.ai/core/go-inference => /Users/snider/Code/core/go-inference
|
||||
)
|
||||
|
|
|
|||
18
go.sum
18
go.sum
|
|
@ -1,9 +1,27 @@
|
|||
cloud.google.com/go v0.123.0 h1:2NAUJwPR47q+E35uaJeYoNhuNEM9kM8SjgRgdeOJUSE=
|
||||
cloud.google.com/go v0.123.0/go.mod h1:xBoMV08QcqUGuPW65Qfm1o9Y4zKZBpGS+7bImXLTAZU=
|
||||
dappco.re/go/core v0.8.0-alpha.1 h1:gj7+Scv+L63Z7wMxbJYHhaRFkHJo2u4MMPuUSv/Dhtk=
|
||||
dappco.re/go/core v0.8.0-alpha.1/go.mod h1:f2/tBZ3+3IqDrg2F5F598llv0nmb/4gJVCFzM5geE4A=
|
||||
dappco.re/go/core/api v0.2.0 h1:5OcN9nawpp18Jp6dB1OwI2CBfs0Tacb0y0zqxFB6TJ0=
|
||||
dappco.re/go/core/api v0.2.0/go.mod h1:AtgNAx8lDY+qhVObFdNQOjSUQrHX1BeiDdMuA6RIfzo=
|
||||
dappco.re/go/core/i18n v0.2.0 h1:NHzk6RCU93/qVRA3f2jvMr9P1R6FYheR/sHL+TnvKbI=
|
||||
dappco.re/go/core/i18n v0.2.0/go.mod h1:9eSVJXr3OpIGWQvDynfhqcp27xnLMwlYLgsByU+p7ok=
|
||||
dappco.re/go/core/io v0.2.0 h1:zuudgIiTsQQ5ipVt97saWdGLROovbEB/zdVyy9/l+I4=
|
||||
dappco.re/go/core/io v0.2.0/go.mod h1:1QnQV6X9LNgFKfm8SkOtR9LLaj3bDcsOIeJOOyjbL5E=
|
||||
dappco.re/go/core/log v0.1.0 h1:pa71Vq2TD2aoEUQWFKwNcaJ3GBY8HbaNGqtE688Unyc=
|
||||
dappco.re/go/core/log v0.1.0/go.mod h1:Nkqb8gsXhZAO8VLpx7B8i1iAmohhzqA20b9Zr8VUcJs=
|
||||
dappco.re/go/core/process v0.3.0 h1:BPF9R79+8ZWe34qCIy/sZy+P4HwbaO95js2oPJL7IqM=
|
||||
dappco.re/go/core/process v0.3.0/go.mod h1:qwx8kt6x+J9gn7fu8lavuess72Ye9jPBODqDZQ9K0as=
|
||||
dappco.re/go/core/ws v0.3.0 h1:ZxR8y5pfrWvnCHVN7qExXz7fdP5a063uNqyqE0Ab8pQ=
|
||||
dappco.re/go/core/ws v0.3.0/go.mod h1:aLyXrJnbCOGL0SW9rC1EHAAIS83w3djO374gHIz4Nic=
|
||||
forge.lthn.ai/core/cli v0.3.7 h1:1GrbaGg0wDGHr6+klSbbGyN/9sSbHvFbdySJznymhwg=
|
||||
forge.lthn.ai/core/cli v0.3.7/go.mod h1:DBUppJkA9P45ZFGgI2B8VXw1rAZxamHoI/KG7fRvTNs=
|
||||
forge.lthn.ai/core/go v0.3.2 h1:VB9pW6ggqBhe438cjfE2iSI5Lg+62MmRbaOFglZM+nQ=
|
||||
forge.lthn.ai/core/go v0.3.2/go.mod h1:f7/zb3Labn4ARfwTq5Bi2AFHY+uxyPHozO+hLb54eFo=
|
||||
forge.lthn.ai/core/go-i18n v0.1.7 h1:aHkAoc3W8fw3RPNvw/UszQbjyFWXHszzbZgty3SwyAA=
|
||||
forge.lthn.ai/core/go-i18n v0.1.7/go.mod h1:0VDjwtY99NSj2iqwrI09h5GUsJeM9s48MLkr+/Dn4G8=
|
||||
forge.lthn.ai/core/go-inference v0.1.7 h1:9Dy6v03jX5ZRH3n5iTzlYyGtucuBIgSe+S7GWvBzx9Q=
|
||||
forge.lthn.ai/core/go-inference v0.1.7/go.mod h1:jfWz+IJX55wAH98+ic6FEqqGB6/P31CHlg7VY7pxREw=
|
||||
forge.lthn.ai/core/go-log v0.0.4 h1:KTuCEPgFmuM8KJfnyQ8vPOU1Jg654W74h8IJvfQMfv0=
|
||||
forge.lthn.ai/core/go-log v0.0.4/go.mod h1:r14MXKOD3LF/sI8XUJQhRk/SZHBE7jAFVuCfgkXoZPw=
|
||||
github.com/99designs/gqlgen v0.17.88 h1:neMQDgehMwT1vYIOx/w5ZYPUU/iMNAJzRO44I5Intoc=
|
||||
|
|
|
|||
408
internal/ax/ax.go
Normal file
408
internal/ax/ax.go
Normal file
|
|
@ -0,0 +1,408 @@
|
|||
package ax
|
||||
|
||||
import (
|
||||
"context"
|
||||
"io"
|
||||
"io/fs"
|
||||
"os"
|
||||
"runtime"
|
||||
"syscall"
|
||||
|
||||
"dappco.re/go/core"
|
||||
coreio "dappco.re/go/core/io"
|
||||
coreerr "dappco.re/go/core/log"
|
||||
process "dappco.re/go/core/process"
|
||||
processexec "dappco.re/go/core/process/exec"
|
||||
)
|
||||
|
||||
// DS returns the current platform directory separator.
|
||||
//
|
||||
// Usage example: read ax.DS() when building Core-aware filesystem paths.
|
||||
func DS() string {
|
||||
if sep := core.Env("DS"); sep != "" {
|
||||
return sep
|
||||
}
|
||||
if runtime.GOOS == "windows" {
|
||||
return "\\"
|
||||
}
|
||||
return "/"
|
||||
}
|
||||
|
||||
// Clean normalises a filesystem path using Core path primitives.
|
||||
//
|
||||
// Usage example: clean := ax.Clean("./dist/../dist/output")
|
||||
func Clean(path string) string {
|
||||
return core.CleanPath(path, DS())
|
||||
}
|
||||
|
||||
// Join combines path segments without relying on path/filepath.
|
||||
//
|
||||
// Usage example: path := ax.Join(projectDir, ".core", "build.yaml")
|
||||
func Join(parts ...string) string {
|
||||
return Clean(core.Join(DS(), parts...))
|
||||
}
|
||||
|
||||
// Abs resolves a path against the current working directory.
|
||||
//
|
||||
// Usage example: abs, err := ax.Abs("./testdata")
|
||||
func Abs(path string) (string, error) {
|
||||
if core.PathIsAbs(path) {
|
||||
return Clean(path), nil
|
||||
}
|
||||
|
||||
cwd, err := Getwd()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return Join(cwd, path), nil
|
||||
}
|
||||
|
||||
// Rel returns target relative to base when target is inside base.
|
||||
//
|
||||
// Usage example: rel, err := ax.Rel(projectDir, artifactPath)
|
||||
func Rel(base, target string) (string, error) {
|
||||
base = Clean(base)
|
||||
target = Clean(target)
|
||||
|
||||
if base == target {
|
||||
return ".", nil
|
||||
}
|
||||
|
||||
prefix := base
|
||||
if !core.HasSuffix(prefix, DS()) {
|
||||
prefix = core.Concat(prefix, DS())
|
||||
}
|
||||
|
||||
if core.HasPrefix(target, prefix) {
|
||||
return core.TrimPrefix(target, prefix), nil
|
||||
}
|
||||
|
||||
return "", coreerr.E("ax.Rel", "path is outside base: "+target, nil)
|
||||
}
|
||||
|
||||
// Base returns the last path element.
|
||||
//
|
||||
// Usage example: name := ax.Base("/tmp/dist/app.tar.gz")
|
||||
func Base(path string) string {
|
||||
return core.PathBase(path)
|
||||
}
|
||||
|
||||
// Dir returns the parent directory for a path.
|
||||
//
|
||||
// Usage example: dir := ax.Dir("/tmp/dist/app.tar.gz")
|
||||
func Dir(path string) string {
|
||||
return core.PathDir(path)
|
||||
}
|
||||
|
||||
// Ext returns the filename extension including the dot.
|
||||
//
|
||||
// Usage example: ext := ax.Ext("app.tar.gz")
|
||||
func Ext(path string) string {
|
||||
return core.PathExt(path)
|
||||
}
|
||||
|
||||
// IsAbs reports whether a path is absolute.
|
||||
//
|
||||
// Usage example: if ax.IsAbs(outputDir) { ... }
|
||||
func IsAbs(path string) bool {
|
||||
return core.PathIsAbs(path)
|
||||
}
|
||||
|
||||
// FromSlash rewrites slash-separated paths for the current platform.
|
||||
//
|
||||
// Usage example: path := ax.FromSlash("ui/dist/index.html")
|
||||
func FromSlash(path string) string {
|
||||
if DS() == "/" {
|
||||
return path
|
||||
}
|
||||
return core.Replace(path, "/", DS())
|
||||
}
|
||||
|
||||
// Getwd returns the current working directory from Core environment metadata.
|
||||
//
|
||||
// Usage example: cwd, err := ax.Getwd()
|
||||
func Getwd() (string, error) {
|
||||
cwd := core.Env("DIR_CWD")
|
||||
if cwd == "" {
|
||||
wd, err := os.Getwd()
|
||||
if err != nil {
|
||||
return "", coreerr.E("ax.Getwd", "failed to get current working directory", err)
|
||||
}
|
||||
return wd, nil
|
||||
}
|
||||
return cwd, nil
|
||||
}
|
||||
|
||||
// TempDir creates a temporary directory via Core's filesystem primitive.
|
||||
//
|
||||
// Usage example: dir, err := ax.TempDir("core-build-*")
|
||||
func TempDir(prefix string) (string, error) {
|
||||
dir := (&core.Fs{}).NewUnrestricted().TempDir(prefix)
|
||||
if dir == "" {
|
||||
return "", coreerr.E("ax.TempDir", "failed to create temporary directory", nil)
|
||||
}
|
||||
return dir, nil
|
||||
}
|
||||
|
||||
// ReadFile reads a file into bytes via io.Local.
|
||||
//
|
||||
// Usage example: data, err := ax.ReadFile("go.mod")
|
||||
func ReadFile(path string) ([]byte, error) {
|
||||
content, err := coreio.Local.Read(path)
|
||||
if err != nil {
|
||||
return nil, coreerr.E("ax.ReadFile", "failed to read file "+path, err)
|
||||
}
|
||||
return []byte(content), nil
|
||||
}
|
||||
|
||||
// WriteFile writes bytes via io.Local with an explicit mode.
|
||||
//
|
||||
// Usage example: err := ax.WriteFile("README.md", []byte("hi"), 0o644)
|
||||
func WriteFile(path string, data []byte, mode fs.FileMode) error {
|
||||
if err := coreio.Local.WriteMode(path, string(data), mode); err != nil {
|
||||
return coreerr.E("ax.WriteFile", "failed to write file "+path, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// WriteString writes text via io.Local with an explicit mode.
|
||||
//
|
||||
// Usage example: err := ax.WriteString("README.md", "hi", 0o644)
|
||||
func WriteString(path, data string, mode fs.FileMode) error {
|
||||
if err := coreio.Local.WriteMode(path, data, mode); err != nil {
|
||||
return coreerr.E("ax.WriteString", "failed to write file "+path, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// MkdirAll ensures a directory exists.
|
||||
//
|
||||
// Usage example: err := ax.MkdirAll("dist/linux_arm64", 0o755)
|
||||
func MkdirAll(path string, mode fs.FileMode) error {
|
||||
if mode == 0 {
|
||||
mode = 0o755
|
||||
}
|
||||
if err := os.MkdirAll(path, mode); err != nil {
|
||||
return coreerr.E("ax.MkdirAll", "failed to create directory "+path, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Mkdir ensures a directory exists.
|
||||
//
|
||||
// Usage example: err := ax.Mkdir(".core", 0o755)
|
||||
func Mkdir(path string, mode fs.FileMode) error {
|
||||
return MkdirAll(path, mode)
|
||||
}
|
||||
|
||||
// RemoveAll removes a file or directory tree.
|
||||
//
|
||||
// Usage example: err := ax.RemoveAll("dist")
|
||||
func RemoveAll(path string) error {
|
||||
if err := coreio.Local.DeleteAll(path); err != nil {
|
||||
return coreerr.E("ax.RemoveAll", "failed to remove path "+path, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Stat returns file metadata from io.Local.
|
||||
//
|
||||
// Usage example: info, err := ax.Stat("go.mod")
|
||||
func Stat(path string) (fs.FileInfo, error) {
|
||||
info, err := coreio.Local.Stat(path)
|
||||
if err != nil {
|
||||
return nil, coreerr.E("ax.Stat", "failed to stat path "+path, err)
|
||||
}
|
||||
return info, nil
|
||||
}
|
||||
|
||||
// ReadDir lists directory entries via io.Local.
|
||||
//
|
||||
// Usage example: entries, err := ax.ReadDir("dist")
|
||||
func ReadDir(path string) ([]fs.DirEntry, error) {
|
||||
entries, err := coreio.Local.List(path)
|
||||
if err != nil {
|
||||
return nil, coreerr.E("ax.ReadDir", "failed to list directory "+path, err)
|
||||
}
|
||||
return entries, nil
|
||||
}
|
||||
|
||||
// Open opens a file for reading via io.Local.
|
||||
//
|
||||
// Usage example: file, err := ax.Open("README.md")
|
||||
func Open(path string) (fs.File, error) {
|
||||
file, err := coreio.Local.Open(path)
|
||||
if err != nil {
|
||||
return nil, coreerr.E("ax.Open", "failed to open file "+path, err)
|
||||
}
|
||||
return file, nil
|
||||
}
|
||||
|
||||
// Create opens a file for writing via io.Local.
|
||||
//
|
||||
// Usage example: file, err := ax.Create("dist/output.txt")
|
||||
func Create(path string) (io.WriteCloser, error) {
|
||||
file, err := coreio.Local.Create(path)
|
||||
if err != nil {
|
||||
return nil, coreerr.E("ax.Create", "failed to create file "+path, err)
|
||||
}
|
||||
return file, nil
|
||||
}
|
||||
|
||||
// Exists reports whether a path exists.
|
||||
//
|
||||
// Usage example: if ax.Exists("dist") { ... }
|
||||
func Exists(path string) bool {
|
||||
return coreio.Local.Exists(path)
|
||||
}
|
||||
|
||||
// IsFile reports whether a path is a regular file.
|
||||
//
|
||||
// Usage example: if ax.IsFile("go.mod") { ... }
|
||||
func IsFile(path string) bool {
|
||||
return coreio.Local.IsFile(path)
|
||||
}
|
||||
|
||||
// IsDir reports whether a path is a directory.
|
||||
//
|
||||
// Usage example: if ax.IsDir(".core") { ... }
|
||||
func IsDir(path string) bool {
|
||||
return coreio.Local.IsDir(path)
|
||||
}
|
||||
|
||||
// Chmod updates file permissions without importing os.
|
||||
//
|
||||
// Usage example: err := ax.Chmod("dist/app", 0o755)
|
||||
func Chmod(path string, mode fs.FileMode) error {
|
||||
if err := os.Chmod(path, mode); err != nil {
|
||||
return coreerr.E("ax.Chmod", "failed to change permissions on "+path, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Getuid returns the current process UID.
|
||||
//
|
||||
// Usage example: uid := ax.Getuid()
|
||||
func Getuid() int {
|
||||
return syscall.Getuid()
|
||||
}
|
||||
|
||||
// Getgid returns the current process GID.
|
||||
//
|
||||
// Usage example: gid := ax.Getgid()
|
||||
func Getgid() int {
|
||||
return syscall.Getgid()
|
||||
}
|
||||
|
||||
// Geteuid returns the effective UID.
|
||||
//
|
||||
// Usage example: if ax.Geteuid() == 0 { ... }
|
||||
func Geteuid() int {
|
||||
return syscall.Geteuid()
|
||||
}
|
||||
|
||||
// JSONMarshal returns a JSON string using Core's JSON wrapper.
|
||||
//
|
||||
// Usage example: data, err := ax.JSONMarshal(cfg)
|
||||
func JSONMarshal(value any) (string, error) {
|
||||
result := core.JSONMarshal(value)
|
||||
if !result.OK {
|
||||
err, ok := result.Value.(error)
|
||||
if !ok {
|
||||
return "", coreerr.E("ax.JSONMarshal", "failed to marshal JSON", nil)
|
||||
}
|
||||
return "", coreerr.E("ax.JSONMarshal", "failed to marshal JSON", err)
|
||||
}
|
||||
encoded, ok := result.Value.([]byte)
|
||||
if !ok {
|
||||
return "", coreerr.E("ax.JSONMarshal", "failed to marshal JSON", nil)
|
||||
}
|
||||
return string(encoded), nil
|
||||
}
|
||||
|
||||
// JSONUnmarshal decodes JSON into target using Core's JSON wrapper.
|
||||
//
|
||||
// Usage example: err := ax.JSONUnmarshal(data, &cfg)
|
||||
func JSONUnmarshal(data []byte, target any) error {
|
||||
result := core.JSONUnmarshal(data, target)
|
||||
if !result.OK {
|
||||
err, ok := result.Value.(error)
|
||||
if !ok {
|
||||
return coreerr.E("ax.JSONUnmarshal", "failed to unmarshal JSON", nil)
|
||||
}
|
||||
return coreerr.E("ax.JSONUnmarshal", "failed to unmarshal JSON", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// LookPath resolves a program on PATH via the Core process package.
|
||||
//
|
||||
// Usage example: path, err := ax.LookPath("git")
|
||||
func LookPath(name string) (string, error) {
|
||||
program := process.Program{Name: name}
|
||||
if err := program.Find(); err != nil {
|
||||
return "", coreerr.E("ax.LookPath", "failed to locate command "+name, err)
|
||||
}
|
||||
return program.Path, nil
|
||||
}
|
||||
|
||||
// Run executes a command and returns trimmed combined output.
|
||||
//
|
||||
// Usage example: output, err := ax.Run(ctx, "git", "status", "--short")
|
||||
func Run(ctx context.Context, command string, args ...string) (string, error) {
|
||||
program := process.Program{Name: command}
|
||||
return program.Run(ctx, args...)
|
||||
}
|
||||
|
||||
// RunDir executes a command in the provided directory and returns combined output.
|
||||
//
|
||||
// Usage example: output, err := ax.RunDir(ctx, repoDir, "git", "log", "--oneline")
|
||||
func RunDir(ctx context.Context, dir, command string, args ...string) (string, error) {
|
||||
program := process.Program{Name: command}
|
||||
return program.RunDir(ctx, dir, args...)
|
||||
}
|
||||
|
||||
// Exec executes a command without capturing output.
|
||||
//
|
||||
// Usage example: err := ax.Exec(ctx, "go", "test", "./...")
|
||||
func Exec(ctx context.Context, command string, args ...string) error {
|
||||
return processexec.Command(ctx, command, args...).Run()
|
||||
}
|
||||
|
||||
// ExecDir executes a command in a specific directory without capturing output.
|
||||
//
|
||||
// Usage example: err := ax.ExecDir(ctx, repoDir, "go", "test", "./...")
|
||||
func ExecDir(ctx context.Context, dir, command string, args ...string) error {
|
||||
return processexec.Command(ctx, command, args...).WithDir(dir).Run()
|
||||
}
|
||||
|
||||
// ExecWithEnv executes a command with additional environment variables.
|
||||
//
|
||||
// Usage example: err := ax.ExecWithEnv(ctx, repoDir, []string{"GOOS=linux"}, "go", "build")
|
||||
func ExecWithEnv(ctx context.Context, dir string, env []string, command string, args ...string) error {
|
||||
return processexec.Command(ctx, command, args...).WithDir(dir).WithEnv(env).Run()
|
||||
}
|
||||
|
||||
// ExecWithWriters executes a command and streams output to the provided writers.
|
||||
//
|
||||
// Usage example: err := ax.ExecWithWriters(ctx, repoDir, nil, w, w, "docker", "build", ".")
|
||||
func ExecWithWriters(ctx context.Context, dir string, env []string, stdout, stderr io.Writer, command string, args ...string) error {
|
||||
cmd := processexec.Command(ctx, command, args...).WithDir(dir).WithEnv(env)
|
||||
if stdout != nil {
|
||||
cmd = cmd.WithStdout(stdout)
|
||||
}
|
||||
if stderr != nil {
|
||||
cmd = cmd.WithStderr(stderr)
|
||||
}
|
||||
return cmd.Run()
|
||||
}
|
||||
|
||||
// CombinedOutput executes a command and returns combined output.
|
||||
//
|
||||
// Usage example: output, err := ax.CombinedOutput(ctx, repoDir, nil, "go", "test", "./...")
|
||||
func CombinedOutput(ctx context.Context, dir string, env []string, command string, args ...string) (string, error) {
|
||||
cmd := processexec.Command(ctx, command, args...).WithDir(dir).WithEnv(env)
|
||||
output, err := cmd.CombinedOutput()
|
||||
return core.Trim(string(output)), err
|
||||
}
|
||||
|
|
@ -7,6 +7,8 @@ import (
|
|||
"dappco.re/go/core/i18n"
|
||||
)
|
||||
|
||||
// Usage example: use locales.FS from package consumers as needed.
|
||||
//
|
||||
//go:embed *.json
|
||||
var FS embed.FS
|
||||
|
||||
|
|
|
|||
|
|
@ -7,5 +7,7 @@ import "embed"
|
|||
// Assets holds the built UI bundle (core-build.js and related files).
|
||||
// The directory is populated by running `npm run build` in the ui/ directory.
|
||||
//
|
||||
// Usage example: use api.Assets from package consumers as needed.
|
||||
//
|
||||
//go:embed all:ui/dist
|
||||
var Assets embed.FS
|
||||
|
|
|
|||
|
|
@ -6,12 +6,12 @@
|
|||
package api
|
||||
|
||||
import (
|
||||
"io/fs"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"dappco.re/go/core/api"
|
||||
"dappco.re/go/core/api/pkg/provider"
|
||||
"dappco.re/go/core/build/internal/ax"
|
||||
"dappco.re/go/core/build/pkg/build"
|
||||
"dappco.re/go/core/build/pkg/build/builders"
|
||||
"dappco.re/go/core/build/pkg/release"
|
||||
|
|
@ -24,6 +24,7 @@ import (
|
|||
// BuildProvider wraps go-build's build, release, and SDK operations as a
|
||||
// service provider. It implements Provider, Streamable, Describable, and
|
||||
// Renderable.
|
||||
// Usage example: declare a value of type api.BuildProvider in integrating code.
|
||||
type BuildProvider struct {
|
||||
hub *ws.Hub
|
||||
projectDir string
|
||||
|
|
@ -41,6 +42,7 @@ var (
|
|||
// NewProvider creates a BuildProvider for the given project directory.
|
||||
// If projectDir is empty, the current working directory is used.
|
||||
// The WS hub is used to emit real-time build events; pass nil if not available.
|
||||
// Usage example: call api.NewProvider(...) from integrating code.
|
||||
func NewProvider(projectDir string, hub *ws.Hub) *BuildProvider {
|
||||
if projectDir == "" {
|
||||
projectDir = "."
|
||||
|
|
@ -53,12 +55,15 @@ func NewProvider(projectDir string, hub *ws.Hub) *BuildProvider {
|
|||
}
|
||||
|
||||
// Name implements api.RouteGroup.
|
||||
// Usage example: call value.Name(...) from integrating code.
|
||||
func (p *BuildProvider) Name() string { return "build" }
|
||||
|
||||
// BasePath implements api.RouteGroup.
|
||||
// Usage example: call value.BasePath(...) from integrating code.
|
||||
func (p *BuildProvider) BasePath() string { return "/api/v1/build" }
|
||||
|
||||
// Element implements provider.Renderable.
|
||||
// Usage example: call value.Element(...) from integrating code.
|
||||
func (p *BuildProvider) Element() provider.ElementSpec {
|
||||
return provider.ElementSpec{
|
||||
Tag: "core-build-panel",
|
||||
|
|
@ -67,6 +72,7 @@ func (p *BuildProvider) Element() provider.ElementSpec {
|
|||
}
|
||||
|
||||
// Channels implements provider.Streamable.
|
||||
// Usage example: call value.Channels(...) from integrating code.
|
||||
func (p *BuildProvider) Channels() []string {
|
||||
return []string{
|
||||
"build.started",
|
||||
|
|
@ -79,6 +85,7 @@ func (p *BuildProvider) Channels() []string {
|
|||
}
|
||||
|
||||
// RegisterRoutes implements api.RouteGroup.
|
||||
// Usage example: call value.RegisterRoutes(...) from integrating code.
|
||||
func (p *BuildProvider) RegisterRoutes(rg *gin.RouterGroup) {
|
||||
// Build
|
||||
rg.GET("/config", p.getConfig)
|
||||
|
|
@ -97,6 +104,7 @@ func (p *BuildProvider) RegisterRoutes(rg *gin.RouterGroup) {
|
|||
}
|
||||
|
||||
// Describe implements api.DescribableGroup.
|
||||
// Usage example: call value.Describe(...) from integrating code.
|
||||
func (p *BuildProvider) Describe() []api.RouteDescription {
|
||||
return []api.RouteDescription{
|
||||
{
|
||||
|
|
@ -180,7 +188,7 @@ func (p *BuildProvider) Describe() []api.RouteDescription {
|
|||
|
||||
// resolveDir returns the absolute project directory.
|
||||
func (p *BuildProvider) resolveDir() (string, error) {
|
||||
return filepath.Abs(p.projectDir)
|
||||
return ax.Abs(p.projectDir)
|
||||
}
|
||||
|
||||
// -- Build Handlers -----------------------------------------------------------
|
||||
|
|
@ -278,10 +286,10 @@ func (p *BuildProvider) triggerBuild(c *gin.Context) {
|
|||
binaryName = cfg.Project.Name
|
||||
}
|
||||
if binaryName == "" {
|
||||
binaryName = filepath.Base(dir)
|
||||
binaryName = ax.Base(dir)
|
||||
}
|
||||
|
||||
outputDir := filepath.Join(dir, "dist")
|
||||
outputDir := ax.Join(dir, "dist")
|
||||
|
||||
buildConfig := &build.Config{
|
||||
FS: p.medium,
|
||||
|
|
@ -348,7 +356,7 @@ func (p *BuildProvider) listArtifacts(c *gin.Context) {
|
|||
return
|
||||
}
|
||||
|
||||
distDir := filepath.Join(dir, "dist")
|
||||
distDir := ax.Join(dir, "dist")
|
||||
if !p.medium.IsDir(distDir) {
|
||||
c.JSON(http.StatusOK, api.OK(map[string]any{
|
||||
"artifacts": []artifactInfo{},
|
||||
|
|
@ -374,7 +382,7 @@ func (p *BuildProvider) listArtifacts(c *gin.Context) {
|
|||
}
|
||||
artifacts = append(artifacts, artifactInfo{
|
||||
Name: entry.Name(),
|
||||
Path: filepath.Join(distDir, entry.Name()),
|
||||
Path: ax.Join(distDir, entry.Name()),
|
||||
Size: info.Size(),
|
||||
})
|
||||
}
|
||||
|
|
@ -566,7 +574,7 @@ func getBuilder(projectType build.ProjectType) (build.Builder, error) {
|
|||
case build.ProjectTypeGo:
|
||||
return builders.NewGoBuilder(), nil
|
||||
default:
|
||||
return nil, os.ErrNotExist
|
||||
return nil, fs.ErrNotExist
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
package api
|
||||
|
||||
import (
|
||||
"os"
|
||||
"io/fs"
|
||||
"testing"
|
||||
|
||||
"dappco.re/go/core/build/pkg/build"
|
||||
|
|
@ -11,14 +11,14 @@ import (
|
|||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestBuildProvider_Good_Identity(t *testing.T) {
|
||||
func TestProvider_BuildProviderIdentity_Good(t *testing.T) {
|
||||
p := NewProvider(".", nil)
|
||||
|
||||
assert.Equal(t, "build", p.Name())
|
||||
assert.Equal(t, "/api/v1/build", p.BasePath())
|
||||
}
|
||||
|
||||
func TestBuildProvider_Good_Element(t *testing.T) {
|
||||
func TestProvider_BuildProviderElement_Good(t *testing.T) {
|
||||
p := NewProvider(".", nil)
|
||||
el := p.Element()
|
||||
|
||||
|
|
@ -26,7 +26,7 @@ func TestBuildProvider_Good_Element(t *testing.T) {
|
|||
assert.Equal(t, "/assets/core-build.js", el.Source)
|
||||
}
|
||||
|
||||
func TestBuildProvider_Good_Channels(t *testing.T) {
|
||||
func TestProvider_BuildProviderChannels_Good(t *testing.T) {
|
||||
p := NewProvider(".", nil)
|
||||
channels := p.Channels()
|
||||
|
||||
|
|
@ -39,7 +39,7 @@ func TestBuildProvider_Good_Channels(t *testing.T) {
|
|||
assert.Len(t, channels, 6)
|
||||
}
|
||||
|
||||
func TestBuildProvider_Good_Describe(t *testing.T) {
|
||||
func TestProvider_BuildProviderDescribe_Good(t *testing.T) {
|
||||
p := NewProvider(".", nil)
|
||||
routes := p.Describe()
|
||||
|
||||
|
|
@ -63,23 +63,23 @@ func TestBuildProvider_Good_Describe(t *testing.T) {
|
|||
assert.Equal(t, "POST", paths["/sdk/generate"])
|
||||
}
|
||||
|
||||
func TestBuildProvider_Good_DefaultProjectDir(t *testing.T) {
|
||||
func TestProvider_BuildProviderDefaultProjectDir_Good(t *testing.T) {
|
||||
p := NewProvider("", nil)
|
||||
assert.Equal(t, ".", p.projectDir)
|
||||
}
|
||||
|
||||
func TestBuildProvider_Good_CustomProjectDir(t *testing.T) {
|
||||
func TestProvider_BuildProviderCustomProjectDir_Good(t *testing.T) {
|
||||
p := NewProvider("/tmp/myproject", nil)
|
||||
assert.Equal(t, "/tmp/myproject", p.projectDir)
|
||||
}
|
||||
|
||||
func TestBuildProvider_Good_NilHub(t *testing.T) {
|
||||
func TestProvider_BuildProviderNilHub_Good(t *testing.T) {
|
||||
p := NewProvider(".", nil)
|
||||
// emitEvent should not panic with nil hub
|
||||
p.emitEvent("build.started", map[string]any{"test": true})
|
||||
}
|
||||
|
||||
func TestGetBuilder_Good_SupportedTypes(t *testing.T) {
|
||||
func TestProvider_GetBuilderSupportedTypes_Good(t *testing.T) {
|
||||
b, err := getBuilder(build.ProjectTypeGo)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "go", b.Name())
|
||||
|
|
@ -89,19 +89,19 @@ func TestGetBuilder_Good_SupportedTypes(t *testing.T) {
|
|||
assert.Equal(t, "wails", b.Name())
|
||||
}
|
||||
|
||||
func TestGetBuilder_Bad_UnsupportedType(t *testing.T) {
|
||||
func TestProvider_GetBuilderUnsupportedType_Bad(t *testing.T) {
|
||||
_, err := getBuilder(build.ProjectType("unknown"))
|
||||
assert.ErrorIs(t, err, os.ErrNotExist)
|
||||
assert.ErrorIs(t, err, fs.ErrNotExist)
|
||||
}
|
||||
|
||||
func TestBuildProvider_Good_ResolveDir(t *testing.T) {
|
||||
func TestProvider_BuildProviderResolveDir_Good(t *testing.T) {
|
||||
p := NewProvider("/tmp", nil)
|
||||
dir, err := p.resolveDir()
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "/tmp", dir)
|
||||
}
|
||||
|
||||
func TestBuildProvider_Good_ResolveDirRelative(t *testing.T) {
|
||||
func TestProvider_BuildProviderResolveDirRelative_Good(t *testing.T) {
|
||||
p := NewProvider(".", nil)
|
||||
dir, err := p.resolveDir()
|
||||
require.NoError(t, err)
|
||||
|
|
@ -109,7 +109,7 @@ func TestBuildProvider_Good_ResolveDirRelative(t *testing.T) {
|
|||
assert.True(t, len(dir) > 1 && dir[0] == '/')
|
||||
}
|
||||
|
||||
func TestBuildProvider_Good_MediumSet(t *testing.T) {
|
||||
func TestProvider_BuildProviderMediumSet_Good(t *testing.T) {
|
||||
p := NewProvider(".", nil)
|
||||
assert.NotNil(t, p.medium, "medium should be set to io.Local")
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,17 +6,17 @@ import (
|
|||
"archive/zip"
|
||||
"bytes"
|
||||
"compress/gzip"
|
||||
"fmt"
|
||||
"io"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"dappco.re/go/core"
|
||||
"dappco.re/go/core/build/internal/ax"
|
||||
io_interface "dappco.re/go/core/io"
|
||||
coreerr "dappco.re/go/core/log"
|
||||
"github.com/Snider/Borg/pkg/compress"
|
||||
)
|
||||
|
||||
// ArchiveFormat specifies the compression format for archives.
|
||||
// Usage example: declare a value of type build.ArchiveFormat in integrating code.
|
||||
type ArchiveFormat string
|
||||
|
||||
const (
|
||||
|
|
@ -32,6 +32,7 @@ const (
|
|||
// Uses tar.gz for linux/darwin and zip for windows.
|
||||
// The archive is created alongside the binary (e.g., dist/myapp_linux_amd64.tar.gz).
|
||||
// Returns a new Artifact with Path pointing to the archive.
|
||||
// Usage example: call build.Archive(...) from integrating code.
|
||||
func Archive(fs io_interface.Medium, artifact Artifact) (Artifact, error) {
|
||||
return ArchiveWithFormat(fs, artifact, ArchiveFormatGzip)
|
||||
}
|
||||
|
|
@ -39,6 +40,7 @@ func Archive(fs io_interface.Medium, artifact Artifact) (Artifact, error) {
|
|||
// ArchiveXZ creates an archive for a single artifact using xz compression.
|
||||
// Uses tar.xz for linux/darwin and zip for windows.
|
||||
// Returns a new Artifact with Path pointing to the archive.
|
||||
// Usage example: call build.ArchiveXZ(...) from integrating code.
|
||||
func ArchiveXZ(fs io_interface.Medium, artifact Artifact) (Artifact, error) {
|
||||
return ArchiveWithFormat(fs, artifact, ArchiveFormatXZ)
|
||||
}
|
||||
|
|
@ -47,6 +49,7 @@ func ArchiveXZ(fs io_interface.Medium, artifact Artifact) (Artifact, error) {
|
|||
// Uses tar.gz or tar.xz for linux/darwin and zip for windows.
|
||||
// The archive is created alongside the binary (e.g., dist/myapp_linux_amd64.tar.xz).
|
||||
// Returns a new Artifact with Path pointing to the archive.
|
||||
// Usage example: call build.ArchiveWithFormat(...) from integrating code.
|
||||
func ArchiveWithFormat(fs io_interface.Medium, artifact Artifact, format ArchiveFormat) (Artifact, error) {
|
||||
if artifact.Path == "" {
|
||||
return Artifact{}, coreerr.E("build.Archive", "artifact path is empty", nil)
|
||||
|
|
@ -94,18 +97,21 @@ func ArchiveWithFormat(fs io_interface.Medium, artifact Artifact, format Archive
|
|||
|
||||
// ArchiveAll archives all artifacts using gzip compression.
|
||||
// Returns a slice of new artifacts pointing to the archives.
|
||||
// Usage example: call build.ArchiveAll(...) from integrating code.
|
||||
func ArchiveAll(fs io_interface.Medium, artifacts []Artifact) ([]Artifact, error) {
|
||||
return ArchiveAllWithFormat(fs, artifacts, ArchiveFormatGzip)
|
||||
}
|
||||
|
||||
// ArchiveAllXZ archives all artifacts using xz compression.
|
||||
// Returns a slice of new artifacts pointing to the archives.
|
||||
// Usage example: call build.ArchiveAllXZ(...) from integrating code.
|
||||
func ArchiveAllXZ(fs io_interface.Medium, artifacts []Artifact) ([]Artifact, error) {
|
||||
return ArchiveAllWithFormat(fs, artifacts, ArchiveFormatXZ)
|
||||
}
|
||||
|
||||
// ArchiveAllWithFormat archives all artifacts with the specified format.
|
||||
// Returns a slice of new artifacts pointing to the archives.
|
||||
// Usage example: call build.ArchiveAllWithFormat(...) from integrating code.
|
||||
func ArchiveAllWithFormat(fs io_interface.Medium, artifacts []Artifact, format ArchiveFormat) ([]Artifact, error) {
|
||||
if len(artifacts) == 0 {
|
||||
return nil, nil
|
||||
|
|
@ -127,18 +133,18 @@ func ArchiveAllWithFormat(fs io_interface.Medium, artifacts []Artifact, format A
|
|||
// Format: dist/myapp_linux_amd64.tar.gz (binary name taken from artifact path).
|
||||
func archiveFilename(artifact Artifact, ext string) string {
|
||||
// Get the directory containing the binary (e.g., dist/linux_amd64)
|
||||
dir := filepath.Dir(artifact.Path)
|
||||
dir := ax.Dir(artifact.Path)
|
||||
// Go up one level to the output directory (e.g., dist)
|
||||
outputDir := filepath.Dir(dir)
|
||||
outputDir := ax.Dir(dir)
|
||||
|
||||
// Get the binary name without extension
|
||||
binaryName := filepath.Base(artifact.Path)
|
||||
binaryName = strings.TrimSuffix(binaryName, ".exe")
|
||||
binaryName := ax.Base(artifact.Path)
|
||||
binaryName = core.TrimSuffix(binaryName, ".exe")
|
||||
|
||||
// Construct archive name: myapp_linux_amd64.tar.gz
|
||||
archiveName := fmt.Sprintf("%s_%s_%s%s", binaryName, artifact.OS, artifact.Arch, ext)
|
||||
archiveName := core.Sprintf("%s_%s_%s%s", binaryName, artifact.OS, artifact.Arch, ext)
|
||||
|
||||
return filepath.Join(outputDir, archiveName)
|
||||
return ax.Join(outputDir, archiveName)
|
||||
}
|
||||
|
||||
// createTarXzArchive creates a tar.xz archive containing a single file.
|
||||
|
|
@ -165,7 +171,7 @@ func createTarXzArchive(fs io_interface.Medium, src, dst string) error {
|
|||
if err != nil {
|
||||
return coreerr.E("build.createTarXzArchive", "failed to create tar header", err)
|
||||
}
|
||||
header.Name = filepath.Base(src)
|
||||
header.Name = ax.Base(src)
|
||||
|
||||
if err := tarWriter.WriteHeader(header); err != nil {
|
||||
return coreerr.E("build.createTarXzArchive", "failed to write tar header", err)
|
||||
|
|
@ -234,7 +240,7 @@ func createTarGzArchive(fs io_interface.Medium, src, dst string) error {
|
|||
return coreerr.E("build.createTarGzArchive", "failed to create tar header", err)
|
||||
}
|
||||
// Use just the filename, not the full path
|
||||
header.Name = filepath.Base(src)
|
||||
header.Name = ax.Base(src)
|
||||
|
||||
// Write header
|
||||
if err := tarWriter.WriteHeader(header); err != nil {
|
||||
|
|
@ -280,7 +286,7 @@ func createZipArchive(fs io_interface.Medium, src, dst string) error {
|
|||
return coreerr.E("build.createZipArchive", "failed to create zip header", err)
|
||||
}
|
||||
// Use just the filename, not the full path
|
||||
header.Name = filepath.Base(src)
|
||||
header.Name = ax.Base(src)
|
||||
header.Method = zip.Deflate
|
||||
|
||||
// Create file in archive
|
||||
|
|
|
|||
|
|
@ -6,10 +6,11 @@ import (
|
|||
"bytes"
|
||||
"compress/gzip"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
stdfs "io/fs"
|
||||
"testing"
|
||||
|
||||
"dappco.re/go/core/build/internal/ax"
|
||||
|
||||
io_interface "dappco.re/go/core/io"
|
||||
"github.com/Snider/Borg/pkg/compress"
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
|
@ -24,20 +25,20 @@ func setupArchiveTestFile(t *testing.T, name, os_, arch string) (binaryPath stri
|
|||
outputDir = t.TempDir()
|
||||
|
||||
// Create platform directory: dist/os_arch
|
||||
platformDir := filepath.Join(outputDir, os_+"_"+arch)
|
||||
err := os.MkdirAll(platformDir, 0755)
|
||||
platformDir := ax.Join(outputDir, os_+"_"+arch)
|
||||
err := ax.MkdirAll(platformDir, 0755)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Create test binary
|
||||
binaryPath = filepath.Join(platformDir, name)
|
||||
binaryPath = ax.Join(platformDir, name)
|
||||
content := []byte("#!/bin/bash\necho 'Hello, World!'\n")
|
||||
err = os.WriteFile(binaryPath, content, 0755)
|
||||
err = ax.WriteFile(binaryPath, content, 0755)
|
||||
require.NoError(t, err)
|
||||
|
||||
return binaryPath, outputDir
|
||||
}
|
||||
|
||||
func TestArchive_Good(t *testing.T) {
|
||||
func TestArchive_Archive_Good(t *testing.T) {
|
||||
fs := io_interface.Local
|
||||
t.Run("creates tar.gz for linux", func(t *testing.T) {
|
||||
binaryPath, outputDir := setupArchiveTestFile(t, "myapp", "linux", "amd64")
|
||||
|
|
@ -52,7 +53,7 @@ func TestArchive_Good(t *testing.T) {
|
|||
require.NoError(t, err)
|
||||
|
||||
// Verify archive was created
|
||||
expectedPath := filepath.Join(outputDir, "myapp_linux_amd64.tar.gz")
|
||||
expectedPath := ax.Join(outputDir, "myapp_linux_amd64.tar.gz")
|
||||
assert.Equal(t, expectedPath, result.Path)
|
||||
assert.FileExists(t, result.Path)
|
||||
|
||||
|
|
@ -76,7 +77,7 @@ func TestArchive_Good(t *testing.T) {
|
|||
result, err := Archive(fs, artifact)
|
||||
require.NoError(t, err)
|
||||
|
||||
expectedPath := filepath.Join(outputDir, "myapp_darwin_arm64.tar.gz")
|
||||
expectedPath := ax.Join(outputDir, "myapp_darwin_arm64.tar.gz")
|
||||
assert.Equal(t, expectedPath, result.Path)
|
||||
assert.FileExists(t, result.Path)
|
||||
|
||||
|
|
@ -96,7 +97,7 @@ func TestArchive_Good(t *testing.T) {
|
|||
require.NoError(t, err)
|
||||
|
||||
// Windows archives should strip .exe from archive name
|
||||
expectedPath := filepath.Join(outputDir, "myapp_windows_amd64.zip")
|
||||
expectedPath := ax.Join(outputDir, "myapp_windows_amd64.zip")
|
||||
assert.Equal(t, expectedPath, result.Path)
|
||||
assert.FileExists(t, result.Path)
|
||||
|
||||
|
|
@ -130,7 +131,7 @@ func TestArchive_Good(t *testing.T) {
|
|||
result, err := ArchiveXZ(fs, artifact)
|
||||
require.NoError(t, err)
|
||||
|
||||
expectedPath := filepath.Join(outputDir, "myapp_linux_amd64.tar.xz")
|
||||
expectedPath := ax.Join(outputDir, "myapp_linux_amd64.tar.xz")
|
||||
assert.Equal(t, expectedPath, result.Path)
|
||||
assert.FileExists(t, result.Path)
|
||||
|
||||
|
|
@ -149,7 +150,7 @@ func TestArchive_Good(t *testing.T) {
|
|||
result, err := ArchiveWithFormat(fs, artifact, ArchiveFormatXZ)
|
||||
require.NoError(t, err)
|
||||
|
||||
expectedPath := filepath.Join(outputDir, "myapp_darwin_arm64.tar.xz")
|
||||
expectedPath := ax.Join(outputDir, "myapp_darwin_arm64.tar.xz")
|
||||
assert.Equal(t, expectedPath, result.Path)
|
||||
assert.FileExists(t, result.Path)
|
||||
|
||||
|
|
@ -169,7 +170,7 @@ func TestArchive_Good(t *testing.T) {
|
|||
require.NoError(t, err)
|
||||
|
||||
// Windows should still get .zip regardless of format
|
||||
expectedPath := filepath.Join(outputDir, "myapp_windows_amd64.zip")
|
||||
expectedPath := ax.Join(outputDir, "myapp_windows_amd64.zip")
|
||||
assert.Equal(t, expectedPath, result.Path)
|
||||
assert.FileExists(t, result.Path)
|
||||
|
||||
|
|
@ -177,7 +178,7 @@ func TestArchive_Good(t *testing.T) {
|
|||
})
|
||||
}
|
||||
|
||||
func TestArchive_Bad(t *testing.T) {
|
||||
func TestArchive_Archive_Bad(t *testing.T) {
|
||||
fs := io_interface.Local
|
||||
t.Run("returns error for empty path", func(t *testing.T) {
|
||||
artifact := Artifact{
|
||||
|
|
@ -221,7 +222,7 @@ func TestArchive_Bad(t *testing.T) {
|
|||
})
|
||||
}
|
||||
|
||||
func TestArchiveAll_Good(t *testing.T) {
|
||||
func TestArchive_ArchiveAll_Good(t *testing.T) {
|
||||
fs := io_interface.Local
|
||||
t.Run("archives multiple artifacts", func(t *testing.T) {
|
||||
outputDir := t.TempDir()
|
||||
|
|
@ -239,8 +240,8 @@ func TestArchiveAll_Good(t *testing.T) {
|
|||
}
|
||||
|
||||
for _, target := range targets {
|
||||
platformDir := filepath.Join(outputDir, target.os_+"_"+target.arch)
|
||||
err := os.MkdirAll(platformDir, 0755)
|
||||
platformDir := ax.Join(outputDir, target.os_+"_"+target.arch)
|
||||
err := ax.MkdirAll(platformDir, 0755)
|
||||
require.NoError(t, err)
|
||||
|
||||
name := "myapp"
|
||||
|
|
@ -248,8 +249,8 @@ func TestArchiveAll_Good(t *testing.T) {
|
|||
name = "myapp.exe"
|
||||
}
|
||||
|
||||
binaryPath := filepath.Join(platformDir, name)
|
||||
err = os.WriteFile(binaryPath, []byte("binary content"), 0755)
|
||||
binaryPath := ax.Join(platformDir, name)
|
||||
err = ax.WriteFile(binaryPath, []byte("binary content"), 0755)
|
||||
require.NoError(t, err)
|
||||
|
||||
artifacts = append(artifacts, Artifact{
|
||||
|
|
@ -284,7 +285,7 @@ func TestArchiveAll_Good(t *testing.T) {
|
|||
})
|
||||
}
|
||||
|
||||
func TestArchiveAll_Bad(t *testing.T) {
|
||||
func TestArchive_ArchiveAll_Bad(t *testing.T) {
|
||||
fs := io_interface.Local
|
||||
t.Run("returns partial results on error", func(t *testing.T) {
|
||||
binaryPath, _ := setupArchiveTestFile(t, "myapp", "linux", "amd64")
|
||||
|
|
@ -302,7 +303,7 @@ func TestArchiveAll_Bad(t *testing.T) {
|
|||
})
|
||||
}
|
||||
|
||||
func TestArchiveFilename_Good(t *testing.T) {
|
||||
func TestArchive_ArchiveFilename_Good(t *testing.T) {
|
||||
t.Run("generates correct tar.gz filename", func(t *testing.T) {
|
||||
artifact := Artifact{
|
||||
Path: "/output/linux_amd64/myapp",
|
||||
|
|
@ -344,7 +345,7 @@ func TestArchive_RoundTrip_Good(t *testing.T) {
|
|||
binaryPath, _ := setupArchiveTestFile(t, "roundtrip-app", "linux", "amd64")
|
||||
|
||||
// Read original content
|
||||
originalContent, err := os.ReadFile(binaryPath)
|
||||
originalContent, err := ax.ReadFile(binaryPath)
|
||||
require.NoError(t, err)
|
||||
|
||||
artifact := Artifact{
|
||||
|
|
@ -366,7 +367,7 @@ func TestArchive_RoundTrip_Good(t *testing.T) {
|
|||
t.Run("tar.xz round trip preserves content", func(t *testing.T) {
|
||||
binaryPath, _ := setupArchiveTestFile(t, "roundtrip-xz", "linux", "arm64")
|
||||
|
||||
originalContent, err := os.ReadFile(binaryPath)
|
||||
originalContent, err := ax.ReadFile(binaryPath)
|
||||
require.NoError(t, err)
|
||||
|
||||
artifact := Artifact{
|
||||
|
|
@ -386,7 +387,7 @@ func TestArchive_RoundTrip_Good(t *testing.T) {
|
|||
t.Run("zip round trip preserves content", func(t *testing.T) {
|
||||
binaryPath, _ := setupArchiveTestFile(t, "roundtrip.exe", "windows", "amd64")
|
||||
|
||||
originalContent, err := os.ReadFile(binaryPath)
|
||||
originalContent, err := ax.ReadFile(binaryPath)
|
||||
require.NoError(t, err)
|
||||
|
||||
artifact := Artifact{
|
||||
|
|
@ -418,21 +419,21 @@ func TestArchive_RoundTrip_Good(t *testing.T) {
|
|||
// Extract and verify permissions are preserved
|
||||
mode := extractTarGzFileMode(t, archiveArtifact.Path, "perms-app")
|
||||
// The original file was written with 0755
|
||||
assert.Equal(t, os.FileMode(0755), mode&os.ModePerm)
|
||||
assert.Equal(t, stdfs.FileMode(0o755), mode&stdfs.ModePerm)
|
||||
})
|
||||
|
||||
t.Run("round trip with large binary content", func(t *testing.T) {
|
||||
outputDir := t.TempDir()
|
||||
platformDir := filepath.Join(outputDir, "linux_amd64")
|
||||
require.NoError(t, os.MkdirAll(platformDir, 0755))
|
||||
platformDir := ax.Join(outputDir, "linux_amd64")
|
||||
require.NoError(t, ax.MkdirAll(platformDir, 0755))
|
||||
|
||||
// Create a larger file (64KB)
|
||||
largeContent := make([]byte, 64*1024)
|
||||
for i := range largeContent {
|
||||
largeContent[i] = byte(i % 256)
|
||||
}
|
||||
binaryPath := filepath.Join(platformDir, "large-app")
|
||||
require.NoError(t, os.WriteFile(binaryPath, largeContent, 0755))
|
||||
binaryPath := ax.Join(platformDir, "large-app")
|
||||
require.NoError(t, ax.WriteFile(binaryPath, largeContent, 0755))
|
||||
|
||||
artifact := Artifact{
|
||||
Path: binaryPath,
|
||||
|
|
@ -449,16 +450,16 @@ func TestArchive_RoundTrip_Good(t *testing.T) {
|
|||
|
||||
t.Run("archive is smaller than original for tar.gz", func(t *testing.T) {
|
||||
outputDir := t.TempDir()
|
||||
platformDir := filepath.Join(outputDir, "linux_amd64")
|
||||
require.NoError(t, os.MkdirAll(platformDir, 0755))
|
||||
platformDir := ax.Join(outputDir, "linux_amd64")
|
||||
require.NoError(t, ax.MkdirAll(platformDir, 0755))
|
||||
|
||||
// Create a compressible file (repeated pattern)
|
||||
compressibleContent := make([]byte, 4096)
|
||||
for i := range compressibleContent {
|
||||
compressibleContent[i] = 'A'
|
||||
}
|
||||
binaryPath := filepath.Join(platformDir, "compressible-app")
|
||||
require.NoError(t, os.WriteFile(binaryPath, compressibleContent, 0755))
|
||||
binaryPath := ax.Join(platformDir, "compressible-app")
|
||||
require.NoError(t, ax.WriteFile(binaryPath, compressibleContent, 0755))
|
||||
|
||||
artifact := Artifact{
|
||||
Path: binaryPath,
|
||||
|
|
@ -469,9 +470,9 @@ func TestArchive_RoundTrip_Good(t *testing.T) {
|
|||
archiveArtifact, err := Archive(fs, artifact)
|
||||
require.NoError(t, err)
|
||||
|
||||
originalInfo, err := os.Stat(binaryPath)
|
||||
originalInfo, err := ax.Stat(binaryPath)
|
||||
require.NoError(t, err)
|
||||
archiveInfo, err := os.Stat(archiveArtifact.Path)
|
||||
archiveInfo, err := ax.Stat(archiveArtifact.Path)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Compressed archive should be smaller than original
|
||||
|
|
@ -483,7 +484,7 @@ func TestArchive_RoundTrip_Good(t *testing.T) {
|
|||
func extractTarGzFile(t *testing.T, archivePath, fileName string) []byte {
|
||||
t.Helper()
|
||||
|
||||
file, err := os.Open(archivePath)
|
||||
file, err := ax.Open(archivePath)
|
||||
require.NoError(t, err)
|
||||
defer func() { _ = file.Close() }()
|
||||
|
||||
|
|
@ -509,10 +510,10 @@ func extractTarGzFile(t *testing.T, archivePath, fileName string) []byte {
|
|||
}
|
||||
|
||||
// extractTarGzFileMode extracts the file mode of a named file from a tar.gz archive.
|
||||
func extractTarGzFileMode(t *testing.T, archivePath, fileName string) os.FileMode {
|
||||
func extractTarGzFileMode(t *testing.T, archivePath, fileName string) stdfs.FileMode {
|
||||
t.Helper()
|
||||
|
||||
file, err := os.Open(archivePath)
|
||||
file, err := ax.Open(archivePath)
|
||||
require.NoError(t, err)
|
||||
defer func() { _ = file.Close() }()
|
||||
|
||||
|
|
@ -539,7 +540,7 @@ func extractTarGzFileMode(t *testing.T, archivePath, fileName string) os.FileMod
|
|||
func extractTarXzFile(t *testing.T, archivePath, fileName string) []byte {
|
||||
t.Helper()
|
||||
|
||||
xzData, err := os.ReadFile(archivePath)
|
||||
xzData, err := ax.ReadFile(archivePath)
|
||||
require.NoError(t, err)
|
||||
|
||||
tarData, err := compress.Decompress(xzData)
|
||||
|
|
@ -590,7 +591,7 @@ func extractZipFile(t *testing.T, archivePath, fileName string) []byte {
|
|||
func verifyTarGzContent(t *testing.T, archivePath, expectedName string) {
|
||||
t.Helper()
|
||||
|
||||
file, err := os.Open(archivePath)
|
||||
file, err := ax.Open(archivePath)
|
||||
require.NoError(t, err)
|
||||
defer func() { _ = file.Close() }()
|
||||
|
||||
|
|
@ -626,7 +627,7 @@ func verifyTarXzContent(t *testing.T, archivePath, expectedName string) {
|
|||
t.Helper()
|
||||
|
||||
// Read the xz-compressed file
|
||||
xzData, err := os.ReadFile(archivePath)
|
||||
xzData, err := ax.ReadFile(archivePath)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Decompress with Borg
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ import (
|
|||
)
|
||||
|
||||
// ProjectType represents a detected project type.
|
||||
// Usage example: declare a value of type build.ProjectType in integrating code.
|
||||
type ProjectType string
|
||||
|
||||
// Project type constants for build detection.
|
||||
|
|
@ -33,17 +34,20 @@ const (
|
|||
)
|
||||
|
||||
// Target represents a build target platform.
|
||||
// Usage example: declare a value of type build.Target in integrating code.
|
||||
type Target struct {
|
||||
OS string
|
||||
Arch string
|
||||
}
|
||||
|
||||
// String returns the target in GOOS/GOARCH format.
|
||||
// Usage example: call value.String(...) from integrating code.
|
||||
func (t Target) String() string {
|
||||
return t.OS + "/" + t.Arch
|
||||
}
|
||||
|
||||
// Artifact represents a build output file.
|
||||
// Usage example: declare a value of type build.Artifact in integrating code.
|
||||
type Artifact struct {
|
||||
Path string
|
||||
OS string
|
||||
|
|
@ -52,6 +56,7 @@ type Artifact struct {
|
|||
}
|
||||
|
||||
// Config holds build configuration.
|
||||
// Usage example: declare a value of type build.Config in integrating code.
|
||||
type Config struct {
|
||||
// FS is the medium used for file operations.
|
||||
FS io.Medium
|
||||
|
|
@ -82,6 +87,7 @@ type Config struct {
|
|||
}
|
||||
|
||||
// Builder defines the interface for project-specific build implementations.
|
||||
// Usage example: declare a value of type build.Builder in integrating code.
|
||||
type Builder interface {
|
||||
// Name returns the builder's identifier.
|
||||
Name() string
|
||||
|
|
|
|||
|
|
@ -3,13 +3,10 @@ package builders
|
|||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strings"
|
||||
|
||||
"dappco.re/go/core"
|
||||
"dappco.re/go/core/build/internal/ax"
|
||||
"dappco.re/go/core/build/pkg/build"
|
||||
"dappco.re/go/core/io"
|
||||
coreerr "dappco.re/go/core/log"
|
||||
|
|
@ -17,19 +14,23 @@ import (
|
|||
|
||||
// CPPBuilder implements the Builder interface for C++ projects using CMake + Conan.
|
||||
// It wraps the Makefile-based build system from the .core/build submodule.
|
||||
// Usage example: declare a value of type builders.CPPBuilder in integrating code.
|
||||
type CPPBuilder struct{}
|
||||
|
||||
// NewCPPBuilder creates a new CPPBuilder instance.
|
||||
// Usage example: call builders.NewCPPBuilder(...) from integrating code.
|
||||
func NewCPPBuilder() *CPPBuilder {
|
||||
return &CPPBuilder{}
|
||||
}
|
||||
|
||||
// Name returns the builder's identifier.
|
||||
// Usage example: call value.Name(...) from integrating code.
|
||||
func (b *CPPBuilder) Name() string {
|
||||
return "cpp"
|
||||
}
|
||||
|
||||
// Detect checks if this builder can handle the project in the given directory.
|
||||
// Usage example: call value.Detect(...) from integrating code.
|
||||
func (b *CPPBuilder) Detect(fs io.Medium, dir string) (bool, error) {
|
||||
return build.IsCPPProject(fs, dir), nil
|
||||
}
|
||||
|
|
@ -37,6 +38,7 @@ func (b *CPPBuilder) Detect(fs io.Medium, dir string) (bool, error) {
|
|||
// Build compiles the C++ project using Make targets.
|
||||
// The build flow is: make configure → make build → make package.
|
||||
// Cross-compilation is handled via Conan profiles specified in .core/build.yaml.
|
||||
// Usage example: call value.Build(...) from integrating code.
|
||||
func (b *CPPBuilder) Build(ctx context.Context, cfg *build.Config, targets []build.Target) ([]build.Artifact, error) {
|
||||
if cfg == nil {
|
||||
return nil, coreerr.E("CPPBuilder.Build", "config is nil", nil)
|
||||
|
|
@ -83,7 +85,7 @@ func (b *CPPBuilder) buildTarget(ctx context.Context, cfg *build.Config, target
|
|||
|
||||
// buildHost runs the standard make configure → make build → make package flow.
|
||||
func (b *CPPBuilder) buildHost(ctx context.Context, cfg *build.Config, target build.Target) ([]build.Artifact, error) {
|
||||
fmt.Printf("Building C++ project for %s/%s (host)\n", target.OS, target.Arch)
|
||||
core.Print(nil, "Building C++ project for %s/%s (host)", target.OS, target.Arch)
|
||||
|
||||
// Step 1: Configure (runs conan install + cmake configure)
|
||||
if err := b.runMake(ctx, cfg.ProjectDir, "configure"); err != nil {
|
||||
|
|
@ -113,7 +115,7 @@ func (b *CPPBuilder) buildCross(ctx context.Context, cfg *build.Config, target b
|
|||
return nil, coreerr.E("CPPBuilder.buildCross", "no Conan profile mapped for target "+target.OS+"/"+target.Arch, nil)
|
||||
}
|
||||
|
||||
fmt.Printf("Building C++ project for %s/%s (cross: %s)\n", target.OS, target.Arch, profile)
|
||||
core.Print(nil, "Building C++ project for %s/%s (cross: %s)", target.OS, target.Arch, profile)
|
||||
|
||||
// The Makefile exposes each profile as a top-level target
|
||||
if err := b.runMake(ctx, cfg.ProjectDir, profile); err != nil {
|
||||
|
|
@ -125,13 +127,7 @@ func (b *CPPBuilder) buildCross(ctx context.Context, cfg *build.Config, target b
|
|||
|
||||
// runMake executes a make target in the project directory.
|
||||
func (b *CPPBuilder) runMake(ctx context.Context, projectDir string, target string) error {
|
||||
cmd := exec.CommandContext(ctx, "make", target)
|
||||
cmd.Dir = projectDir
|
||||
cmd.Stdout = os.Stdout
|
||||
cmd.Stderr = os.Stderr
|
||||
cmd.Env = os.Environ()
|
||||
|
||||
if err := cmd.Run(); err != nil {
|
||||
if err := ax.ExecDir(ctx, projectDir, "make", target); err != nil {
|
||||
return coreerr.E("CPPBuilder.runMake", "make "+target+" failed", err)
|
||||
}
|
||||
return nil
|
||||
|
|
@ -139,7 +135,7 @@ func (b *CPPBuilder) runMake(ctx context.Context, projectDir string, target stri
|
|||
|
||||
// findArtifacts searches for built packages in build/packages/.
|
||||
func (b *CPPBuilder) findArtifacts(fs io.Medium, projectDir string, target build.Target) ([]build.Artifact, error) {
|
||||
packagesDir := filepath.Join(projectDir, "build", "packages")
|
||||
packagesDir := ax.Join(projectDir, "build", "packages")
|
||||
|
||||
if !fs.IsDir(packagesDir) {
|
||||
// Fall back to searching build/release/src/ for raw binaries
|
||||
|
|
@ -159,12 +155,12 @@ func (b *CPPBuilder) findArtifacts(fs io.Medium, projectDir string, target build
|
|||
|
||||
name := entry.Name()
|
||||
// Skip checksum files and hidden files
|
||||
if strings.HasSuffix(name, ".sha256") || strings.HasPrefix(name, ".") {
|
||||
if core.HasSuffix(name, ".sha256") || core.HasPrefix(name, ".") {
|
||||
continue
|
||||
}
|
||||
|
||||
artifacts = append(artifacts, build.Artifact{
|
||||
Path: filepath.Join(packagesDir, name),
|
||||
Path: ax.Join(packagesDir, name),
|
||||
OS: target.OS,
|
||||
Arch: target.Arch,
|
||||
})
|
||||
|
|
@ -175,7 +171,7 @@ func (b *CPPBuilder) findArtifacts(fs io.Medium, projectDir string, target build
|
|||
|
||||
// findBinaries searches for compiled binaries in build/release/src/.
|
||||
func (b *CPPBuilder) findBinaries(fs io.Medium, projectDir string, target build.Target) ([]build.Artifact, error) {
|
||||
binDir := filepath.Join(projectDir, "build", "release", "src")
|
||||
binDir := ax.Join(projectDir, "build", "release", "src")
|
||||
|
||||
if !fs.IsDir(binDir) {
|
||||
return nil, coreerr.E("CPPBuilder.findBinaries", "no build output found in "+binDir, nil)
|
||||
|
|
@ -194,16 +190,16 @@ func (b *CPPBuilder) findBinaries(fs io.Medium, projectDir string, target build.
|
|||
|
||||
name := entry.Name()
|
||||
// Skip non-executable files (libraries, cmake files, etc.)
|
||||
if strings.HasSuffix(name, ".a") || strings.HasSuffix(name, ".o") ||
|
||||
strings.HasSuffix(name, ".cmake") || strings.HasPrefix(name, ".") {
|
||||
if core.HasSuffix(name, ".a") || core.HasSuffix(name, ".o") ||
|
||||
core.HasSuffix(name, ".cmake") || core.HasPrefix(name, ".") {
|
||||
continue
|
||||
}
|
||||
|
||||
fullPath := filepath.Join(binDir, name)
|
||||
fullPath := ax.Join(binDir, name)
|
||||
|
||||
// On Unix, check if file is executable
|
||||
if target.OS != "windows" {
|
||||
info, err := os.Stat(fullPath)
|
||||
info, err := fs.Stat(fullPath)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
|
@ -244,7 +240,7 @@ func (b *CPPBuilder) targetToProfile(target build.Target) string {
|
|||
|
||||
// validateMake checks if make is available.
|
||||
func (b *CPPBuilder) validateMake() error {
|
||||
if _, err := exec.LookPath("make"); err != nil {
|
||||
if _, err := ax.LookPath("make"); err != nil {
|
||||
return coreerr.E("CPPBuilder.validateMake", "make not found. Install build-essential (Linux) or Xcode Command Line Tools (macOS)", nil)
|
||||
}
|
||||
return nil
|
||||
|
|
|
|||
|
|
@ -1,27 +1,27 @@
|
|||
package builders
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"dappco.re/go/core/build/internal/ax"
|
||||
|
||||
"dappco.re/go/core/build/pkg/build"
|
||||
"dappco.re/go/core/io"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestCPPBuilder_Name_Good(t *testing.T) {
|
||||
func TestCPP_CPPBuilderName_Good(t *testing.T) {
|
||||
builder := NewCPPBuilder()
|
||||
assert.Equal(t, "cpp", builder.Name())
|
||||
}
|
||||
|
||||
func TestCPPBuilder_Detect_Good(t *testing.T) {
|
||||
func TestCPP_CPPBuilderDetect_Good(t *testing.T) {
|
||||
fs := io.Local
|
||||
|
||||
t.Run("detects C++ project with CMakeLists.txt", func(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
err := os.WriteFile(filepath.Join(dir, "CMakeLists.txt"), []byte("cmake_minimum_required(VERSION 3.16)"), 0644)
|
||||
err := ax.WriteFile(ax.Join(dir, "CMakeLists.txt"), []byte("cmake_minimum_required(VERSION 3.16)"), 0644)
|
||||
require.NoError(t, err)
|
||||
|
||||
builder := NewCPPBuilder()
|
||||
|
|
@ -32,7 +32,7 @@ func TestCPPBuilder_Detect_Good(t *testing.T) {
|
|||
|
||||
t.Run("returns false for non-C++ project", func(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
err := os.WriteFile(filepath.Join(dir, "go.mod"), []byte("module test"), 0644)
|
||||
err := ax.WriteFile(ax.Join(dir, "go.mod"), []byte("module test"), 0644)
|
||||
require.NoError(t, err)
|
||||
|
||||
builder := NewCPPBuilder()
|
||||
|
|
@ -51,7 +51,7 @@ func TestCPPBuilder_Detect_Good(t *testing.T) {
|
|||
})
|
||||
}
|
||||
|
||||
func TestCPPBuilder_Build_Bad(t *testing.T) {
|
||||
func TestCPP_CPPBuilderBuild_Bad(t *testing.T) {
|
||||
t.Run("returns error for nil config", func(t *testing.T) {
|
||||
builder := NewCPPBuilder()
|
||||
artifacts, err := builder.Build(nil, nil, []build.Target{{OS: "linux", Arch: "amd64"}})
|
||||
|
|
@ -61,7 +61,7 @@ func TestCPPBuilder_Build_Bad(t *testing.T) {
|
|||
})
|
||||
}
|
||||
|
||||
func TestCPPBuilder_TargetToProfile_Good(t *testing.T) {
|
||||
func TestCPP_CPPBuilderTargetToProfile_Good(t *testing.T) {
|
||||
builder := NewCPPBuilder()
|
||||
|
||||
tests := []struct {
|
||||
|
|
@ -84,7 +84,7 @@ func TestCPPBuilder_TargetToProfile_Good(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestCPPBuilder_TargetToProfile_Bad(t *testing.T) {
|
||||
func TestCPP_CPPBuilderTargetToProfile_Bad(t *testing.T) {
|
||||
builder := NewCPPBuilder()
|
||||
|
||||
t.Run("returns empty for unknown target", func(t *testing.T) {
|
||||
|
|
@ -93,18 +93,18 @@ func TestCPPBuilder_TargetToProfile_Bad(t *testing.T) {
|
|||
})
|
||||
}
|
||||
|
||||
func TestCPPBuilder_FindArtifacts_Good(t *testing.T) {
|
||||
func TestCPP_CPPBuilderFindArtifacts_Good(t *testing.T) {
|
||||
fs := io.Local
|
||||
|
||||
t.Run("finds packages in build/packages", func(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
packagesDir := filepath.Join(dir, "build", "packages")
|
||||
require.NoError(t, os.MkdirAll(packagesDir, 0755))
|
||||
packagesDir := ax.Join(dir, "build", "packages")
|
||||
require.NoError(t, ax.MkdirAll(packagesDir, 0755))
|
||||
|
||||
// Create mock package files
|
||||
require.NoError(t, os.WriteFile(filepath.Join(packagesDir, "test-1.0-linux-x86_64.tar.xz"), []byte("pkg"), 0644))
|
||||
require.NoError(t, os.WriteFile(filepath.Join(packagesDir, "test-1.0-linux-x86_64.tar.xz.sha256"), []byte("checksum"), 0644))
|
||||
require.NoError(t, os.WriteFile(filepath.Join(packagesDir, "test-1.0-linux-x86_64.rpm"), []byte("rpm"), 0644))
|
||||
require.NoError(t, ax.WriteFile(ax.Join(packagesDir, "test-1.0-linux-x86_64.tar.xz"), []byte("pkg"), 0644))
|
||||
require.NoError(t, ax.WriteFile(ax.Join(packagesDir, "test-1.0-linux-x86_64.tar.xz.sha256"), []byte("checksum"), 0644))
|
||||
require.NoError(t, ax.WriteFile(ax.Join(packagesDir, "test-1.0-linux-x86_64.rpm"), []byte("rpm"), 0644))
|
||||
|
||||
builder := NewCPPBuilder()
|
||||
target := build.Target{OS: "linux", Arch: "amd64"}
|
||||
|
|
@ -116,21 +116,21 @@ func TestCPPBuilder_FindArtifacts_Good(t *testing.T) {
|
|||
for _, a := range artifacts {
|
||||
assert.Equal(t, "linux", a.OS)
|
||||
assert.Equal(t, "amd64", a.Arch)
|
||||
assert.False(t, filepath.Ext(a.Path) == ".sha256")
|
||||
assert.False(t, ax.Ext(a.Path) == ".sha256")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("falls back to binaries in build/release/src", func(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
binDir := filepath.Join(dir, "build", "release", "src")
|
||||
require.NoError(t, os.MkdirAll(binDir, 0755))
|
||||
binDir := ax.Join(dir, "build", "release", "src")
|
||||
require.NoError(t, ax.MkdirAll(binDir, 0755))
|
||||
|
||||
// Create mock binary (executable)
|
||||
binPath := filepath.Join(binDir, "test-daemon")
|
||||
require.NoError(t, os.WriteFile(binPath, []byte("binary"), 0755))
|
||||
binPath := ax.Join(binDir, "test-daemon")
|
||||
require.NoError(t, ax.WriteFile(binPath, []byte("binary"), 0755))
|
||||
|
||||
// Create a library (should be skipped)
|
||||
require.NoError(t, os.WriteFile(filepath.Join(binDir, "libcrypto.a"), []byte("lib"), 0644))
|
||||
require.NoError(t, ax.WriteFile(ax.Join(binDir, "libcrypto.a"), []byte("lib"), 0644))
|
||||
|
||||
builder := NewCPPBuilder()
|
||||
target := build.Target{OS: "linux", Arch: "amd64"}
|
||||
|
|
@ -143,7 +143,7 @@ func TestCPPBuilder_FindArtifacts_Good(t *testing.T) {
|
|||
})
|
||||
}
|
||||
|
||||
func TestCPPBuilder_Interface_Good(t *testing.T) {
|
||||
func TestCPP_CPPBuilderInterface_Good(t *testing.T) {
|
||||
var _ build.Builder = (*CPPBuilder)(nil)
|
||||
var _ build.Builder = NewCPPBuilder()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,33 +3,34 @@ package builders
|
|||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"dappco.re/go/core"
|
||||
"dappco.re/go/core/build/internal/ax"
|
||||
"dappco.re/go/core/build/pkg/build"
|
||||
"dappco.re/go/core/io"
|
||||
coreerr "dappco.re/go/core/log"
|
||||
)
|
||||
|
||||
// DockerBuilder builds Docker images.
|
||||
// Usage example: declare a value of type builders.DockerBuilder in integrating code.
|
||||
type DockerBuilder struct{}
|
||||
|
||||
// NewDockerBuilder creates a new Docker builder.
|
||||
// Usage example: call builders.NewDockerBuilder(...) from integrating code.
|
||||
func NewDockerBuilder() *DockerBuilder {
|
||||
return &DockerBuilder{}
|
||||
}
|
||||
|
||||
// Name returns the builder's identifier.
|
||||
// Usage example: call value.Name(...) from integrating code.
|
||||
func (b *DockerBuilder) Name() string {
|
||||
return "docker"
|
||||
}
|
||||
|
||||
// Detect checks if a Dockerfile exists in the directory.
|
||||
// Usage example: call value.Detect(...) from integrating code.
|
||||
func (b *DockerBuilder) Detect(fs io.Medium, dir string) (bool, error) {
|
||||
dockerfilePath := filepath.Join(dir, "Dockerfile")
|
||||
dockerfilePath := ax.Join(dir, "Dockerfile")
|
||||
if fs.IsFile(dockerfilePath) {
|
||||
return true, nil
|
||||
}
|
||||
|
|
@ -37,6 +38,7 @@ func (b *DockerBuilder) Detect(fs io.Medium, dir string) (bool, error) {
|
|||
}
|
||||
|
||||
// Build builds Docker images for the specified targets.
|
||||
// Usage example: call value.Build(...) from integrating code.
|
||||
func (b *DockerBuilder) Build(ctx context.Context, cfg *build.Config, targets []build.Target) ([]build.Artifact, error) {
|
||||
// Validate docker CLI is available
|
||||
if err := b.validateDockerCli(); err != nil {
|
||||
|
|
@ -51,7 +53,7 @@ func (b *DockerBuilder) Build(ctx context.Context, cfg *build.Config, targets []
|
|||
// Determine Dockerfile path
|
||||
dockerfile := cfg.Dockerfile
|
||||
if dockerfile == "" {
|
||||
dockerfile = filepath.Join(cfg.ProjectDir, "Dockerfile")
|
||||
dockerfile = ax.Join(cfg.ProjectDir, "Dockerfile")
|
||||
}
|
||||
|
||||
// Validate Dockerfile exists
|
||||
|
|
@ -65,13 +67,13 @@ func (b *DockerBuilder) Build(ctx context.Context, cfg *build.Config, targets []
|
|||
imageName = cfg.Name
|
||||
}
|
||||
if imageName == "" {
|
||||
imageName = filepath.Base(cfg.ProjectDir)
|
||||
imageName = ax.Base(cfg.ProjectDir)
|
||||
}
|
||||
|
||||
// Build platform string from targets
|
||||
var platforms []string
|
||||
for _, t := range targets {
|
||||
platforms = append(platforms, fmt.Sprintf("%s/%s", t.OS, t.Arch))
|
||||
platforms = append(platforms, core.Sprintf("%s/%s", t.OS, t.Arch))
|
||||
}
|
||||
|
||||
// If no targets specified, use current platform
|
||||
|
|
@ -98,13 +100,13 @@ func (b *DockerBuilder) Build(ctx context.Context, cfg *build.Config, targets []
|
|||
var imageRefs []string
|
||||
for _, tag := range tags {
|
||||
// Expand version template
|
||||
expandedTag := strings.ReplaceAll(tag, "{{.Version}}", cfg.Version)
|
||||
expandedTag = strings.ReplaceAll(expandedTag, "{{Version}}", cfg.Version)
|
||||
expandedTag := core.Replace(tag, "{{.Version}}", cfg.Version)
|
||||
expandedTag = core.Replace(expandedTag, "{{Version}}", cfg.Version)
|
||||
|
||||
if registry != "" {
|
||||
imageRefs = append(imageRefs, fmt.Sprintf("%s/%s:%s", registry, imageName, expandedTag))
|
||||
imageRefs = append(imageRefs, core.Sprintf("%s/%s:%s", registry, imageName, expandedTag))
|
||||
} else {
|
||||
imageRefs = append(imageRefs, fmt.Sprintf("%s:%s", imageName, expandedTag))
|
||||
imageRefs = append(imageRefs, core.Sprintf("%s:%s", imageName, expandedTag))
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -112,7 +114,7 @@ func (b *DockerBuilder) Build(ctx context.Context, cfg *build.Config, targets []
|
|||
args := []string{"buildx", "build"}
|
||||
|
||||
// Multi-platform support
|
||||
args = append(args, "--platform", strings.Join(platforms, ","))
|
||||
args = append(args, "--platform", core.Join(",", platforms...))
|
||||
|
||||
// Add all tags
|
||||
for _, ref := range imageRefs {
|
||||
|
|
@ -124,14 +126,14 @@ func (b *DockerBuilder) Build(ctx context.Context, cfg *build.Config, targets []
|
|||
|
||||
// Build arguments
|
||||
for k, v := range cfg.BuildArgs {
|
||||
expandedValue := strings.ReplaceAll(v, "{{.Version}}", cfg.Version)
|
||||
expandedValue = strings.ReplaceAll(expandedValue, "{{Version}}", cfg.Version)
|
||||
args = append(args, "--build-arg", fmt.Sprintf("%s=%s", k, expandedValue))
|
||||
expandedValue := core.Replace(v, "{{.Version}}", cfg.Version)
|
||||
expandedValue = core.Replace(expandedValue, "{{Version}}", cfg.Version)
|
||||
args = append(args, "--build-arg", core.Sprintf("%s=%s", k, expandedValue))
|
||||
}
|
||||
|
||||
// Always add VERSION build arg if version is set
|
||||
if cfg.Version != "" {
|
||||
args = append(args, "--build-arg", fmt.Sprintf("VERSION=%s", cfg.Version))
|
||||
args = append(args, "--build-arg", core.Sprintf("VERSION=%s", cfg.Version))
|
||||
}
|
||||
|
||||
// Output to local docker images or push
|
||||
|
|
@ -143,8 +145,8 @@ func (b *DockerBuilder) Build(ctx context.Context, cfg *build.Config, targets []
|
|||
args = append(args, "--load")
|
||||
} else {
|
||||
// Multi-platform builds can't use --load, output to tarball
|
||||
outputPath := filepath.Join(cfg.OutputDir, fmt.Sprintf("%s.tar", imageName))
|
||||
args = append(args, "--output", fmt.Sprintf("type=oci,dest=%s", outputPath))
|
||||
outputPath := ax.Join(cfg.OutputDir, core.Sprintf("%s.tar", imageName))
|
||||
args = append(args, "--output", core.Sprintf("type=oci,dest=%s", outputPath))
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -156,17 +158,11 @@ func (b *DockerBuilder) Build(ctx context.Context, cfg *build.Config, targets []
|
|||
return nil, coreerr.E("DockerBuilder.Build", "failed to create output directory", err)
|
||||
}
|
||||
|
||||
// Execute build
|
||||
cmd := exec.CommandContext(ctx, "docker", args...)
|
||||
cmd.Dir = cfg.ProjectDir
|
||||
cmd.Stdout = os.Stdout
|
||||
cmd.Stderr = os.Stderr
|
||||
core.Print(nil, "Building Docker image: %s", imageName)
|
||||
core.Print(nil, " Platforms: %s", core.Join(", ", platforms...))
|
||||
core.Print(nil, " Tags: %s", core.Join(", ", imageRefs...))
|
||||
|
||||
fmt.Printf("Building Docker image: %s\n", imageName)
|
||||
fmt.Printf(" Platforms: %s\n", strings.Join(platforms, ", "))
|
||||
fmt.Printf(" Tags: %s\n", strings.Join(imageRefs, ", "))
|
||||
|
||||
if err := cmd.Run(); err != nil {
|
||||
if err := ax.ExecDir(ctx, cfg.ProjectDir, "docker", args...); err != nil {
|
||||
return nil, coreerr.E("DockerBuilder.Build", "buildx build failed", err)
|
||||
}
|
||||
|
||||
|
|
@ -185,8 +181,7 @@ func (b *DockerBuilder) Build(ctx context.Context, cfg *build.Config, targets []
|
|||
|
||||
// validateDockerCli checks if the docker CLI is available.
|
||||
func (b *DockerBuilder) validateDockerCli() error {
|
||||
cmd := exec.Command("docker", "--version")
|
||||
if err := cmd.Run(); err != nil {
|
||||
if _, err := ax.LookPath("docker"); err != nil {
|
||||
return coreerr.E("DockerBuilder.validateDockerCli", "docker CLI not found. Install it from https://docs.docker.com/get-docker/", err)
|
||||
}
|
||||
return nil
|
||||
|
|
@ -195,19 +190,14 @@ func (b *DockerBuilder) validateDockerCli() error {
|
|||
// ensureBuildx ensures docker buildx is available and has a builder.
|
||||
func (b *DockerBuilder) ensureBuildx(ctx context.Context) error {
|
||||
// Check if buildx is available
|
||||
cmd := exec.CommandContext(ctx, "docker", "buildx", "version")
|
||||
if err := cmd.Run(); err != nil {
|
||||
if err := ax.Exec(ctx, "docker", "buildx", "version"); err != nil {
|
||||
return coreerr.E("DockerBuilder.ensureBuildx", "buildx is not available. Install it from https://docs.docker.com/buildx/working-with-buildx/", err)
|
||||
}
|
||||
|
||||
// Check if we have a builder, create one if not
|
||||
cmd = exec.CommandContext(ctx, "docker", "buildx", "inspect", "--bootstrap")
|
||||
if err := cmd.Run(); err != nil {
|
||||
if err := ax.Exec(ctx, "docker", "buildx", "inspect", "--bootstrap"); err != nil {
|
||||
// Try to create a builder
|
||||
cmd = exec.CommandContext(ctx, "docker", "buildx", "create", "--use", "--bootstrap")
|
||||
cmd.Stdout = os.Stdout
|
||||
cmd.Stderr = os.Stderr
|
||||
if err := cmd.Run(); err != nil {
|
||||
if err := ax.Exec(ctx, "docker", "buildx", "create", "--use", "--bootstrap"); err != nil {
|
||||
return coreerr.E("DockerBuilder.ensureBuildx", "failed to create buildx builder", err)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,27 +1,27 @@
|
|||
package builders
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"dappco.re/go/core/build/internal/ax"
|
||||
|
||||
"dappco.re/go/core/build/pkg/build"
|
||||
"dappco.re/go/core/io"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestDockerBuilder_Name_Good(t *testing.T) {
|
||||
func TestDocker_DockerBuilderName_Good(t *testing.T) {
|
||||
builder := NewDockerBuilder()
|
||||
assert.Equal(t, "docker", builder.Name())
|
||||
}
|
||||
|
||||
func TestDockerBuilder_Detect_Good(t *testing.T) {
|
||||
func TestDocker_DockerBuilderDetect_Good(t *testing.T) {
|
||||
fs := io.Local
|
||||
|
||||
t.Run("detects Dockerfile", func(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
err := os.WriteFile(filepath.Join(dir, "Dockerfile"), []byte("FROM alpine\n"), 0644)
|
||||
err := ax.WriteFile(ax.Join(dir, "Dockerfile"), []byte("FROM alpine\n"), 0644)
|
||||
require.NoError(t, err)
|
||||
|
||||
builder := NewDockerBuilder()
|
||||
|
|
@ -42,7 +42,7 @@ func TestDockerBuilder_Detect_Good(t *testing.T) {
|
|||
t.Run("returns false for non-Docker project", func(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
// Create a Go project instead
|
||||
err := os.WriteFile(filepath.Join(dir, "go.mod"), []byte("module test"), 0644)
|
||||
err := ax.WriteFile(ax.Join(dir, "go.mod"), []byte("module test"), 0644)
|
||||
require.NoError(t, err)
|
||||
|
||||
builder := NewDockerBuilder()
|
||||
|
|
@ -53,7 +53,7 @@ func TestDockerBuilder_Detect_Good(t *testing.T) {
|
|||
|
||||
t.Run("does not match docker-compose.yml", func(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
err := os.WriteFile(filepath.Join(dir, "docker-compose.yml"), []byte("version: '3'\n"), 0644)
|
||||
err := ax.WriteFile(ax.Join(dir, "docker-compose.yml"), []byte("version: '3'\n"), 0644)
|
||||
require.NoError(t, err)
|
||||
|
||||
builder := NewDockerBuilder()
|
||||
|
|
@ -64,9 +64,9 @@ func TestDockerBuilder_Detect_Good(t *testing.T) {
|
|||
|
||||
t.Run("does not match Dockerfile in subdirectory", func(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
subDir := filepath.Join(dir, "subdir")
|
||||
require.NoError(t, os.MkdirAll(subDir, 0755))
|
||||
err := os.WriteFile(filepath.Join(subDir, "Dockerfile"), []byte("FROM alpine\n"), 0644)
|
||||
subDir := ax.Join(dir, "subdir")
|
||||
require.NoError(t, ax.MkdirAll(subDir, 0755))
|
||||
err := ax.WriteFile(ax.Join(subDir, "Dockerfile"), []byte("FROM alpine\n"), 0644)
|
||||
require.NoError(t, err)
|
||||
|
||||
builder := NewDockerBuilder()
|
||||
|
|
@ -76,7 +76,7 @@ func TestDockerBuilder_Detect_Good(t *testing.T) {
|
|||
})
|
||||
}
|
||||
|
||||
func TestDockerBuilder_Interface_Good(t *testing.T) {
|
||||
func TestDocker_DockerBuilderInterface_Good(t *testing.T) {
|
||||
// Verify DockerBuilder implements Builder interface
|
||||
var _ build.Builder = (*DockerBuilder)(nil)
|
||||
var _ build.Builder = NewDockerBuilder()
|
||||
|
|
|
|||
|
|
@ -3,32 +3,33 @@ package builders
|
|||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"dappco.re/go/core"
|
||||
"dappco.re/go/core/build/internal/ax"
|
||||
"dappco.re/go/core/build/pkg/build"
|
||||
"dappco.re/go/core/io"
|
||||
coreerr "dappco.re/go/core/log"
|
||||
)
|
||||
|
||||
// GoBuilder implements the Builder interface for Go projects.
|
||||
// Usage example: declare a value of type builders.GoBuilder in integrating code.
|
||||
type GoBuilder struct{}
|
||||
|
||||
// NewGoBuilder creates a new GoBuilder instance.
|
||||
// Usage example: call builders.NewGoBuilder(...) from integrating code.
|
||||
func NewGoBuilder() *GoBuilder {
|
||||
return &GoBuilder{}
|
||||
}
|
||||
|
||||
// Name returns the builder's identifier.
|
||||
// Usage example: call value.Name(...) from integrating code.
|
||||
func (b *GoBuilder) Name() string {
|
||||
return "go"
|
||||
}
|
||||
|
||||
// Detect checks if this builder can handle the project in the given directory.
|
||||
// Uses IsGoProject from the build package which checks for go.mod or wails.json.
|
||||
// Usage example: call value.Detect(...) from integrating code.
|
||||
func (b *GoBuilder) Detect(fs io.Medium, dir string) (bool, error) {
|
||||
return build.IsGoProject(fs, dir), nil
|
||||
}
|
||||
|
|
@ -36,6 +37,7 @@ func (b *GoBuilder) Detect(fs io.Medium, dir string) (bool, error) {
|
|||
// Build compiles the Go project for the specified targets.
|
||||
// It sets GOOS, GOARCH, and CGO_ENABLED environment variables,
|
||||
// applies ldflags and trimpath, and runs go build.
|
||||
// Usage example: call value.Build(...) from integrating code.
|
||||
func (b *GoBuilder) Build(ctx context.Context, cfg *build.Config, targets []build.Target) ([]build.Artifact, error) {
|
||||
if cfg == nil {
|
||||
return nil, coreerr.E("GoBuilder.Build", "config is nil", nil)
|
||||
|
|
@ -68,21 +70,21 @@ func (b *GoBuilder) buildTarget(ctx context.Context, cfg *build.Config, target b
|
|||
// Determine output binary name
|
||||
binaryName := cfg.Name
|
||||
if binaryName == "" {
|
||||
binaryName = filepath.Base(cfg.ProjectDir)
|
||||
binaryName = ax.Base(cfg.ProjectDir)
|
||||
}
|
||||
|
||||
// Add .exe extension for Windows
|
||||
if target.OS == "windows" && !strings.HasSuffix(binaryName, ".exe") {
|
||||
if target.OS == "windows" && !core.HasSuffix(binaryName, ".exe") {
|
||||
binaryName += ".exe"
|
||||
}
|
||||
|
||||
// Create platform-specific output path: output/os_arch/binary
|
||||
platformDir := filepath.Join(cfg.OutputDir, fmt.Sprintf("%s_%s", target.OS, target.Arch))
|
||||
platformDir := ax.Join(cfg.OutputDir, core.Sprintf("%s_%s", target.OS, target.Arch))
|
||||
if err := cfg.FS.EnsureDir(platformDir); err != nil {
|
||||
return build.Artifact{}, coreerr.E("GoBuilder.buildTarget", "failed to create platform directory", err)
|
||||
}
|
||||
|
||||
outputPath := filepath.Join(platformDir, binaryName)
|
||||
outputPath := ax.Join(platformDir, binaryName)
|
||||
|
||||
// Build the go build arguments
|
||||
args := []string{"build"}
|
||||
|
|
@ -92,7 +94,7 @@ func (b *GoBuilder) buildTarget(ctx context.Context, cfg *build.Config, target b
|
|||
|
||||
// Add ldflags if specified
|
||||
if len(cfg.LDFlags) > 0 {
|
||||
ldflags := strings.Join(cfg.LDFlags, " ")
|
||||
ldflags := core.Join(" ", cfg.LDFlags...)
|
||||
args = append(args, "-ldflags", ldflags)
|
||||
}
|
||||
|
||||
|
|
@ -102,25 +104,21 @@ func (b *GoBuilder) buildTarget(ctx context.Context, cfg *build.Config, target b
|
|||
// Add the project directory as the build target (current directory)
|
||||
args = append(args, ".")
|
||||
|
||||
// Create the command
|
||||
cmd := exec.CommandContext(ctx, "go", args...)
|
||||
cmd.Dir = cfg.ProjectDir
|
||||
|
||||
// Set up environment
|
||||
env := os.Environ()
|
||||
env = append(env, fmt.Sprintf("GOOS=%s", target.OS))
|
||||
env = append(env, fmt.Sprintf("GOARCH=%s", target.Arch))
|
||||
env := []string{
|
||||
core.Sprintf("GOOS=%s", target.OS),
|
||||
core.Sprintf("GOARCH=%s", target.Arch),
|
||||
}
|
||||
if cfg.CGO {
|
||||
env = append(env, "CGO_ENABLED=1")
|
||||
} else {
|
||||
env = append(env, "CGO_ENABLED=0")
|
||||
}
|
||||
cmd.Env = env
|
||||
|
||||
// Capture output for error messages
|
||||
output, err := cmd.CombinedOutput()
|
||||
output, err := ax.CombinedOutput(ctx, cfg.ProjectDir, env, "go", args...)
|
||||
if err != nil {
|
||||
return build.Artifact{}, coreerr.E("GoBuilder.buildTarget", "go build failed: "+string(output), err)
|
||||
return build.Artifact{}, coreerr.E("GoBuilder.buildTarget", "go build failed: "+output, err)
|
||||
}
|
||||
|
||||
return build.Artifact{
|
||||
|
|
|
|||
|
|
@ -2,11 +2,11 @@ package builders
|
|||
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"testing"
|
||||
|
||||
"dappco.re/go/core/build/internal/ax"
|
||||
|
||||
"dappco.re/go/core/build/pkg/build"
|
||||
"dappco.re/go/core/io"
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
|
@ -23,7 +23,7 @@ func setupGoTestProject(t *testing.T) string {
|
|||
|
||||
go 1.21
|
||||
`
|
||||
err := os.WriteFile(filepath.Join(dir, "go.mod"), []byte(goMod), 0644)
|
||||
err := ax.WriteFile(ax.Join(dir, "go.mod"), []byte(goMod), 0644)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Create a minimal main.go
|
||||
|
|
@ -33,22 +33,22 @@ func main() {
|
|||
println("hello")
|
||||
}
|
||||
`
|
||||
err = os.WriteFile(filepath.Join(dir, "main.go"), []byte(mainGo), 0644)
|
||||
err = ax.WriteFile(ax.Join(dir, "main.go"), []byte(mainGo), 0644)
|
||||
require.NoError(t, err)
|
||||
|
||||
return dir
|
||||
}
|
||||
|
||||
func TestGoBuilder_Name_Good(t *testing.T) {
|
||||
func TestGo_GoBuilderName_Good(t *testing.T) {
|
||||
builder := NewGoBuilder()
|
||||
assert.Equal(t, "go", builder.Name())
|
||||
}
|
||||
|
||||
func TestGoBuilder_Detect_Good(t *testing.T) {
|
||||
func TestGo_GoBuilderDetect_Good(t *testing.T) {
|
||||
fs := io.Local
|
||||
t.Run("detects Go project with go.mod", func(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
err := os.WriteFile(filepath.Join(dir, "go.mod"), []byte("module test"), 0644)
|
||||
err := ax.WriteFile(ax.Join(dir, "go.mod"), []byte("module test"), 0644)
|
||||
require.NoError(t, err)
|
||||
|
||||
builder := NewGoBuilder()
|
||||
|
|
@ -59,7 +59,7 @@ func TestGoBuilder_Detect_Good(t *testing.T) {
|
|||
|
||||
t.Run("detects Wails project", func(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
err := os.WriteFile(filepath.Join(dir, "wails.json"), []byte("{}"), 0644)
|
||||
err := ax.WriteFile(ax.Join(dir, "wails.json"), []byte("{}"), 0644)
|
||||
require.NoError(t, err)
|
||||
|
||||
builder := NewGoBuilder()
|
||||
|
|
@ -71,7 +71,7 @@ func TestGoBuilder_Detect_Good(t *testing.T) {
|
|||
t.Run("returns false for non-Go project", func(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
// Create a Node.js project instead
|
||||
err := os.WriteFile(filepath.Join(dir, "package.json"), []byte("{}"), 0644)
|
||||
err := ax.WriteFile(ax.Join(dir, "package.json"), []byte("{}"), 0644)
|
||||
require.NoError(t, err)
|
||||
|
||||
builder := NewGoBuilder()
|
||||
|
|
@ -90,7 +90,7 @@ func TestGoBuilder_Detect_Good(t *testing.T) {
|
|||
})
|
||||
}
|
||||
|
||||
func TestGoBuilder_Build_Good(t *testing.T) {
|
||||
func TestGo_GoBuilderBuild_Good(t *testing.T) {
|
||||
if testing.Short() {
|
||||
t.Skip("skipping integration test in short mode")
|
||||
}
|
||||
|
|
@ -178,7 +178,7 @@ func TestGoBuilder_Build_Good(t *testing.T) {
|
|||
require.Len(t, artifacts, 1)
|
||||
|
||||
// Verify .exe extension
|
||||
assert.True(t, filepath.Ext(artifacts[0].Path) == ".exe")
|
||||
assert.True(t, ax.Ext(artifacts[0].Path) == ".exe")
|
||||
assert.FileExists(t, artifacts[0].Path)
|
||||
})
|
||||
|
||||
|
|
@ -202,7 +202,7 @@ func TestGoBuilder_Build_Good(t *testing.T) {
|
|||
require.Len(t, artifacts, 1)
|
||||
|
||||
// Binary should use the project directory base name
|
||||
baseName := filepath.Base(projectDir)
|
||||
baseName := ax.Base(projectDir)
|
||||
if runtime.GOOS == "windows" {
|
||||
baseName += ".exe"
|
||||
}
|
||||
|
|
@ -233,7 +233,7 @@ func TestGoBuilder_Build_Good(t *testing.T) {
|
|||
|
||||
t.Run("creates output directory if missing", func(t *testing.T) {
|
||||
projectDir := setupGoTestProject(t)
|
||||
outputDir := filepath.Join(t.TempDir(), "nested", "output")
|
||||
outputDir := ax.Join(t.TempDir(), "nested", "output")
|
||||
|
||||
builder := NewGoBuilder()
|
||||
cfg := &build.Config{
|
||||
|
|
@ -254,7 +254,7 @@ func TestGoBuilder_Build_Good(t *testing.T) {
|
|||
})
|
||||
}
|
||||
|
||||
func TestGoBuilder_Build_Bad(t *testing.T) {
|
||||
func TestGo_GoBuilderBuild_Bad(t *testing.T) {
|
||||
t.Run("returns error for nil config", func(t *testing.T) {
|
||||
builder := NewGoBuilder()
|
||||
|
||||
|
|
@ -310,11 +310,11 @@ func TestGoBuilder_Build_Bad(t *testing.T) {
|
|||
dir := t.TempDir()
|
||||
|
||||
// Create go.mod
|
||||
err := os.WriteFile(filepath.Join(dir, "go.mod"), []byte("module test\n\ngo 1.21"), 0644)
|
||||
err := ax.WriteFile(ax.Join(dir, "go.mod"), []byte("module test\n\ngo 1.21"), 0644)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Create invalid Go code
|
||||
err = os.WriteFile(filepath.Join(dir, "main.go"), []byte("this is not valid go code"), 0644)
|
||||
err = ax.WriteFile(ax.Join(dir, "main.go"), []byte("this is not valid go code"), 0644)
|
||||
require.NoError(t, err)
|
||||
|
||||
builder := NewGoBuilder()
|
||||
|
|
@ -391,7 +391,7 @@ func TestGoBuilder_Build_Bad(t *testing.T) {
|
|||
})
|
||||
}
|
||||
|
||||
func TestGoBuilder_Interface_Good(t *testing.T) {
|
||||
func TestGo_GoBuilderInterface_Good(t *testing.T) {
|
||||
// Verify GoBuilder implements Builder interface
|
||||
var _ build.Builder = (*GoBuilder)(nil)
|
||||
var _ build.Builder = NewGoBuilder()
|
||||
|
|
|
|||
|
|
@ -3,43 +3,44 @@ package builders
|
|||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"dappco.re/go/core"
|
||||
"dappco.re/go/core/build/internal/ax"
|
||||
"dappco.re/go/core/build/pkg/build"
|
||||
"dappco.re/go/core/io"
|
||||
coreerr "dappco.re/go/core/log"
|
||||
)
|
||||
|
||||
// LinuxKitBuilder builds LinuxKit images.
|
||||
// Usage example: declare a value of type builders.LinuxKitBuilder in integrating code.
|
||||
type LinuxKitBuilder struct{}
|
||||
|
||||
// NewLinuxKitBuilder creates a new LinuxKit builder.
|
||||
// Usage example: call builders.NewLinuxKitBuilder(...) from integrating code.
|
||||
func NewLinuxKitBuilder() *LinuxKitBuilder {
|
||||
return &LinuxKitBuilder{}
|
||||
}
|
||||
|
||||
// Name returns the builder's identifier.
|
||||
// Usage example: call value.Name(...) from integrating code.
|
||||
func (b *LinuxKitBuilder) Name() string {
|
||||
return "linuxkit"
|
||||
}
|
||||
|
||||
// Detect checks if a linuxkit.yml or .yml config exists in the directory.
|
||||
// Usage example: call value.Detect(...) from integrating code.
|
||||
func (b *LinuxKitBuilder) Detect(fs io.Medium, dir string) (bool, error) {
|
||||
// Check for linuxkit.yml
|
||||
if fs.IsFile(filepath.Join(dir, "linuxkit.yml")) {
|
||||
if fs.IsFile(ax.Join(dir, "linuxkit.yml")) {
|
||||
return true, nil
|
||||
}
|
||||
// Check for .core/linuxkit/
|
||||
lkDir := filepath.Join(dir, ".core", "linuxkit")
|
||||
lkDir := ax.Join(dir, ".core", "linuxkit")
|
||||
if fs.IsDir(lkDir) {
|
||||
entries, err := fs.List(lkDir)
|
||||
if err == nil {
|
||||
for _, entry := range entries {
|
||||
if !entry.IsDir() && strings.HasSuffix(entry.Name(), ".yml") {
|
||||
if !entry.IsDir() && core.HasSuffix(entry.Name(), ".yml") {
|
||||
return true, nil
|
||||
}
|
||||
}
|
||||
|
|
@ -49,6 +50,7 @@ func (b *LinuxKitBuilder) Detect(fs io.Medium, dir string) (bool, error) {
|
|||
}
|
||||
|
||||
// Build builds LinuxKit images for the specified targets.
|
||||
// Usage example: call value.Build(...) from integrating code.
|
||||
func (b *LinuxKitBuilder) Build(ctx context.Context, cfg *build.Config, targets []build.Target) ([]build.Artifact, error) {
|
||||
// Validate linuxkit CLI is available
|
||||
if err := b.validateLinuxKitCli(); err != nil {
|
||||
|
|
@ -59,17 +61,17 @@ func (b *LinuxKitBuilder) Build(ctx context.Context, cfg *build.Config, targets
|
|||
configPath := cfg.LinuxKitConfig
|
||||
if configPath == "" {
|
||||
// Auto-detect
|
||||
if cfg.FS.IsFile(filepath.Join(cfg.ProjectDir, "linuxkit.yml")) {
|
||||
configPath = filepath.Join(cfg.ProjectDir, "linuxkit.yml")
|
||||
if cfg.FS.IsFile(ax.Join(cfg.ProjectDir, "linuxkit.yml")) {
|
||||
configPath = ax.Join(cfg.ProjectDir, "linuxkit.yml")
|
||||
} else {
|
||||
// Look in .core/linuxkit/
|
||||
lkDir := filepath.Join(cfg.ProjectDir, ".core", "linuxkit")
|
||||
lkDir := ax.Join(cfg.ProjectDir, ".core", "linuxkit")
|
||||
if cfg.FS.IsDir(lkDir) {
|
||||
entries, err := cfg.FS.List(lkDir)
|
||||
if err == nil {
|
||||
for _, entry := range entries {
|
||||
if !entry.IsDir() && strings.HasSuffix(entry.Name(), ".yml") {
|
||||
configPath = filepath.Join(lkDir, entry.Name())
|
||||
if !entry.IsDir() && core.HasSuffix(entry.Name(), ".yml") {
|
||||
configPath = ax.Join(lkDir, entry.Name())
|
||||
break
|
||||
}
|
||||
}
|
||||
|
|
@ -96,7 +98,7 @@ func (b *LinuxKitBuilder) Build(ctx context.Context, cfg *build.Config, targets
|
|||
// Create output directory
|
||||
outputDir := cfg.OutputDir
|
||||
if outputDir == "" {
|
||||
outputDir = filepath.Join(cfg.ProjectDir, "dist")
|
||||
outputDir = ax.Join(cfg.ProjectDir, "dist")
|
||||
}
|
||||
if err := cfg.FS.EnsureDir(outputDir); err != nil {
|
||||
return nil, coreerr.E("LinuxKitBuilder.Build", "failed to create output directory", err)
|
||||
|
|
@ -105,7 +107,7 @@ func (b *LinuxKitBuilder) Build(ctx context.Context, cfg *build.Config, targets
|
|||
// Determine base name from config file or project name
|
||||
baseName := cfg.Name
|
||||
if baseName == "" {
|
||||
baseName = strings.TrimSuffix(filepath.Base(configPath), ".yml")
|
||||
baseName = core.TrimSuffix(ax.Base(configPath), ".yml")
|
||||
}
|
||||
|
||||
// If no targets, default to linux/amd64
|
||||
|
|
@ -119,23 +121,17 @@ func (b *LinuxKitBuilder) Build(ctx context.Context, cfg *build.Config, targets
|
|||
for _, target := range targets {
|
||||
// LinuxKit only supports Linux
|
||||
if target.OS != "linux" {
|
||||
fmt.Printf("Skipping %s/%s (LinuxKit only supports Linux)\n", target.OS, target.Arch)
|
||||
core.Print(nil, "Skipping %s/%s (LinuxKit only supports Linux)", target.OS, target.Arch)
|
||||
continue
|
||||
}
|
||||
|
||||
for _, format := range formats {
|
||||
outputName := fmt.Sprintf("%s-%s", baseName, target.Arch)
|
||||
outputName := core.Sprintf("%s-%s", baseName, target.Arch)
|
||||
|
||||
args := b.buildLinuxKitArgs(configPath, format, outputName, outputDir, target.Arch)
|
||||
|
||||
cmd := exec.CommandContext(ctx, "linuxkit", args...)
|
||||
cmd.Dir = cfg.ProjectDir
|
||||
cmd.Stdout = os.Stdout
|
||||
cmd.Stderr = os.Stderr
|
||||
|
||||
fmt.Printf("Building LinuxKit image: %s (%s, %s)\n", outputName, format, target.Arch)
|
||||
|
||||
if err := cmd.Run(); err != nil {
|
||||
core.Print(nil, "Building LinuxKit image: %s (%s, %s)", outputName, format, target.Arch)
|
||||
if err := ax.ExecDir(ctx, cfg.ProjectDir, "linuxkit", args...); err != nil {
|
||||
return nil, coreerr.E("LinuxKitBuilder.Build", "build failed for "+target.Arch+"/"+format, err)
|
||||
}
|
||||
|
||||
|
|
@ -189,7 +185,7 @@ func (b *LinuxKitBuilder) buildLinuxKitArgs(configPath, format, outputName, outp
|
|||
// getArtifactPath returns the expected path of the built artifact.
|
||||
func (b *LinuxKitBuilder) getArtifactPath(outputDir, outputName, format string) string {
|
||||
ext := b.getFormatExtension(format)
|
||||
return filepath.Join(outputDir, outputName+ext)
|
||||
return ax.Join(outputDir, outputName+ext)
|
||||
}
|
||||
|
||||
// findArtifact searches for the built artifact with various naming conventions.
|
||||
|
|
@ -202,7 +198,7 @@ func (b *LinuxKitBuilder) findArtifact(fs io.Medium, outputDir, outputName, form
|
|||
}
|
||||
|
||||
for _, ext := range extensions {
|
||||
path := filepath.Join(outputDir, outputName+ext)
|
||||
path := ax.Join(outputDir, outputName+ext)
|
||||
if fs.Exists(path) {
|
||||
return path
|
||||
}
|
||||
|
|
@ -212,10 +208,10 @@ func (b *LinuxKitBuilder) findArtifact(fs io.Medium, outputDir, outputName, form
|
|||
entries, err := fs.List(outputDir)
|
||||
if err == nil {
|
||||
for _, entry := range entries {
|
||||
if strings.HasPrefix(entry.Name(), outputName) {
|
||||
match := filepath.Join(outputDir, entry.Name())
|
||||
if core.HasPrefix(entry.Name(), outputName) {
|
||||
match := ax.Join(outputDir, entry.Name())
|
||||
// Return first match that looks like an image
|
||||
ext := filepath.Ext(match)
|
||||
ext := ax.Ext(match)
|
||||
if ext == ".iso" || ext == ".qcow2" || ext == ".raw" || ext == ".vmdk" || ext == ".vhd" {
|
||||
return match
|
||||
}
|
||||
|
|
@ -244,14 +240,14 @@ func (b *LinuxKitBuilder) getFormatExtension(format string) string {
|
|||
case "aws":
|
||||
return ".raw"
|
||||
default:
|
||||
return "." + strings.TrimSuffix(format, "-bios")
|
||||
return "." + core.TrimSuffix(format, "-bios")
|
||||
}
|
||||
}
|
||||
|
||||
// validateLinuxKitCli checks if the linuxkit CLI is available.
|
||||
func (b *LinuxKitBuilder) validateLinuxKitCli() error {
|
||||
// Check PATH first
|
||||
if _, err := exec.LookPath("linuxkit"); err == nil {
|
||||
if _, err := ax.LookPath("linuxkit"); err == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,27 +1,27 @@
|
|||
package builders
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"dappco.re/go/core/build/internal/ax"
|
||||
|
||||
"dappco.re/go/core/build/pkg/build"
|
||||
"dappco.re/go/core/io"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestLinuxKitBuilder_Name_Good(t *testing.T) {
|
||||
func TestLinuxKit_LinuxKitBuilderName_Good(t *testing.T) {
|
||||
builder := NewLinuxKitBuilder()
|
||||
assert.Equal(t, "linuxkit", builder.Name())
|
||||
}
|
||||
|
||||
func TestLinuxKitBuilder_Detect_Good(t *testing.T) {
|
||||
func TestLinuxKit_LinuxKitBuilderDetect_Good(t *testing.T) {
|
||||
fs := io.Local
|
||||
|
||||
t.Run("detects linuxkit.yml in root", func(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
err := os.WriteFile(filepath.Join(dir, "linuxkit.yml"), []byte("kernel:\n image: test\n"), 0644)
|
||||
err := ax.WriteFile(ax.Join(dir, "linuxkit.yml"), []byte("kernel:\n image: test\n"), 0644)
|
||||
require.NoError(t, err)
|
||||
|
||||
builder := NewLinuxKitBuilder()
|
||||
|
|
@ -32,9 +32,9 @@ func TestLinuxKitBuilder_Detect_Good(t *testing.T) {
|
|||
|
||||
t.Run("detects .core/linuxkit/*.yml", func(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
lkDir := filepath.Join(dir, ".core", "linuxkit")
|
||||
require.NoError(t, os.MkdirAll(lkDir, 0755))
|
||||
err := os.WriteFile(filepath.Join(lkDir, "server.yml"), []byte("kernel:\n image: test\n"), 0644)
|
||||
lkDir := ax.Join(dir, ".core", "linuxkit")
|
||||
require.NoError(t, ax.MkdirAll(lkDir, 0755))
|
||||
err := ax.WriteFile(ax.Join(lkDir, "server.yml"), []byte("kernel:\n image: test\n"), 0644)
|
||||
require.NoError(t, err)
|
||||
|
||||
builder := NewLinuxKitBuilder()
|
||||
|
|
@ -45,11 +45,11 @@ func TestLinuxKitBuilder_Detect_Good(t *testing.T) {
|
|||
|
||||
t.Run("detects .core/linuxkit with multiple yml files", func(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
lkDir := filepath.Join(dir, ".core", "linuxkit")
|
||||
require.NoError(t, os.MkdirAll(lkDir, 0755))
|
||||
err := os.WriteFile(filepath.Join(lkDir, "server.yml"), []byte("kernel:\n"), 0644)
|
||||
lkDir := ax.Join(dir, ".core", "linuxkit")
|
||||
require.NoError(t, ax.MkdirAll(lkDir, 0755))
|
||||
err := ax.WriteFile(ax.Join(lkDir, "server.yml"), []byte("kernel:\n"), 0644)
|
||||
require.NoError(t, err)
|
||||
err = os.WriteFile(filepath.Join(lkDir, "desktop.yml"), []byte("kernel:\n"), 0644)
|
||||
err = ax.WriteFile(ax.Join(lkDir, "desktop.yml"), []byte("kernel:\n"), 0644)
|
||||
require.NoError(t, err)
|
||||
|
||||
builder := NewLinuxKitBuilder()
|
||||
|
|
@ -69,7 +69,7 @@ func TestLinuxKitBuilder_Detect_Good(t *testing.T) {
|
|||
|
||||
t.Run("returns false for non-LinuxKit project", func(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
err := os.WriteFile(filepath.Join(dir, "go.mod"), []byte("module test"), 0644)
|
||||
err := ax.WriteFile(ax.Join(dir, "go.mod"), []byte("module test"), 0644)
|
||||
require.NoError(t, err)
|
||||
|
||||
builder := NewLinuxKitBuilder()
|
||||
|
|
@ -80,8 +80,8 @@ func TestLinuxKitBuilder_Detect_Good(t *testing.T) {
|
|||
|
||||
t.Run("returns false for empty .core/linuxkit directory", func(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
lkDir := filepath.Join(dir, ".core", "linuxkit")
|
||||
require.NoError(t, os.MkdirAll(lkDir, 0755))
|
||||
lkDir := ax.Join(dir, ".core", "linuxkit")
|
||||
require.NoError(t, ax.MkdirAll(lkDir, 0755))
|
||||
|
||||
builder := NewLinuxKitBuilder()
|
||||
detected, err := builder.Detect(fs, dir)
|
||||
|
|
@ -91,9 +91,9 @@ func TestLinuxKitBuilder_Detect_Good(t *testing.T) {
|
|||
|
||||
t.Run("returns false when .core/linuxkit has only non-yml files", func(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
lkDir := filepath.Join(dir, ".core", "linuxkit")
|
||||
require.NoError(t, os.MkdirAll(lkDir, 0755))
|
||||
err := os.WriteFile(filepath.Join(lkDir, "README.md"), []byte("# LinuxKit\n"), 0644)
|
||||
lkDir := ax.Join(dir, ".core", "linuxkit")
|
||||
require.NoError(t, ax.MkdirAll(lkDir, 0755))
|
||||
err := ax.WriteFile(ax.Join(lkDir, "README.md"), []byte("# LinuxKit\n"), 0644)
|
||||
require.NoError(t, err)
|
||||
|
||||
builder := NewLinuxKitBuilder()
|
||||
|
|
@ -104,11 +104,11 @@ func TestLinuxKitBuilder_Detect_Good(t *testing.T) {
|
|||
|
||||
t.Run("ignores subdirectories in .core/linuxkit", func(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
lkDir := filepath.Join(dir, ".core", "linuxkit")
|
||||
subDir := filepath.Join(lkDir, "subdir")
|
||||
require.NoError(t, os.MkdirAll(subDir, 0755))
|
||||
lkDir := ax.Join(dir, ".core", "linuxkit")
|
||||
subDir := ax.Join(lkDir, "subdir")
|
||||
require.NoError(t, ax.MkdirAll(subDir, 0755))
|
||||
// Put yml in subdir only, not in lkDir itself
|
||||
err := os.WriteFile(filepath.Join(subDir, "server.yml"), []byte("kernel:\n"), 0644)
|
||||
err := ax.WriteFile(ax.Join(subDir, "server.yml"), []byte("kernel:\n"), 0644)
|
||||
require.NoError(t, err)
|
||||
|
||||
builder := NewLinuxKitBuilder()
|
||||
|
|
@ -118,7 +118,7 @@ func TestLinuxKitBuilder_Detect_Good(t *testing.T) {
|
|||
})
|
||||
}
|
||||
|
||||
func TestLinuxKitBuilder_GetFormatExtension_Good(t *testing.T) {
|
||||
func TestLinuxKit_LinuxKitBuilderGetFormatExtension_Good(t *testing.T) {
|
||||
builder := NewLinuxKitBuilder()
|
||||
|
||||
tests := []struct {
|
||||
|
|
@ -149,7 +149,7 @@ func TestLinuxKitBuilder_GetFormatExtension_Good(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestLinuxKitBuilder_GetArtifactPath_Good(t *testing.T) {
|
||||
func TestLinuxKit_LinuxKitBuilderGetArtifactPath_Good(t *testing.T) {
|
||||
builder := NewLinuxKitBuilder()
|
||||
|
||||
t.Run("constructs correct path", func(t *testing.T) {
|
||||
|
|
@ -163,7 +163,7 @@ func TestLinuxKitBuilder_GetArtifactPath_Good(t *testing.T) {
|
|||
})
|
||||
}
|
||||
|
||||
func TestLinuxKitBuilder_BuildLinuxKitArgs_Good(t *testing.T) {
|
||||
func TestLinuxKit_LinuxKitBuilderBuildLinuxKitArgs_Good(t *testing.T) {
|
||||
builder := NewLinuxKitBuilder()
|
||||
|
||||
t.Run("builds args for amd64 without --arch", func(t *testing.T) {
|
||||
|
|
@ -186,14 +186,14 @@ func TestLinuxKitBuilder_BuildLinuxKitArgs_Good(t *testing.T) {
|
|||
})
|
||||
}
|
||||
|
||||
func TestLinuxKitBuilder_FindArtifact_Good(t *testing.T) {
|
||||
func TestLinuxKit_LinuxKitBuilderFindArtifact_Good(t *testing.T) {
|
||||
fs := io.Local
|
||||
builder := NewLinuxKitBuilder()
|
||||
|
||||
t.Run("finds artifact with exact extension", func(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
artifactPath := filepath.Join(dir, "server-amd64.iso")
|
||||
require.NoError(t, os.WriteFile(artifactPath, []byte("fake iso"), 0644))
|
||||
artifactPath := ax.Join(dir, "server-amd64.iso")
|
||||
require.NoError(t, ax.WriteFile(artifactPath, []byte("fake iso"), 0644))
|
||||
|
||||
found := builder.findArtifact(fs, dir, "server-amd64", "iso")
|
||||
assert.Equal(t, artifactPath, found)
|
||||
|
|
@ -209,15 +209,15 @@ func TestLinuxKitBuilder_FindArtifact_Good(t *testing.T) {
|
|||
t.Run("finds artifact with alternate naming", func(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
// Create file matching the name prefix + known image extension
|
||||
artifactPath := filepath.Join(dir, "server-amd64.qcow2")
|
||||
require.NoError(t, os.WriteFile(artifactPath, []byte("fake qcow2"), 0644))
|
||||
artifactPath := ax.Join(dir, "server-amd64.qcow2")
|
||||
require.NoError(t, ax.WriteFile(artifactPath, []byte("fake qcow2"), 0644))
|
||||
|
||||
found := builder.findArtifact(fs, dir, "server-amd64", "qcow2")
|
||||
assert.Equal(t, artifactPath, found)
|
||||
})
|
||||
}
|
||||
|
||||
func TestLinuxKitBuilder_Interface_Good(t *testing.T) {
|
||||
func TestLinuxKit_LinuxKitBuilderInterface_Good(t *testing.T) {
|
||||
// Verify LinuxKitBuilder implements Builder interface
|
||||
var _ build.Builder = (*LinuxKitBuilder)(nil)
|
||||
var _ build.Builder = NewLinuxKitBuilder()
|
||||
|
|
|
|||
|
|
@ -3,12 +3,10 @@ package builders
|
|||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"path"
|
||||
|
||||
"dappco.re/go/core"
|
||||
"dappco.re/go/core/build/internal/ax"
|
||||
"dappco.re/go/core/build/pkg/build"
|
||||
"dappco.re/go/core/io"
|
||||
coreerr "dappco.re/go/core/log"
|
||||
|
|
@ -16,19 +14,23 @@ import (
|
|||
|
||||
// TaskfileBuilder builds projects using Taskfile (https://taskfile.dev/).
|
||||
// This is a generic builder that can handle any project type that has a Taskfile.
|
||||
// Usage example: declare a value of type builders.TaskfileBuilder in integrating code.
|
||||
type TaskfileBuilder struct{}
|
||||
|
||||
// NewTaskfileBuilder creates a new Taskfile builder.
|
||||
// Usage example: call builders.NewTaskfileBuilder(...) from integrating code.
|
||||
func NewTaskfileBuilder() *TaskfileBuilder {
|
||||
return &TaskfileBuilder{}
|
||||
}
|
||||
|
||||
// Name returns the builder's identifier.
|
||||
// Usage example: call value.Name(...) from integrating code.
|
||||
func (b *TaskfileBuilder) Name() string {
|
||||
return "taskfile"
|
||||
}
|
||||
|
||||
// Detect checks if a Taskfile exists in the directory.
|
||||
// Usage example: call value.Detect(...) from integrating code.
|
||||
func (b *TaskfileBuilder) Detect(fs io.Medium, dir string) (bool, error) {
|
||||
// Check for Taskfile.yml, Taskfile.yaml, or Taskfile
|
||||
taskfiles := []string{
|
||||
|
|
@ -40,7 +42,7 @@ func (b *TaskfileBuilder) Detect(fs io.Medium, dir string) (bool, error) {
|
|||
}
|
||||
|
||||
for _, tf := range taskfiles {
|
||||
if fs.IsFile(filepath.Join(dir, tf)) {
|
||||
if fs.IsFile(ax.Join(dir, tf)) {
|
||||
return true, nil
|
||||
}
|
||||
}
|
||||
|
|
@ -48,6 +50,7 @@ func (b *TaskfileBuilder) Detect(fs io.Medium, dir string) (bool, error) {
|
|||
}
|
||||
|
||||
// Build runs the Taskfile build task for each target platform.
|
||||
// Usage example: call value.Build(...) from integrating code.
|
||||
func (b *TaskfileBuilder) Build(ctx context.Context, cfg *build.Config, targets []build.Target) ([]build.Artifact, error) {
|
||||
// Validate task CLI is available
|
||||
if err := b.validateTaskCli(); err != nil {
|
||||
|
|
@ -57,7 +60,7 @@ func (b *TaskfileBuilder) Build(ctx context.Context, cfg *build.Config, targets
|
|||
// Create output directory
|
||||
outputDir := cfg.OutputDir
|
||||
if outputDir == "" {
|
||||
outputDir = filepath.Join(cfg.ProjectDir, "dist")
|
||||
outputDir = ax.Join(cfg.ProjectDir, "dist")
|
||||
}
|
||||
if err := cfg.FS.EnsureDir(outputDir); err != nil {
|
||||
return nil, coreerr.E("TaskfileBuilder.Build", "failed to create output directory", err)
|
||||
|
|
@ -94,54 +97,42 @@ func (b *TaskfileBuilder) Build(ctx context.Context, cfg *build.Config, targets
|
|||
func (b *TaskfileBuilder) runTask(ctx context.Context, cfg *build.Config, goos, goarch string) error {
|
||||
// Build task command
|
||||
args := []string{"build"}
|
||||
env := []string{}
|
||||
|
||||
// Pass variables if targets are specified
|
||||
if goos != "" {
|
||||
args = append(args, fmt.Sprintf("GOOS=%s", goos))
|
||||
value := core.Sprintf("GOOS=%s", goos)
|
||||
args = append(args, value)
|
||||
env = append(env, value)
|
||||
}
|
||||
if goarch != "" {
|
||||
args = append(args, fmt.Sprintf("GOARCH=%s", goarch))
|
||||
value := core.Sprintf("GOARCH=%s", goarch)
|
||||
args = append(args, value)
|
||||
env = append(env, value)
|
||||
}
|
||||
if cfg.OutputDir != "" {
|
||||
args = append(args, fmt.Sprintf("OUTPUT_DIR=%s", cfg.OutputDir))
|
||||
value := core.Sprintf("OUTPUT_DIR=%s", cfg.OutputDir)
|
||||
args = append(args, value)
|
||||
env = append(env, value)
|
||||
}
|
||||
if cfg.Name != "" {
|
||||
args = append(args, fmt.Sprintf("NAME=%s", cfg.Name))
|
||||
value := core.Sprintf("NAME=%s", cfg.Name)
|
||||
args = append(args, value)
|
||||
env = append(env, value)
|
||||
}
|
||||
if cfg.Version != "" {
|
||||
args = append(args, fmt.Sprintf("VERSION=%s", cfg.Version))
|
||||
}
|
||||
|
||||
cmd := exec.CommandContext(ctx, "task", args...)
|
||||
cmd.Dir = cfg.ProjectDir
|
||||
cmd.Stdout = os.Stdout
|
||||
cmd.Stderr = os.Stderr
|
||||
|
||||
// Set environment variables
|
||||
cmd.Env = os.Environ()
|
||||
if goos != "" {
|
||||
cmd.Env = append(cmd.Env, fmt.Sprintf("GOOS=%s", goos))
|
||||
}
|
||||
if goarch != "" {
|
||||
cmd.Env = append(cmd.Env, fmt.Sprintf("GOARCH=%s", goarch))
|
||||
}
|
||||
if cfg.OutputDir != "" {
|
||||
cmd.Env = append(cmd.Env, fmt.Sprintf("OUTPUT_DIR=%s", cfg.OutputDir))
|
||||
}
|
||||
if cfg.Name != "" {
|
||||
cmd.Env = append(cmd.Env, fmt.Sprintf("NAME=%s", cfg.Name))
|
||||
}
|
||||
if cfg.Version != "" {
|
||||
cmd.Env = append(cmd.Env, fmt.Sprintf("VERSION=%s", cfg.Version))
|
||||
value := core.Sprintf("VERSION=%s", cfg.Version)
|
||||
args = append(args, value)
|
||||
env = append(env, value)
|
||||
}
|
||||
|
||||
if goos != "" && goarch != "" {
|
||||
fmt.Printf("Running task build for %s/%s\n", goos, goarch)
|
||||
core.Print(nil, "Running task build for %s/%s", goos, goarch)
|
||||
} else {
|
||||
fmt.Println("Running task build")
|
||||
core.Print(nil, "Running task build")
|
||||
}
|
||||
|
||||
if err := cmd.Run(); err != nil {
|
||||
if err := ax.ExecWithEnv(ctx, cfg.ProjectDir, env, "task", args...); err != nil {
|
||||
return coreerr.E("TaskfileBuilder.runTask", "task build failed", err)
|
||||
}
|
||||
|
||||
|
|
@ -164,12 +155,12 @@ func (b *TaskfileBuilder) findArtifacts(fs io.Medium, outputDir string) []build.
|
|||
|
||||
// Skip common non-artifact files
|
||||
name := entry.Name()
|
||||
if strings.HasPrefix(name, ".") || name == "CHECKSUMS.txt" {
|
||||
if core.HasPrefix(name, ".") || name == "CHECKSUMS.txt" {
|
||||
continue
|
||||
}
|
||||
|
||||
artifacts = append(artifacts, build.Artifact{
|
||||
Path: filepath.Join(outputDir, name),
|
||||
Path: ax.Join(outputDir, name),
|
||||
OS: "",
|
||||
Arch: "",
|
||||
})
|
||||
|
|
@ -183,15 +174,15 @@ func (b *TaskfileBuilder) findArtifactsForTarget(fs io.Medium, outputDir string,
|
|||
var artifacts []build.Artifact
|
||||
|
||||
// 1. Look for platform-specific subdirectory: output/os_arch/
|
||||
platformSubdir := filepath.Join(outputDir, fmt.Sprintf("%s_%s", target.OS, target.Arch))
|
||||
platformSubdir := ax.Join(outputDir, core.Sprintf("%s_%s", target.OS, target.Arch))
|
||||
if fs.IsDir(platformSubdir) {
|
||||
entries, _ := fs.List(platformSubdir)
|
||||
for _, entry := range entries {
|
||||
if entry.IsDir() {
|
||||
// Handle .app bundles on macOS
|
||||
if target.OS == "darwin" && strings.HasSuffix(entry.Name(), ".app") {
|
||||
if target.OS == "darwin" && core.HasSuffix(entry.Name(), ".app") {
|
||||
artifacts = append(artifacts, build.Artifact{
|
||||
Path: filepath.Join(platformSubdir, entry.Name()),
|
||||
Path: ax.Join(platformSubdir, entry.Name()),
|
||||
OS: target.OS,
|
||||
Arch: target.Arch,
|
||||
})
|
||||
|
|
@ -199,11 +190,11 @@ func (b *TaskfileBuilder) findArtifactsForTarget(fs io.Medium, outputDir string,
|
|||
continue
|
||||
}
|
||||
// Skip hidden files
|
||||
if strings.HasPrefix(entry.Name(), ".") {
|
||||
if core.HasPrefix(entry.Name(), ".") {
|
||||
continue
|
||||
}
|
||||
artifacts = append(artifacts, build.Artifact{
|
||||
Path: filepath.Join(platformSubdir, entry.Name()),
|
||||
Path: ax.Join(platformSubdir, entry.Name()),
|
||||
OS: target.OS,
|
||||
Arch: target.Arch,
|
||||
})
|
||||
|
|
@ -215,9 +206,9 @@ func (b *TaskfileBuilder) findArtifactsForTarget(fs io.Medium, outputDir string,
|
|||
|
||||
// 2. Look for files matching the target pattern in the root output dir
|
||||
patterns := []string{
|
||||
fmt.Sprintf("*-%s-%s*", target.OS, target.Arch),
|
||||
fmt.Sprintf("*_%s_%s*", target.OS, target.Arch),
|
||||
fmt.Sprintf("*-%s*", target.Arch),
|
||||
core.Sprintf("*-%s-%s*", target.OS, target.Arch),
|
||||
core.Sprintf("*_%s_%s*", target.OS, target.Arch),
|
||||
core.Sprintf("*-%s*", target.Arch),
|
||||
}
|
||||
|
||||
for _, pattern := range patterns {
|
||||
|
|
@ -226,7 +217,7 @@ func (b *TaskfileBuilder) findArtifactsForTarget(fs io.Medium, outputDir string,
|
|||
match := entry.Name()
|
||||
// Simple glob matching
|
||||
if b.matchPattern(match, pattern) {
|
||||
fullPath := filepath.Join(outputDir, match)
|
||||
fullPath := ax.Join(outputDir, match)
|
||||
if fs.IsDir(fullPath) {
|
||||
continue
|
||||
}
|
||||
|
|
@ -249,14 +240,14 @@ func (b *TaskfileBuilder) findArtifactsForTarget(fs io.Medium, outputDir string,
|
|||
|
||||
// matchPattern implements glob matching for Taskfile artifacts.
|
||||
func (b *TaskfileBuilder) matchPattern(name, pattern string) bool {
|
||||
matched, _ := filepath.Match(pattern, name)
|
||||
matched, _ := path.Match(pattern, name)
|
||||
return matched
|
||||
}
|
||||
|
||||
// validateTaskCli checks if the task CLI is available.
|
||||
func (b *TaskfileBuilder) validateTaskCli() error {
|
||||
// Check PATH first
|
||||
if _, err := exec.LookPath("task"); err == nil {
|
||||
if _, err := ax.LookPath("task"); err == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,27 +1,27 @@
|
|||
package builders
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"dappco.re/go/core/build/internal/ax"
|
||||
|
||||
"dappco.re/go/core/build/pkg/build"
|
||||
"dappco.re/go/core/io"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestTaskfileBuilder_Name_Good(t *testing.T) {
|
||||
func TestTaskfile_TaskfileBuilderName_Good(t *testing.T) {
|
||||
builder := NewTaskfileBuilder()
|
||||
assert.Equal(t, "taskfile", builder.Name())
|
||||
}
|
||||
|
||||
func TestTaskfileBuilder_Detect_Good(t *testing.T) {
|
||||
func TestTaskfile_TaskfileBuilderDetect_Good(t *testing.T) {
|
||||
fs := io.Local
|
||||
|
||||
t.Run("detects Taskfile.yml", func(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
err := os.WriteFile(filepath.Join(dir, "Taskfile.yml"), []byte("version: '3'\n"), 0644)
|
||||
err := ax.WriteFile(ax.Join(dir, "Taskfile.yml"), []byte("version: '3'\n"), 0644)
|
||||
require.NoError(t, err)
|
||||
|
||||
builder := NewTaskfileBuilder()
|
||||
|
|
@ -32,7 +32,7 @@ func TestTaskfileBuilder_Detect_Good(t *testing.T) {
|
|||
|
||||
t.Run("detects Taskfile.yaml", func(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
err := os.WriteFile(filepath.Join(dir, "Taskfile.yaml"), []byte("version: '3'\n"), 0644)
|
||||
err := ax.WriteFile(ax.Join(dir, "Taskfile.yaml"), []byte("version: '3'\n"), 0644)
|
||||
require.NoError(t, err)
|
||||
|
||||
builder := NewTaskfileBuilder()
|
||||
|
|
@ -43,7 +43,7 @@ func TestTaskfileBuilder_Detect_Good(t *testing.T) {
|
|||
|
||||
t.Run("detects Taskfile (no extension)", func(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
err := os.WriteFile(filepath.Join(dir, "Taskfile"), []byte("version: '3'\n"), 0644)
|
||||
err := ax.WriteFile(ax.Join(dir, "Taskfile"), []byte("version: '3'\n"), 0644)
|
||||
require.NoError(t, err)
|
||||
|
||||
builder := NewTaskfileBuilder()
|
||||
|
|
@ -54,7 +54,7 @@ func TestTaskfileBuilder_Detect_Good(t *testing.T) {
|
|||
|
||||
t.Run("detects lowercase taskfile.yml", func(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
err := os.WriteFile(filepath.Join(dir, "taskfile.yml"), []byte("version: '3'\n"), 0644)
|
||||
err := ax.WriteFile(ax.Join(dir, "taskfile.yml"), []byte("version: '3'\n"), 0644)
|
||||
require.NoError(t, err)
|
||||
|
||||
builder := NewTaskfileBuilder()
|
||||
|
|
@ -65,7 +65,7 @@ func TestTaskfileBuilder_Detect_Good(t *testing.T) {
|
|||
|
||||
t.Run("detects lowercase taskfile.yaml", func(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
err := os.WriteFile(filepath.Join(dir, "taskfile.yaml"), []byte("version: '3'\n"), 0644)
|
||||
err := ax.WriteFile(ax.Join(dir, "taskfile.yaml"), []byte("version: '3'\n"), 0644)
|
||||
require.NoError(t, err)
|
||||
|
||||
builder := NewTaskfileBuilder()
|
||||
|
|
@ -85,7 +85,7 @@ func TestTaskfileBuilder_Detect_Good(t *testing.T) {
|
|||
|
||||
t.Run("returns false for non-Taskfile project", func(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
err := os.WriteFile(filepath.Join(dir, "Makefile"), []byte("all:\n\techo hello\n"), 0644)
|
||||
err := ax.WriteFile(ax.Join(dir, "Makefile"), []byte("all:\n\techo hello\n"), 0644)
|
||||
require.NoError(t, err)
|
||||
|
||||
builder := NewTaskfileBuilder()
|
||||
|
|
@ -96,9 +96,9 @@ func TestTaskfileBuilder_Detect_Good(t *testing.T) {
|
|||
|
||||
t.Run("does not match Taskfile in subdirectory", func(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
subDir := filepath.Join(dir, "subdir")
|
||||
require.NoError(t, os.MkdirAll(subDir, 0755))
|
||||
err := os.WriteFile(filepath.Join(subDir, "Taskfile.yml"), []byte("version: '3'\n"), 0644)
|
||||
subDir := ax.Join(dir, "subdir")
|
||||
require.NoError(t, ax.MkdirAll(subDir, 0755))
|
||||
err := ax.WriteFile(ax.Join(subDir, "Taskfile.yml"), []byte("version: '3'\n"), 0644)
|
||||
require.NoError(t, err)
|
||||
|
||||
builder := NewTaskfileBuilder()
|
||||
|
|
@ -108,14 +108,14 @@ func TestTaskfileBuilder_Detect_Good(t *testing.T) {
|
|||
})
|
||||
}
|
||||
|
||||
func TestTaskfileBuilder_FindArtifacts_Good(t *testing.T) {
|
||||
func TestTaskfile_TaskfileBuilderFindArtifacts_Good(t *testing.T) {
|
||||
fs := io.Local
|
||||
builder := NewTaskfileBuilder()
|
||||
|
||||
t.Run("finds files in output directory", func(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
require.NoError(t, os.WriteFile(filepath.Join(dir, "myapp"), []byte("binary"), 0755))
|
||||
require.NoError(t, os.WriteFile(filepath.Join(dir, "myapp.tar.gz"), []byte("archive"), 0644))
|
||||
require.NoError(t, ax.WriteFile(ax.Join(dir, "myapp"), []byte("binary"), 0755))
|
||||
require.NoError(t, ax.WriteFile(ax.Join(dir, "myapp.tar.gz"), []byte("archive"), 0644))
|
||||
|
||||
artifacts := builder.findArtifacts(fs, dir)
|
||||
assert.Len(t, artifacts, 2)
|
||||
|
|
@ -123,8 +123,8 @@ func TestTaskfileBuilder_FindArtifacts_Good(t *testing.T) {
|
|||
|
||||
t.Run("skips hidden files", func(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
require.NoError(t, os.WriteFile(filepath.Join(dir, "myapp"), []byte("binary"), 0755))
|
||||
require.NoError(t, os.WriteFile(filepath.Join(dir, ".hidden"), []byte("hidden"), 0644))
|
||||
require.NoError(t, ax.WriteFile(ax.Join(dir, "myapp"), []byte("binary"), 0755))
|
||||
require.NoError(t, ax.WriteFile(ax.Join(dir, ".hidden"), []byte("hidden"), 0644))
|
||||
|
||||
artifacts := builder.findArtifacts(fs, dir)
|
||||
assert.Len(t, artifacts, 1)
|
||||
|
|
@ -133,8 +133,8 @@ func TestTaskfileBuilder_FindArtifacts_Good(t *testing.T) {
|
|||
|
||||
t.Run("skips CHECKSUMS.txt", func(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
require.NoError(t, os.WriteFile(filepath.Join(dir, "myapp"), []byte("binary"), 0755))
|
||||
require.NoError(t, os.WriteFile(filepath.Join(dir, "CHECKSUMS.txt"), []byte("sha256"), 0644))
|
||||
require.NoError(t, ax.WriteFile(ax.Join(dir, "myapp"), []byte("binary"), 0755))
|
||||
require.NoError(t, ax.WriteFile(ax.Join(dir, "CHECKSUMS.txt"), []byte("sha256"), 0644))
|
||||
|
||||
artifacts := builder.findArtifacts(fs, dir)
|
||||
assert.Len(t, artifacts, 1)
|
||||
|
|
@ -143,8 +143,8 @@ func TestTaskfileBuilder_FindArtifacts_Good(t *testing.T) {
|
|||
|
||||
t.Run("skips directories", func(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
require.NoError(t, os.WriteFile(filepath.Join(dir, "myapp"), []byte("binary"), 0755))
|
||||
require.NoError(t, os.MkdirAll(filepath.Join(dir, "subdir"), 0755))
|
||||
require.NoError(t, ax.WriteFile(ax.Join(dir, "myapp"), []byte("binary"), 0755))
|
||||
require.NoError(t, ax.MkdirAll(ax.Join(dir, "subdir"), 0755))
|
||||
|
||||
artifacts := builder.findArtifacts(fs, dir)
|
||||
assert.Len(t, artifacts, 1)
|
||||
|
|
@ -163,15 +163,15 @@ func TestTaskfileBuilder_FindArtifacts_Good(t *testing.T) {
|
|||
})
|
||||
}
|
||||
|
||||
func TestTaskfileBuilder_FindArtifactsForTarget_Good(t *testing.T) {
|
||||
func TestTaskfile_TaskfileBuilderFindArtifactsForTarget_Good(t *testing.T) {
|
||||
fs := io.Local
|
||||
builder := NewTaskfileBuilder()
|
||||
|
||||
t.Run("finds artifacts in platform subdirectory", func(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
platformDir := filepath.Join(dir, "linux_amd64")
|
||||
require.NoError(t, os.MkdirAll(platformDir, 0755))
|
||||
require.NoError(t, os.WriteFile(filepath.Join(platformDir, "myapp"), []byte("binary"), 0755))
|
||||
platformDir := ax.Join(dir, "linux_amd64")
|
||||
require.NoError(t, ax.MkdirAll(platformDir, 0755))
|
||||
require.NoError(t, ax.WriteFile(ax.Join(platformDir, "myapp"), []byte("binary"), 0755))
|
||||
|
||||
target := build.Target{OS: "linux", Arch: "amd64"}
|
||||
artifacts := builder.findArtifactsForTarget(fs, dir, target)
|
||||
|
|
@ -182,7 +182,7 @@ func TestTaskfileBuilder_FindArtifactsForTarget_Good(t *testing.T) {
|
|||
|
||||
t.Run("finds artifacts by name pattern in root", func(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
require.NoError(t, os.WriteFile(filepath.Join(dir, "myapp-linux-amd64"), []byte("binary"), 0755))
|
||||
require.NoError(t, ax.WriteFile(ax.Join(dir, "myapp-linux-amd64"), []byte("binary"), 0755))
|
||||
|
||||
target := build.Target{OS: "linux", Arch: "amd64"}
|
||||
artifacts := builder.findArtifactsForTarget(fs, dir, target)
|
||||
|
|
@ -191,7 +191,7 @@ func TestTaskfileBuilder_FindArtifactsForTarget_Good(t *testing.T) {
|
|||
|
||||
t.Run("returns empty when no matching artifacts", func(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
require.NoError(t, os.WriteFile(filepath.Join(dir, "myapp"), []byte("binary"), 0755))
|
||||
require.NoError(t, ax.WriteFile(ax.Join(dir, "myapp"), []byte("binary"), 0755))
|
||||
|
||||
target := build.Target{OS: "linux", Arch: "arm64"}
|
||||
artifacts := builder.findArtifactsForTarget(fs, dir, target)
|
||||
|
|
@ -200,9 +200,9 @@ func TestTaskfileBuilder_FindArtifactsForTarget_Good(t *testing.T) {
|
|||
|
||||
t.Run("handles .app bundles on darwin", func(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
platformDir := filepath.Join(dir, "darwin_arm64")
|
||||
appDir := filepath.Join(platformDir, "MyApp.app")
|
||||
require.NoError(t, os.MkdirAll(appDir, 0755))
|
||||
platformDir := ax.Join(dir, "darwin_arm64")
|
||||
appDir := ax.Join(platformDir, "MyApp.app")
|
||||
require.NoError(t, ax.MkdirAll(appDir, 0755))
|
||||
|
||||
target := build.Target{OS: "darwin", Arch: "arm64"}
|
||||
artifacts := builder.findArtifactsForTarget(fs, dir, target)
|
||||
|
|
@ -211,7 +211,7 @@ func TestTaskfileBuilder_FindArtifactsForTarget_Good(t *testing.T) {
|
|||
})
|
||||
}
|
||||
|
||||
func TestTaskfileBuilder_MatchPattern_Good(t *testing.T) {
|
||||
func TestTaskfile_TaskfileBuilderMatchPattern_Good(t *testing.T) {
|
||||
builder := NewTaskfileBuilder()
|
||||
|
||||
t.Run("matches simple glob", func(t *testing.T) {
|
||||
|
|
@ -227,7 +227,7 @@ func TestTaskfileBuilder_MatchPattern_Good(t *testing.T) {
|
|||
})
|
||||
}
|
||||
|
||||
func TestTaskfileBuilder_Interface_Good(t *testing.T) {
|
||||
func TestTaskfile_TaskfileBuilderInterface_Good(t *testing.T) {
|
||||
// Verify TaskfileBuilder implements Builder interface
|
||||
var _ build.Builder = (*TaskfileBuilder)(nil)
|
||||
var _ build.Builder = NewTaskfileBuilder()
|
||||
|
|
|
|||
|
|
@ -3,31 +3,33 @@ package builders
|
|||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"dappco.re/go/core"
|
||||
"dappco.re/go/core/build/internal/ax"
|
||||
"dappco.re/go/core/build/pkg/build"
|
||||
"dappco.re/go/core/io"
|
||||
coreerr "dappco.re/go/core/log"
|
||||
)
|
||||
|
||||
// WailsBuilder implements the Builder interface for Wails v3 projects.
|
||||
// Usage example: declare a value of type builders.WailsBuilder in integrating code.
|
||||
type WailsBuilder struct{}
|
||||
|
||||
// NewWailsBuilder creates a new WailsBuilder instance.
|
||||
// Usage example: call builders.NewWailsBuilder(...) from integrating code.
|
||||
func NewWailsBuilder() *WailsBuilder {
|
||||
return &WailsBuilder{}
|
||||
}
|
||||
|
||||
// Name returns the builder's identifier.
|
||||
// Usage example: call value.Name(...) from integrating code.
|
||||
func (b *WailsBuilder) Name() string {
|
||||
return "wails"
|
||||
}
|
||||
|
||||
// Detect checks if this builder can handle the project in the given directory.
|
||||
// Uses IsWailsProject from the build package which checks for wails.json.
|
||||
// Usage example: call value.Detect(...) from integrating code.
|
||||
func (b *WailsBuilder) Detect(fs io.Medium, dir string) (bool, error) {
|
||||
return build.IsWailsProject(fs, dir), nil
|
||||
}
|
||||
|
|
@ -36,6 +38,7 @@ func (b *WailsBuilder) Detect(fs io.Medium, dir string) (bool, error) {
|
|||
// It detects the Wails version and chooses the appropriate build strategy:
|
||||
// - Wails v3: Delegates to Taskfile (error if missing)
|
||||
// - Wails v2: Uses 'wails build' command
|
||||
// Usage example: call value.Build(...) from integrating code.
|
||||
func (b *WailsBuilder) Build(ctx context.Context, cfg *build.Config, targets []build.Target) ([]build.Artifact, error) {
|
||||
if cfg == nil {
|
||||
return nil, coreerr.E("WailsBuilder.Build", "config is nil", nil)
|
||||
|
|
@ -83,12 +86,12 @@ func (b *WailsBuilder) Build(ctx context.Context, cfg *build.Config, targets []b
|
|||
|
||||
// isWailsV3 checks if the project uses Wails v3 by inspecting go.mod.
|
||||
func (b *WailsBuilder) isWailsV3(fs io.Medium, dir string) bool {
|
||||
goModPath := filepath.Join(dir, "go.mod")
|
||||
goModPath := ax.Join(dir, "go.mod")
|
||||
content, err := fs.Read(goModPath)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
return strings.Contains(content, "github.com/wailsapp/wails/v3")
|
||||
return core.Contains(content, "github.com/wailsapp/wails/v3")
|
||||
}
|
||||
|
||||
// buildV2Target compiles for a single target platform using wails (v2).
|
||||
|
|
@ -96,35 +99,31 @@ func (b *WailsBuilder) buildV2Target(ctx context.Context, cfg *build.Config, tar
|
|||
// Determine output binary name
|
||||
binaryName := cfg.Name
|
||||
if binaryName == "" {
|
||||
binaryName = filepath.Base(cfg.ProjectDir)
|
||||
binaryName = ax.Base(cfg.ProjectDir)
|
||||
}
|
||||
|
||||
// Build the wails build arguments
|
||||
args := []string{"build"}
|
||||
|
||||
// Platform
|
||||
args = append(args, "-platform", fmt.Sprintf("%s/%s", target.OS, target.Arch))
|
||||
args = append(args, "-platform", core.Sprintf("%s/%s", target.OS, target.Arch))
|
||||
|
||||
// Output (Wails v2 uses -o for the binary name, relative to build/bin usually, but we want to control it)
|
||||
// Actually, Wails v2 is opinionated about output dir (build/bin).
|
||||
// We might need to copy artifacts after build if we want them in cfg.OutputDir.
|
||||
// For now, let's try to let Wails do its thing and find the artifact.
|
||||
|
||||
// Create the command
|
||||
cmd := exec.CommandContext(ctx, "wails", args...)
|
||||
cmd.Dir = cfg.ProjectDir
|
||||
|
||||
// Capture output for error messages
|
||||
output, err := cmd.CombinedOutput()
|
||||
output, err := ax.CombinedOutput(ctx, cfg.ProjectDir, nil, "wails", args...)
|
||||
if err != nil {
|
||||
return build.Artifact{}, coreerr.E("WailsBuilder.buildV2Target", "wails build failed: "+string(output), err)
|
||||
return build.Artifact{}, coreerr.E("WailsBuilder.buildV2Target", "wails build failed: "+output, err)
|
||||
}
|
||||
|
||||
// Wails v2 typically outputs to build/bin
|
||||
// We need to move/copy it to our desired output dir
|
||||
|
||||
// Construct the source path where Wails v2 puts the binary
|
||||
wailsOutputDir := filepath.Join(cfg.ProjectDir, "build", "bin")
|
||||
wailsOutputDir := ax.Join(cfg.ProjectDir, "build", "bin")
|
||||
|
||||
// Find the artifact in Wails output dir
|
||||
sourcePath, err := b.findArtifact(cfg.FS, wailsOutputDir, binaryName, target)
|
||||
|
|
@ -134,12 +133,12 @@ func (b *WailsBuilder) buildV2Target(ctx context.Context, cfg *build.Config, tar
|
|||
|
||||
// Move/Copy to our output dir
|
||||
// Create platform specific dir in our output
|
||||
platformDir := filepath.Join(cfg.OutputDir, fmt.Sprintf("%s_%s", target.OS, target.Arch))
|
||||
platformDir := ax.Join(cfg.OutputDir, core.Sprintf("%s_%s", target.OS, target.Arch))
|
||||
if err := cfg.FS.EnsureDir(platformDir); err != nil {
|
||||
return build.Artifact{}, coreerr.E("WailsBuilder.buildV2Target", "failed to create output dir", err)
|
||||
}
|
||||
|
||||
destPath := filepath.Join(platformDir, filepath.Base(sourcePath))
|
||||
destPath := ax.Join(platformDir, ax.Base(sourcePath))
|
||||
|
||||
// Simple copy using the medium
|
||||
content, err := cfg.FS.Read(sourcePath)
|
||||
|
|
@ -165,21 +164,21 @@ func (b *WailsBuilder) findArtifact(fs io.Medium, platformDir, binaryName string
|
|||
case "windows":
|
||||
// Look for NSIS installer first, then plain exe
|
||||
candidates = []string{
|
||||
filepath.Join(platformDir, binaryName+"-installer.exe"),
|
||||
filepath.Join(platformDir, binaryName+".exe"),
|
||||
filepath.Join(platformDir, binaryName+"-amd64-installer.exe"),
|
||||
ax.Join(platformDir, binaryName+"-installer.exe"),
|
||||
ax.Join(platformDir, binaryName+".exe"),
|
||||
ax.Join(platformDir, binaryName+"-amd64-installer.exe"),
|
||||
}
|
||||
case "darwin":
|
||||
// Look for .dmg, then .app bundle, then plain binary
|
||||
candidates = []string{
|
||||
filepath.Join(platformDir, binaryName+".dmg"),
|
||||
filepath.Join(platformDir, binaryName+".app"),
|
||||
filepath.Join(platformDir, binaryName),
|
||||
ax.Join(platformDir, binaryName+".dmg"),
|
||||
ax.Join(platformDir, binaryName+".app"),
|
||||
ax.Join(platformDir, binaryName),
|
||||
}
|
||||
default:
|
||||
// Linux and others: look for plain binary
|
||||
candidates = []string{
|
||||
filepath.Join(platformDir, binaryName),
|
||||
ax.Join(platformDir, binaryName),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -199,11 +198,11 @@ func (b *WailsBuilder) findArtifact(fs io.Medium, platformDir, binaryName string
|
|||
for _, entry := range entries {
|
||||
name := entry.Name()
|
||||
// Skip common non-artifact files
|
||||
if strings.HasSuffix(name, ".go") || strings.HasSuffix(name, ".json") {
|
||||
if core.HasSuffix(name, ".go") || core.HasSuffix(name, ".json") {
|
||||
continue
|
||||
}
|
||||
|
||||
path := filepath.Join(platformDir, name)
|
||||
path := ax.Join(platformDir, name)
|
||||
info, err := entry.Info()
|
||||
if err != nil {
|
||||
continue
|
||||
|
|
@ -211,7 +210,7 @@ func (b *WailsBuilder) findArtifact(fs io.Medium, platformDir, binaryName string
|
|||
|
||||
// On Unix, check if it's executable; on Windows, check for .exe
|
||||
if target.OS == "windows" {
|
||||
if strings.HasSuffix(name, ".exe") {
|
||||
if core.HasSuffix(name, ".exe") {
|
||||
return path, nil
|
||||
}
|
||||
} else if info.Mode()&0111 != 0 || entry.IsDir() {
|
||||
|
|
@ -238,7 +237,7 @@ func detectPackageManager(fs io.Medium, dir string) string {
|
|||
}
|
||||
|
||||
for _, lf := range lockFiles {
|
||||
if fs.IsFile(filepath.Join(dir, lf.file)) {
|
||||
if fs.IsFile(ax.Join(dir, lf.file)) {
|
||||
return lf.manager
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,12 +2,10 @@ package builders
|
|||
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"testing"
|
||||
|
||||
"dappco.re/go/core/build/internal/ax"
|
||||
"dappco.re/go/core/build/pkg/build"
|
||||
"dappco.re/go/core/io"
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
|
@ -24,7 +22,7 @@ func setupWailsTestProject(t *testing.T) string {
|
|||
"name": "testapp",
|
||||
"outputfilename": "testapp"
|
||||
}`
|
||||
err := os.WriteFile(filepath.Join(dir, "wails.json"), []byte(wailsJSON), 0644)
|
||||
err := ax.WriteFile(ax.Join(dir, "wails.json"), []byte(wailsJSON), 0o644)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Create a minimal go.mod
|
||||
|
|
@ -34,7 +32,7 @@ go 1.21
|
|||
|
||||
require github.com/wailsapp/wails/v3 v3.0.0
|
||||
`
|
||||
err = os.WriteFile(filepath.Join(dir, "go.mod"), []byte(goMod), 0644)
|
||||
err = ax.WriteFile(ax.Join(dir, "go.mod"), []byte(goMod), 0o644)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Create a minimal main.go
|
||||
|
|
@ -44,7 +42,7 @@ func main() {
|
|||
println("hello wails")
|
||||
}
|
||||
`
|
||||
err = os.WriteFile(filepath.Join(dir, "main.go"), []byte(mainGo), 0644)
|
||||
err = ax.WriteFile(ax.Join(dir, "main.go"), []byte(mainGo), 0o644)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Create a minimal Taskfile.yml
|
||||
|
|
@ -55,7 +53,7 @@ tasks:
|
|||
- mkdir -p {{.OUTPUT_DIR}}/{{.GOOS}}_{{.GOARCH}}
|
||||
- touch {{.OUTPUT_DIR}}/{{.GOOS}}_{{.GOARCH}}/testapp
|
||||
`
|
||||
err = os.WriteFile(filepath.Join(dir, "Taskfile.yml"), []byte(taskfile), 0644)
|
||||
err = ax.WriteFile(ax.Join(dir, "Taskfile.yml"), []byte(taskfile), 0o644)
|
||||
require.NoError(t, err)
|
||||
|
||||
return dir
|
||||
|
|
@ -67,7 +65,7 @@ func setupWailsV2TestProject(t *testing.T) string {
|
|||
dir := t.TempDir()
|
||||
|
||||
// wails.json
|
||||
err := os.WriteFile(filepath.Join(dir, "wails.json"), []byte("{}"), 0644)
|
||||
err := ax.WriteFile(ax.Join(dir, "wails.json"), []byte("{}"), 0o644)
|
||||
require.NoError(t, err)
|
||||
|
||||
// go.mod with v2
|
||||
|
|
@ -75,19 +73,19 @@ func setupWailsV2TestProject(t *testing.T) string {
|
|||
go 1.21
|
||||
require github.com/wailsapp/wails/v2 v2.8.0
|
||||
`
|
||||
err = os.WriteFile(filepath.Join(dir, "go.mod"), []byte(goMod), 0644)
|
||||
err = ax.WriteFile(ax.Join(dir, "go.mod"), []byte(goMod), 0o644)
|
||||
require.NoError(t, err)
|
||||
|
||||
return dir
|
||||
}
|
||||
|
||||
func TestWailsBuilder_Build_Taskfile_Good(t *testing.T) {
|
||||
func TestWails_WailsBuilderBuildTaskfile_Good(t *testing.T) {
|
||||
if testing.Short() {
|
||||
t.Skip("skipping integration test in short mode")
|
||||
}
|
||||
|
||||
// Check if task is available
|
||||
if _, err := exec.LookPath("task"); err != nil {
|
||||
if _, err := ax.LookPath("task"); err != nil {
|
||||
t.Skip("task not installed, skipping test")
|
||||
}
|
||||
|
||||
|
|
@ -104,7 +102,7 @@ tasks:
|
|||
- mkdir -p {{.OUTPUT_DIR}}/{{.GOOS}}_{{.GOARCH}}
|
||||
- touch {{.OUTPUT_DIR}}/{{.GOOS}}_{{.GOARCH}}/testapp
|
||||
`
|
||||
err := os.WriteFile(filepath.Join(projectDir, "Taskfile.yml"), []byte(taskfile), 0644)
|
||||
err := ax.WriteFile(ax.Join(projectDir, "Taskfile.yml"), []byte(taskfile), 0o644)
|
||||
require.NoError(t, err)
|
||||
|
||||
builder := NewWailsBuilder()
|
||||
|
|
@ -124,17 +122,17 @@ tasks:
|
|||
})
|
||||
}
|
||||
|
||||
func TestWailsBuilder_Name_Good(t *testing.T) {
|
||||
func TestWails_WailsBuilderName_Good(t *testing.T) {
|
||||
builder := NewWailsBuilder()
|
||||
assert.Equal(t, "wails", builder.Name())
|
||||
}
|
||||
|
||||
func TestWailsBuilder_Build_V2_Good(t *testing.T) {
|
||||
func TestWails_WailsBuilderBuildV2_Good(t *testing.T) {
|
||||
if testing.Short() {
|
||||
t.Skip("skipping integration test in short mode")
|
||||
}
|
||||
|
||||
if _, err := exec.LookPath("wails"); err != nil {
|
||||
if _, err := ax.LookPath("wails"); err != nil {
|
||||
t.Skip("wails not installed, skipping integration test")
|
||||
}
|
||||
|
||||
|
|
@ -162,11 +160,11 @@ func TestWailsBuilder_Build_V2_Good(t *testing.T) {
|
|||
})
|
||||
}
|
||||
|
||||
func TestWailsBuilder_Detect_Good(t *testing.T) {
|
||||
func TestWails_WailsBuilderDetect_Good(t *testing.T) {
|
||||
fs := io.Local
|
||||
t.Run("detects Wails project with wails.json", func(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
err := os.WriteFile(filepath.Join(dir, "wails.json"), []byte("{}"), 0644)
|
||||
err := ax.WriteFile(ax.Join(dir, "wails.json"), []byte("{}"), 0o644)
|
||||
require.NoError(t, err)
|
||||
|
||||
builder := NewWailsBuilder()
|
||||
|
|
@ -177,7 +175,7 @@ func TestWailsBuilder_Detect_Good(t *testing.T) {
|
|||
|
||||
t.Run("returns false for Go-only project", func(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
err := os.WriteFile(filepath.Join(dir, "go.mod"), []byte("module test"), 0644)
|
||||
err := ax.WriteFile(ax.Join(dir, "go.mod"), []byte("module test"), 0o644)
|
||||
require.NoError(t, err)
|
||||
|
||||
builder := NewWailsBuilder()
|
||||
|
|
@ -188,7 +186,7 @@ func TestWailsBuilder_Detect_Good(t *testing.T) {
|
|||
|
||||
t.Run("returns false for Node.js project", func(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
err := os.WriteFile(filepath.Join(dir, "package.json"), []byte("{}"), 0644)
|
||||
err := ax.WriteFile(ax.Join(dir, "package.json"), []byte("{}"), 0o644)
|
||||
require.NoError(t, err)
|
||||
|
||||
builder := NewWailsBuilder()
|
||||
|
|
@ -207,11 +205,11 @@ func TestWailsBuilder_Detect_Good(t *testing.T) {
|
|||
})
|
||||
}
|
||||
|
||||
func TestDetectPackageManager_Good(t *testing.T) {
|
||||
func TestWails_DetectPackageManager_Good(t *testing.T) {
|
||||
fs := io.Local
|
||||
t.Run("detects bun from bun.lockb", func(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
err := os.WriteFile(filepath.Join(dir, "bun.lockb"), []byte(""), 0644)
|
||||
err := ax.WriteFile(ax.Join(dir, "bun.lockb"), []byte(""), 0o644)
|
||||
require.NoError(t, err)
|
||||
|
||||
result := detectPackageManager(fs, dir)
|
||||
|
|
@ -220,7 +218,7 @@ func TestDetectPackageManager_Good(t *testing.T) {
|
|||
|
||||
t.Run("detects pnpm from pnpm-lock.yaml", func(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
err := os.WriteFile(filepath.Join(dir, "pnpm-lock.yaml"), []byte(""), 0644)
|
||||
err := ax.WriteFile(ax.Join(dir, "pnpm-lock.yaml"), []byte(""), 0o644)
|
||||
require.NoError(t, err)
|
||||
|
||||
result := detectPackageManager(fs, dir)
|
||||
|
|
@ -229,7 +227,7 @@ func TestDetectPackageManager_Good(t *testing.T) {
|
|||
|
||||
t.Run("detects yarn from yarn.lock", func(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
err := os.WriteFile(filepath.Join(dir, "yarn.lock"), []byte(""), 0644)
|
||||
err := ax.WriteFile(ax.Join(dir, "yarn.lock"), []byte(""), 0o644)
|
||||
require.NoError(t, err)
|
||||
|
||||
result := detectPackageManager(fs, dir)
|
||||
|
|
@ -238,7 +236,7 @@ func TestDetectPackageManager_Good(t *testing.T) {
|
|||
|
||||
t.Run("detects npm from package-lock.json", func(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
err := os.WriteFile(filepath.Join(dir, "package-lock.json"), []byte(""), 0644)
|
||||
err := ax.WriteFile(ax.Join(dir, "package-lock.json"), []byte(""), 0o644)
|
||||
require.NoError(t, err)
|
||||
|
||||
result := detectPackageManager(fs, dir)
|
||||
|
|
@ -255,9 +253,9 @@ func TestDetectPackageManager_Good(t *testing.T) {
|
|||
t.Run("prefers bun over other lock files", func(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
// Create multiple lock files
|
||||
require.NoError(t, os.WriteFile(filepath.Join(dir, "bun.lockb"), []byte(""), 0644))
|
||||
require.NoError(t, os.WriteFile(filepath.Join(dir, "yarn.lock"), []byte(""), 0644))
|
||||
require.NoError(t, os.WriteFile(filepath.Join(dir, "package-lock.json"), []byte(""), 0644))
|
||||
require.NoError(t, ax.WriteFile(ax.Join(dir, "bun.lockb"), []byte(""), 0o644))
|
||||
require.NoError(t, ax.WriteFile(ax.Join(dir, "yarn.lock"), []byte(""), 0o644))
|
||||
require.NoError(t, ax.WriteFile(ax.Join(dir, "package-lock.json"), []byte(""), 0o644))
|
||||
|
||||
result := detectPackageManager(fs, dir)
|
||||
assert.Equal(t, "bun", result)
|
||||
|
|
@ -266,9 +264,9 @@ func TestDetectPackageManager_Good(t *testing.T) {
|
|||
t.Run("prefers pnpm over yarn and npm", func(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
// Create multiple lock files (no bun)
|
||||
require.NoError(t, os.WriteFile(filepath.Join(dir, "pnpm-lock.yaml"), []byte(""), 0644))
|
||||
require.NoError(t, os.WriteFile(filepath.Join(dir, "yarn.lock"), []byte(""), 0644))
|
||||
require.NoError(t, os.WriteFile(filepath.Join(dir, "package-lock.json"), []byte(""), 0644))
|
||||
require.NoError(t, ax.WriteFile(ax.Join(dir, "pnpm-lock.yaml"), []byte(""), 0o644))
|
||||
require.NoError(t, ax.WriteFile(ax.Join(dir, "yarn.lock"), []byte(""), 0o644))
|
||||
require.NoError(t, ax.WriteFile(ax.Join(dir, "package-lock.json"), []byte(""), 0o644))
|
||||
|
||||
result := detectPackageManager(fs, dir)
|
||||
assert.Equal(t, "pnpm", result)
|
||||
|
|
@ -277,15 +275,15 @@ func TestDetectPackageManager_Good(t *testing.T) {
|
|||
t.Run("prefers yarn over npm", func(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
// Create multiple lock files (no bun or pnpm)
|
||||
require.NoError(t, os.WriteFile(filepath.Join(dir, "yarn.lock"), []byte(""), 0644))
|
||||
require.NoError(t, os.WriteFile(filepath.Join(dir, "package-lock.json"), []byte(""), 0644))
|
||||
require.NoError(t, ax.WriteFile(ax.Join(dir, "yarn.lock"), []byte(""), 0o644))
|
||||
require.NoError(t, ax.WriteFile(ax.Join(dir, "package-lock.json"), []byte(""), 0o644))
|
||||
|
||||
result := detectPackageManager(fs, dir)
|
||||
assert.Equal(t, "yarn", result)
|
||||
})
|
||||
}
|
||||
|
||||
func TestWailsBuilder_Build_Bad(t *testing.T) {
|
||||
func TestWails_WailsBuilderBuild_Bad(t *testing.T) {
|
||||
t.Run("returns error for nil config", func(t *testing.T) {
|
||||
builder := NewWailsBuilder()
|
||||
|
||||
|
|
@ -313,13 +311,13 @@ func TestWailsBuilder_Build_Bad(t *testing.T) {
|
|||
})
|
||||
}
|
||||
|
||||
func TestWailsBuilder_Build_Good(t *testing.T) {
|
||||
func TestWails_WailsBuilderBuild_Good(t *testing.T) {
|
||||
if testing.Short() {
|
||||
t.Skip("skipping integration test in short mode")
|
||||
}
|
||||
|
||||
// Check if wails3 is available in PATH
|
||||
if _, err := exec.LookPath("wails3"); err != nil {
|
||||
if _, err := ax.LookPath("wails3"); err != nil {
|
||||
t.Skip("wails3 not installed, skipping integration test")
|
||||
}
|
||||
|
||||
|
|
@ -349,13 +347,13 @@ func TestWailsBuilder_Build_Good(t *testing.T) {
|
|||
})
|
||||
}
|
||||
|
||||
func TestWailsBuilder_Interface_Good(t *testing.T) {
|
||||
func TestWails_WailsBuilderInterface_Good(t *testing.T) {
|
||||
// Verify WailsBuilder implements Builder interface
|
||||
var _ build.Builder = (*WailsBuilder)(nil)
|
||||
var _ build.Builder = NewWailsBuilder()
|
||||
}
|
||||
|
||||
func TestWailsBuilder_Ugly(t *testing.T) {
|
||||
func TestWails_WailsBuilder_Ugly(t *testing.T) {
|
||||
t.Run("handles nonexistent frontend directory gracefully", func(t *testing.T) {
|
||||
if testing.Short() {
|
||||
t.Skip("skipping integration test in short mode")
|
||||
|
|
@ -363,7 +361,7 @@ func TestWailsBuilder_Ugly(t *testing.T) {
|
|||
|
||||
// Create a Wails project without a frontend directory
|
||||
dir := t.TempDir()
|
||||
err := os.WriteFile(filepath.Join(dir, "wails.json"), []byte("{}"), 0644)
|
||||
err := ax.WriteFile(ax.Join(dir, "wails.json"), []byte("{}"), 0o644)
|
||||
require.NoError(t, err)
|
||||
|
||||
builder := NewWailsBuilder()
|
||||
|
|
|
|||
|
|
@ -4,18 +4,16 @@ package build
|
|||
import (
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"io"
|
||||
"path/filepath"
|
||||
"slices"
|
||||
|
||||
"strings"
|
||||
"sort"
|
||||
|
||||
"dappco.re/go/core"
|
||||
io_interface "dappco.re/go/core/io"
|
||||
coreerr "dappco.re/go/core/log"
|
||||
)
|
||||
|
||||
// Checksum computes SHA256 for an artifact and returns the artifact with the Checksum field filled.
|
||||
// Usage example: call build.Checksum(...) from integrating code.
|
||||
func Checksum(fs io_interface.Medium, artifact Artifact) (Artifact, error) {
|
||||
if artifact.Path == "" {
|
||||
return Artifact{}, coreerr.E("build.Checksum", "artifact path is empty", nil)
|
||||
|
|
@ -46,6 +44,7 @@ func Checksum(fs io_interface.Medium, artifact Artifact) (Artifact, error) {
|
|||
|
||||
// ChecksumAll computes checksums for all artifacts.
|
||||
// Returns a slice of artifacts with their Checksum fields filled.
|
||||
// Usage example: call build.ChecksumAll(...) from integrating code.
|
||||
func ChecksumAll(fs io_interface.Medium, artifacts []Artifact) ([]Artifact, error) {
|
||||
if len(artifacts) == 0 {
|
||||
return nil, nil
|
||||
|
|
@ -70,6 +69,7 @@ func ChecksumAll(fs io_interface.Medium, artifacts []Artifact) ([]Artifact, erro
|
|||
//
|
||||
// The artifacts should have their Checksum fields filled (call ChecksumAll first).
|
||||
// Filenames are relative to the output directory (just the basename).
|
||||
// Usage example: call build.WriteChecksumFile(...) from integrating code.
|
||||
func WriteChecksumFile(fs io_interface.Medium, artifacts []Artifact, path string) error {
|
||||
if len(artifacts) == 0 {
|
||||
return nil
|
||||
|
|
@ -81,14 +81,14 @@ func WriteChecksumFile(fs io_interface.Medium, artifacts []Artifact, path string
|
|||
if artifact.Checksum == "" {
|
||||
return coreerr.E("build.WriteChecksumFile", "artifact "+artifact.Path+" has no checksum", nil)
|
||||
}
|
||||
filename := filepath.Base(artifact.Path)
|
||||
lines = append(lines, fmt.Sprintf("%s %s", artifact.Checksum, filename))
|
||||
filename := core.PathBase(artifact.Path)
|
||||
lines = append(lines, core.Sprintf("%s %s", artifact.Checksum, filename))
|
||||
}
|
||||
|
||||
// Sort lines for consistent output
|
||||
slices.Sort(lines)
|
||||
sort.Strings(lines)
|
||||
|
||||
content := strings.Join(lines, "\n") + "\n"
|
||||
content := core.Concat(core.Join("\n", lines...), "\n")
|
||||
|
||||
// Write the file using the medium (which handles directory creation in Write)
|
||||
if err := fs.Write(path, content); err != nil {
|
||||
|
|
|
|||
|
|
@ -1,11 +1,10 @@
|
|||
package build
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"dappco.re/go/core"
|
||||
"dappco.re/go/core/build/internal/ax"
|
||||
"dappco.re/go/core/io"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
|
@ -16,14 +15,14 @@ func setupChecksumTestFile(t *testing.T, content string) string {
|
|||
t.Helper()
|
||||
|
||||
dir := t.TempDir()
|
||||
path := filepath.Join(dir, "testfile")
|
||||
err := os.WriteFile(path, []byte(content), 0644)
|
||||
path := ax.Join(dir, "testfile")
|
||||
err := ax.WriteFile(path, []byte(content), 0644)
|
||||
require.NoError(t, err)
|
||||
|
||||
return path
|
||||
}
|
||||
|
||||
func TestChecksum_Good(t *testing.T) {
|
||||
func TestChecksum_Checksum_Good(t *testing.T) {
|
||||
fs := io.Local
|
||||
t.Run("computes SHA256 checksum", func(t *testing.T) {
|
||||
// Known SHA256 of "Hello, World!\n"
|
||||
|
|
@ -99,7 +98,7 @@ func TestChecksum_Good(t *testing.T) {
|
|||
})
|
||||
}
|
||||
|
||||
func TestChecksum_Bad(t *testing.T) {
|
||||
func TestChecksum_Checksum_Bad(t *testing.T) {
|
||||
fs := io.Local
|
||||
t.Run("returns error for empty path", func(t *testing.T) {
|
||||
artifact := Artifact{
|
||||
|
|
@ -128,7 +127,7 @@ func TestChecksum_Bad(t *testing.T) {
|
|||
})
|
||||
}
|
||||
|
||||
func TestChecksumAll_Good(t *testing.T) {
|
||||
func TestChecksum_ChecksumAll_Good(t *testing.T) {
|
||||
fs := io.Local
|
||||
t.Run("checksums multiple artifacts", func(t *testing.T) {
|
||||
paths := []string{
|
||||
|
|
@ -168,7 +167,7 @@ func TestChecksumAll_Good(t *testing.T) {
|
|||
})
|
||||
}
|
||||
|
||||
func TestChecksumAll_Bad(t *testing.T) {
|
||||
func TestChecksum_ChecksumAll_Bad(t *testing.T) {
|
||||
fs := io.Local
|
||||
t.Run("returns partial results on error", func(t *testing.T) {
|
||||
path := setupChecksumTestFile(t, "valid content")
|
||||
|
|
@ -186,11 +185,11 @@ func TestChecksumAll_Bad(t *testing.T) {
|
|||
})
|
||||
}
|
||||
|
||||
func TestWriteChecksumFile_Good(t *testing.T) {
|
||||
func TestChecksum_WriteChecksumFile_Good(t *testing.T) {
|
||||
fs := io.Local
|
||||
t.Run("writes checksum file with correct format", func(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
checksumPath := filepath.Join(dir, "CHECKSUMS.txt")
|
||||
checksumPath := ax.Join(dir, "CHECKSUMS.txt")
|
||||
|
||||
artifacts := []Artifact{
|
||||
{Path: "/output/app_linux_amd64.tar.gz", Checksum: "abc123def456", OS: "linux", Arch: "amd64"},
|
||||
|
|
@ -201,10 +200,10 @@ func TestWriteChecksumFile_Good(t *testing.T) {
|
|||
require.NoError(t, err)
|
||||
|
||||
// Read and verify content
|
||||
content, err := os.ReadFile(checksumPath)
|
||||
content, err := ax.ReadFile(checksumPath)
|
||||
require.NoError(t, err)
|
||||
|
||||
lines := strings.Split(strings.TrimSpace(string(content)), "\n")
|
||||
lines := core.Split(core.Trim(string(content)), "\n")
|
||||
require.Len(t, lines, 2)
|
||||
|
||||
// Lines should be sorted alphabetically
|
||||
|
|
@ -214,7 +213,7 @@ func TestWriteChecksumFile_Good(t *testing.T) {
|
|||
|
||||
t.Run("creates parent directories", func(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
checksumPath := filepath.Join(dir, "nested", "deep", "CHECKSUMS.txt")
|
||||
checksumPath := ax.Join(dir, "nested", "deep", "CHECKSUMS.txt")
|
||||
|
||||
artifacts := []Artifact{
|
||||
{Path: "/output/app.tar.gz", Checksum: "abc123", OS: "linux", Arch: "amd64"},
|
||||
|
|
@ -227,19 +226,18 @@ func TestWriteChecksumFile_Good(t *testing.T) {
|
|||
|
||||
t.Run("does nothing for empty artifacts", func(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
checksumPath := filepath.Join(dir, "CHECKSUMS.txt")
|
||||
checksumPath := ax.Join(dir, "CHECKSUMS.txt")
|
||||
|
||||
err := WriteChecksumFile(fs, []Artifact{}, checksumPath)
|
||||
require.NoError(t, err)
|
||||
|
||||
// File should not exist
|
||||
_, err = os.Stat(checksumPath)
|
||||
assert.True(t, os.IsNotExist(err))
|
||||
assert.False(t, ax.Exists(checksumPath))
|
||||
})
|
||||
|
||||
t.Run("does nothing for nil artifacts", func(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
checksumPath := filepath.Join(dir, "CHECKSUMS.txt")
|
||||
checksumPath := ax.Join(dir, "CHECKSUMS.txt")
|
||||
|
||||
err := WriteChecksumFile(fs, nil, checksumPath)
|
||||
require.NoError(t, err)
|
||||
|
|
@ -247,7 +245,7 @@ func TestWriteChecksumFile_Good(t *testing.T) {
|
|||
|
||||
t.Run("uses only basename for filenames", func(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
checksumPath := filepath.Join(dir, "CHECKSUMS.txt")
|
||||
checksumPath := ax.Join(dir, "CHECKSUMS.txt")
|
||||
|
||||
artifacts := []Artifact{
|
||||
{Path: "/some/deep/nested/path/myapp_linux_amd64.tar.gz", Checksum: "checksum123", OS: "linux", Arch: "amd64"},
|
||||
|
|
@ -256,7 +254,7 @@ func TestWriteChecksumFile_Good(t *testing.T) {
|
|||
err := WriteChecksumFile(fs, artifacts, checksumPath)
|
||||
require.NoError(t, err)
|
||||
|
||||
content, err := os.ReadFile(checksumPath)
|
||||
content, err := ax.ReadFile(checksumPath)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Should only contain the basename
|
||||
|
|
@ -265,11 +263,11 @@ func TestWriteChecksumFile_Good(t *testing.T) {
|
|||
})
|
||||
}
|
||||
|
||||
func TestWriteChecksumFile_Bad(t *testing.T) {
|
||||
func TestChecksum_WriteChecksumFile_Bad(t *testing.T) {
|
||||
fs := io.Local
|
||||
t.Run("returns error for artifact without checksum", func(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
checksumPath := filepath.Join(dir, "CHECKSUMS.txt")
|
||||
checksumPath := ax.Join(dir, "CHECKSUMS.txt")
|
||||
|
||||
artifacts := []Artifact{
|
||||
{Path: "/output/app.tar.gz", Checksum: "", OS: "linux", Arch: "amd64"}, // No checksum
|
||||
|
|
|
|||
|
|
@ -4,9 +4,8 @@ package build
|
|||
|
||||
import (
|
||||
"iter"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"dappco.re/go/core/build/internal/ax"
|
||||
"dappco.re/go/core/build/pkg/build/signing"
|
||||
"dappco.re/go/core/io"
|
||||
coreerr "dappco.re/go/core/log"
|
||||
|
|
@ -14,13 +13,16 @@ import (
|
|||
)
|
||||
|
||||
// ConfigFileName is the name of the build configuration file.
|
||||
// Usage example: reference build.ConfigFileName from package consumers.
|
||||
const ConfigFileName = "build.yaml"
|
||||
|
||||
// ConfigDir is the directory where build configuration is stored.
|
||||
// Usage example: reference build.ConfigDir from package consumers.
|
||||
const ConfigDir = ".core"
|
||||
|
||||
// BuildConfig holds the complete build configuration loaded from .core/build.yaml.
|
||||
// This is distinct from Config which holds runtime build parameters.
|
||||
// Usage example: declare a value of type build.BuildConfig in integrating code.
|
||||
type BuildConfig struct {
|
||||
// Version is the config file format version.
|
||||
Version int `yaml:"version"`
|
||||
|
|
@ -35,6 +37,7 @@ type BuildConfig struct {
|
|||
}
|
||||
|
||||
// Project holds project metadata.
|
||||
// Usage example: declare a value of type build.Project in integrating code.
|
||||
type Project struct {
|
||||
// Name is the project name.
|
||||
Name string `yaml:"name"`
|
||||
|
|
@ -47,6 +50,7 @@ type Project struct {
|
|||
}
|
||||
|
||||
// Build holds build-time settings.
|
||||
// Usage example: declare a value of type build.Build in integrating code.
|
||||
type Build struct {
|
||||
// Type overrides project type auto-detection (e.g., "go", "wails", "docker").
|
||||
Type string `yaml:"type"`
|
||||
|
|
@ -62,6 +66,7 @@ type Build struct {
|
|||
|
||||
// TargetConfig defines a build target in the config file.
|
||||
// This is separate from Target to allow for additional config-specific fields.
|
||||
// Usage example: declare a value of type build.TargetConfig in integrating code.
|
||||
type TargetConfig struct {
|
||||
// OS is the target operating system (e.g., "linux", "darwin", "windows").
|
||||
OS string `yaml:"os"`
|
||||
|
|
@ -72,12 +77,13 @@ type TargetConfig struct {
|
|||
// LoadConfig loads build configuration from the .core/build.yaml file in the given directory.
|
||||
// If the config file does not exist, it returns DefaultConfig().
|
||||
// Returns an error if the file exists but cannot be parsed.
|
||||
// Usage example: call build.LoadConfig(...) from integrating code.
|
||||
func LoadConfig(fs io.Medium, dir string) (*BuildConfig, error) {
|
||||
configPath := filepath.Join(dir, ConfigDir, ConfigFileName)
|
||||
configPath := ax.Join(dir, ConfigDir, ConfigFileName)
|
||||
|
||||
content, err := fs.Read(configPath)
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
if !fs.Exists(configPath) {
|
||||
return DefaultConfig(), nil
|
||||
}
|
||||
return nil, coreerr.E("build.LoadConfig", "failed to read config file", err)
|
||||
|
|
@ -96,6 +102,7 @@ func LoadConfig(fs io.Medium, dir string) (*BuildConfig, error) {
|
|||
}
|
||||
|
||||
// DefaultConfig returns sensible defaults for Go projects.
|
||||
// Usage example: call build.DefaultConfig(...) from integrating code.
|
||||
func DefaultConfig() *BuildConfig {
|
||||
return &BuildConfig{
|
||||
Version: 1,
|
||||
|
|
@ -153,16 +160,19 @@ func applyDefaults(cfg *BuildConfig) {
|
|||
}
|
||||
|
||||
// ConfigPath returns the path to the build config file for a given directory.
|
||||
// Usage example: call build.ConfigPath(...) from integrating code.
|
||||
func ConfigPath(dir string) string {
|
||||
return filepath.Join(dir, ConfigDir, ConfigFileName)
|
||||
return ax.Join(dir, ConfigDir, ConfigFileName)
|
||||
}
|
||||
|
||||
// ConfigExists checks if a build config file exists in the given directory.
|
||||
// Usage example: call build.ConfigExists(...) from integrating code.
|
||||
func ConfigExists(fs io.Medium, dir string) bool {
|
||||
return fileExists(fs, ConfigPath(dir))
|
||||
}
|
||||
|
||||
// TargetsIter returns an iterator for the build targets.
|
||||
// Usage example: call value.TargetsIter(...) from integrating code.
|
||||
func (cfg *BuildConfig) TargetsIter() iter.Seq[TargetConfig] {
|
||||
return func(yield func(TargetConfig) bool) {
|
||||
for _, t := range cfg.Targets {
|
||||
|
|
@ -174,6 +184,7 @@ func (cfg *BuildConfig) TargetsIter() iter.Seq[TargetConfig] {
|
|||
}
|
||||
|
||||
// ToTargets converts TargetConfig slice to Target slice for use with builders.
|
||||
// Usage example: call value.ToTargets(...) from integrating code.
|
||||
func (cfg *BuildConfig) ToTargets() []Target {
|
||||
targets := make([]Target, len(cfg.Targets))
|
||||
for i, t := range cfg.Targets {
|
||||
|
|
|
|||
|
|
@ -1,10 +1,10 @@
|
|||
package build
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"dappco.re/go/core/build/internal/ax"
|
||||
|
||||
"dappco.re/go/core/io"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
|
@ -16,19 +16,19 @@ func setupConfigTestDir(t *testing.T, configContent string) string {
|
|||
dir := t.TempDir()
|
||||
|
||||
if configContent != "" {
|
||||
coreDir := filepath.Join(dir, ConfigDir)
|
||||
err := os.MkdirAll(coreDir, 0755)
|
||||
coreDir := ax.Join(dir, ConfigDir)
|
||||
err := ax.MkdirAll(coreDir, 0755)
|
||||
require.NoError(t, err)
|
||||
|
||||
configPath := filepath.Join(coreDir, ConfigFileName)
|
||||
err = os.WriteFile(configPath, []byte(configContent), 0644)
|
||||
configPath := ax.Join(coreDir, ConfigFileName)
|
||||
err = ax.WriteFile(configPath, []byte(configContent), 0644)
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
return dir
|
||||
}
|
||||
|
||||
func TestLoadConfig_Good(t *testing.T) {
|
||||
func TestConfig_LoadConfig_Good(t *testing.T) {
|
||||
fs := io.Local
|
||||
t.Run("loads valid config", func(t *testing.T) {
|
||||
content := `
|
||||
|
|
@ -142,7 +142,7 @@ targets:
|
|||
})
|
||||
}
|
||||
|
||||
func TestLoadConfig_Bad(t *testing.T) {
|
||||
func TestConfig_LoadConfig_Bad(t *testing.T) {
|
||||
fs := io.Local
|
||||
t.Run("returns error for invalid YAML", func(t *testing.T) {
|
||||
content := `
|
||||
|
|
@ -160,13 +160,13 @@ project:
|
|||
|
||||
t.Run("returns error for unreadable file", func(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
coreDir := filepath.Join(dir, ConfigDir)
|
||||
err := os.MkdirAll(coreDir, 0755)
|
||||
coreDir := ax.Join(dir, ConfigDir)
|
||||
err := ax.MkdirAll(coreDir, 0755)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Create config as a directory instead of file
|
||||
configPath := filepath.Join(coreDir, ConfigFileName)
|
||||
err = os.Mkdir(configPath, 0755)
|
||||
configPath := ax.Join(coreDir, ConfigFileName)
|
||||
err = ax.Mkdir(configPath, 0755)
|
||||
require.NoError(t, err)
|
||||
|
||||
cfg, err := LoadConfig(fs, dir)
|
||||
|
|
@ -176,7 +176,7 @@ project:
|
|||
})
|
||||
}
|
||||
|
||||
func TestDefaultConfig_Good(t *testing.T) {
|
||||
func TestConfig_DefaultConfig_Good(t *testing.T) {
|
||||
t.Run("returns sensible defaults", func(t *testing.T) {
|
||||
cfg := DefaultConfig()
|
||||
|
||||
|
|
@ -212,14 +212,14 @@ func TestDefaultConfig_Good(t *testing.T) {
|
|||
})
|
||||
}
|
||||
|
||||
func TestConfigPath_Good(t *testing.T) {
|
||||
func TestConfig_ConfigPath_Good(t *testing.T) {
|
||||
t.Run("returns correct path", func(t *testing.T) {
|
||||
path := ConfigPath("/project/root")
|
||||
assert.Equal(t, "/project/root/.core/build.yaml", path)
|
||||
})
|
||||
}
|
||||
|
||||
func TestConfigExists_Good(t *testing.T) {
|
||||
func TestConfig_ConfigExists_Good(t *testing.T) {
|
||||
fs := io.Local
|
||||
t.Run("returns true when config exists", func(t *testing.T) {
|
||||
dir := setupConfigTestDir(t, "version: 1")
|
||||
|
|
@ -237,10 +237,10 @@ func TestConfigExists_Good(t *testing.T) {
|
|||
})
|
||||
}
|
||||
|
||||
func TestLoadConfig_Good_SignConfig(t *testing.T) {
|
||||
func TestConfig_LoadConfigSignConfig_Good(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
coreDir := filepath.Join(tmpDir, ".core")
|
||||
_ = os.MkdirAll(coreDir, 0755)
|
||||
coreDir := ax.Join(tmpDir, ".core")
|
||||
_ = ax.MkdirAll(coreDir, 0755)
|
||||
|
||||
configContent := `version: 1
|
||||
sign:
|
||||
|
|
@ -251,7 +251,7 @@ sign:
|
|||
identity: "Developer ID Application: Test"
|
||||
notarize: true
|
||||
`
|
||||
_ = os.WriteFile(filepath.Join(coreDir, "build.yaml"), []byte(configContent), 0644)
|
||||
_ = ax.WriteFile(ax.Join(coreDir, "build.yaml"), []byte(configContent), 0644)
|
||||
|
||||
cfg, err := LoadConfig(io.Local, tmpDir)
|
||||
if err != nil {
|
||||
|
|
@ -272,7 +272,7 @@ sign:
|
|||
}
|
||||
}
|
||||
|
||||
func TestBuildConfig_ToTargets_Good(t *testing.T) {
|
||||
func TestConfig_BuildConfigToTargets_Good(t *testing.T) {
|
||||
t.Run("converts TargetConfig to Target", func(t *testing.T) {
|
||||
cfg := &BuildConfig{
|
||||
Targets: []TargetConfig{
|
||||
|
|
@ -301,9 +301,9 @@ func TestBuildConfig_ToTargets_Good(t *testing.T) {
|
|||
}
|
||||
|
||||
// TestLoadConfig_Testdata tests loading from the testdata fixture.
|
||||
func TestLoadConfig_Testdata(t *testing.T) {
|
||||
func TestConfig_LoadConfigTestdata_Good(t *testing.T) {
|
||||
fs := io.Local
|
||||
abs, err := filepath.Abs("testdata/config-project")
|
||||
abs, err := ax.Abs("testdata/config-project")
|
||||
require.NoError(t, err)
|
||||
|
||||
t.Run("loads config-project fixture", func(t *testing.T) {
|
||||
|
|
|
|||
|
|
@ -1,9 +1,8 @@
|
|||
package build
|
||||
|
||||
import (
|
||||
"path/filepath"
|
||||
"slices"
|
||||
|
||||
"dappco.re/go/core"
|
||||
"dappco.re/go/core/build/internal/ax"
|
||||
"dappco.re/go/core/io"
|
||||
)
|
||||
|
||||
|
|
@ -33,14 +32,15 @@ var markers = []projectMarker{
|
|||
// Discover detects project types in the given directory by checking for marker files.
|
||||
// Returns a slice of detected project types, ordered by priority (most specific first).
|
||||
// For example, a Wails project returns [wails, go] since it has both wails.json and go.mod.
|
||||
// Usage example: call build.Discover(...) from integrating code.
|
||||
func Discover(fs io.Medium, dir string) ([]ProjectType, error) {
|
||||
var detected []ProjectType
|
||||
|
||||
for _, m := range markers {
|
||||
path := filepath.Join(dir, m.file)
|
||||
path := ax.Join(dir, m.file)
|
||||
if fileExists(fs, path) {
|
||||
// Avoid duplicates (shouldn't happen with current markers, but defensive)
|
||||
if !slices.Contains(detected, m.projectType) {
|
||||
if !core.NewArray(detected...).Contains(m.projectType) {
|
||||
detected = append(detected, m.projectType)
|
||||
}
|
||||
}
|
||||
|
|
@ -51,6 +51,7 @@ func Discover(fs io.Medium, dir string) ([]ProjectType, error) {
|
|||
|
||||
// PrimaryType returns the most specific project type detected in the directory.
|
||||
// Returns empty string if no project type is detected.
|
||||
// Usage example: call build.PrimaryType(...) from integrating code.
|
||||
func PrimaryType(fs io.Medium, dir string) (ProjectType, error) {
|
||||
types, err := Discover(fs, dir)
|
||||
if err != nil {
|
||||
|
|
@ -63,29 +64,34 @@ func PrimaryType(fs io.Medium, dir string) (ProjectType, error) {
|
|||
}
|
||||
|
||||
// IsGoProject checks if the directory contains a Go project (go.mod or wails.json).
|
||||
// Usage example: call build.IsGoProject(...) from integrating code.
|
||||
func IsGoProject(fs io.Medium, dir string) bool {
|
||||
return fileExists(fs, filepath.Join(dir, markerGoMod)) ||
|
||||
fileExists(fs, filepath.Join(dir, markerWails))
|
||||
return fileExists(fs, ax.Join(dir, markerGoMod)) ||
|
||||
fileExists(fs, ax.Join(dir, markerWails))
|
||||
}
|
||||
|
||||
// IsWailsProject checks if the directory contains a Wails project.
|
||||
// Usage example: call build.IsWailsProject(...) from integrating code.
|
||||
func IsWailsProject(fs io.Medium, dir string) bool {
|
||||
return fileExists(fs, filepath.Join(dir, markerWails))
|
||||
return fileExists(fs, ax.Join(dir, markerWails))
|
||||
}
|
||||
|
||||
// IsNodeProject checks if the directory contains a Node.js project.
|
||||
// Usage example: call build.IsNodeProject(...) from integrating code.
|
||||
func IsNodeProject(fs io.Medium, dir string) bool {
|
||||
return fileExists(fs, filepath.Join(dir, markerNodePackage))
|
||||
return fileExists(fs, ax.Join(dir, markerNodePackage))
|
||||
}
|
||||
|
||||
// IsPHPProject checks if the directory contains a PHP project.
|
||||
// Usage example: call build.IsPHPProject(...) from integrating code.
|
||||
func IsPHPProject(fs io.Medium, dir string) bool {
|
||||
return fileExists(fs, filepath.Join(dir, markerComposer))
|
||||
return fileExists(fs, ax.Join(dir, markerComposer))
|
||||
}
|
||||
|
||||
// IsCPPProject checks if the directory contains a C++ project (CMakeLists.txt).
|
||||
// Usage example: call build.IsCPPProject(...) from integrating code.
|
||||
func IsCPPProject(fs io.Medium, dir string) bool {
|
||||
return fileExists(fs, filepath.Join(dir, "CMakeLists.txt"))
|
||||
return fileExists(fs, ax.Join(dir, "CMakeLists.txt"))
|
||||
}
|
||||
|
||||
// fileExists checks if a file exists and is not a directory.
|
||||
|
|
|
|||
|
|
@ -1,10 +1,10 @@
|
|||
package build
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"dappco.re/go/core/build/internal/ax"
|
||||
|
||||
"dappco.re/go/core/io"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
|
@ -15,14 +15,14 @@ func setupTestDir(t *testing.T, markers ...string) string {
|
|||
t.Helper()
|
||||
dir := t.TempDir()
|
||||
for _, m := range markers {
|
||||
path := filepath.Join(dir, m)
|
||||
err := os.WriteFile(path, []byte("{}"), 0644)
|
||||
path := ax.Join(dir, m)
|
||||
err := ax.WriteFile(path, []byte("{}"), 0644)
|
||||
require.NoError(t, err)
|
||||
}
|
||||
return dir
|
||||
}
|
||||
|
||||
func TestDiscover_Good(t *testing.T) {
|
||||
func TestDiscovery_Discover_Good(t *testing.T) {
|
||||
fs := io.Local
|
||||
t.Run("detects Go project", func(t *testing.T) {
|
||||
dir := setupTestDir(t, "go.mod")
|
||||
|
|
@ -67,18 +67,18 @@ func TestDiscover_Good(t *testing.T) {
|
|||
})
|
||||
}
|
||||
|
||||
func TestDiscover_Bad(t *testing.T) {
|
||||
func TestDiscovery_Discover_Bad(t *testing.T) {
|
||||
fs := io.Local
|
||||
t.Run("non-existent directory returns empty slice", func(t *testing.T) {
|
||||
types, err := Discover(fs, "/non/existent/path")
|
||||
assert.NoError(t, err) // os.Stat fails silently in fileExists
|
||||
assert.NoError(t, err) // ax.Stat fails silently in fileExists
|
||||
assert.Empty(t, types)
|
||||
})
|
||||
|
||||
t.Run("directory marker is ignored", func(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
// Create go.mod as a directory instead of a file
|
||||
err := os.Mkdir(filepath.Join(dir, "go.mod"), 0755)
|
||||
err := ax.Mkdir(ax.Join(dir, "go.mod"), 0755)
|
||||
require.NoError(t, err)
|
||||
|
||||
types, err := Discover(fs, dir)
|
||||
|
|
@ -87,7 +87,7 @@ func TestDiscover_Bad(t *testing.T) {
|
|||
})
|
||||
}
|
||||
|
||||
func TestPrimaryType_Good(t *testing.T) {
|
||||
func TestDiscovery_PrimaryType_Good(t *testing.T) {
|
||||
fs := io.Local
|
||||
t.Run("returns wails for wails project", func(t *testing.T) {
|
||||
dir := setupTestDir(t, "wails.json", "go.mod")
|
||||
|
|
@ -111,7 +111,7 @@ func TestPrimaryType_Good(t *testing.T) {
|
|||
})
|
||||
}
|
||||
|
||||
func TestIsGoProject_Good(t *testing.T) {
|
||||
func TestDiscovery_IsGoProject_Good(t *testing.T) {
|
||||
fs := io.Local
|
||||
t.Run("true with go.mod", func(t *testing.T) {
|
||||
dir := setupTestDir(t, "go.mod")
|
||||
|
|
@ -129,7 +129,7 @@ func TestIsGoProject_Good(t *testing.T) {
|
|||
})
|
||||
}
|
||||
|
||||
func TestIsWailsProject_Good(t *testing.T) {
|
||||
func TestDiscovery_IsWailsProject_Good(t *testing.T) {
|
||||
fs := io.Local
|
||||
t.Run("true with wails.json", func(t *testing.T) {
|
||||
dir := setupTestDir(t, "wails.json")
|
||||
|
|
@ -142,7 +142,7 @@ func TestIsWailsProject_Good(t *testing.T) {
|
|||
})
|
||||
}
|
||||
|
||||
func TestIsNodeProject_Good(t *testing.T) {
|
||||
func TestDiscovery_IsNodeProject_Good(t *testing.T) {
|
||||
fs := io.Local
|
||||
t.Run("true with package.json", func(t *testing.T) {
|
||||
dir := setupTestDir(t, "package.json")
|
||||
|
|
@ -155,7 +155,7 @@ func TestIsNodeProject_Good(t *testing.T) {
|
|||
})
|
||||
}
|
||||
|
||||
func TestIsPHPProject_Good(t *testing.T) {
|
||||
func TestDiscovery_IsPHPProject_Good(t *testing.T) {
|
||||
fs := io.Local
|
||||
t.Run("true with composer.json", func(t *testing.T) {
|
||||
dir := setupTestDir(t, "composer.json")
|
||||
|
|
@ -168,17 +168,17 @@ func TestIsPHPProject_Good(t *testing.T) {
|
|||
})
|
||||
}
|
||||
|
||||
func TestTarget_Good(t *testing.T) {
|
||||
func TestDiscovery_Target_Good(t *testing.T) {
|
||||
target := Target{OS: "linux", Arch: "amd64"}
|
||||
assert.Equal(t, "linux/amd64", target.String())
|
||||
}
|
||||
|
||||
func TestFileExists_Good(t *testing.T) {
|
||||
func TestDiscovery_FileExists_Good(t *testing.T) {
|
||||
fs := io.Local
|
||||
t.Run("returns true for existing file", func(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
path := filepath.Join(dir, "test.txt")
|
||||
err := os.WriteFile(path, []byte("content"), 0644)
|
||||
path := ax.Join(dir, "test.txt")
|
||||
err := ax.WriteFile(path, []byte("content"), 0644)
|
||||
require.NoError(t, err)
|
||||
assert.True(t, fileExists(fs, path))
|
||||
})
|
||||
|
|
@ -195,9 +195,9 @@ func TestFileExists_Good(t *testing.T) {
|
|||
|
||||
// TestDiscover_Testdata tests discovery using the testdata fixtures.
|
||||
// These serve as integration tests with realistic project structures.
|
||||
func TestDiscover_Testdata(t *testing.T) {
|
||||
func TestDiscovery_DiscoverTestdata_Good(t *testing.T) {
|
||||
fs := io.Local
|
||||
testdataDir, err := filepath.Abs("testdata")
|
||||
testdataDir, err := ax.Abs("testdata")
|
||||
require.NoError(t, err)
|
||||
|
||||
tests := []struct {
|
||||
|
|
@ -215,7 +215,7 @@ func TestDiscover_Testdata(t *testing.T) {
|
|||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
dir := filepath.Join(testdataDir, tt.dir)
|
||||
dir := ax.Join(testdataDir, tt.dir)
|
||||
types, err := Discover(fs, dir)
|
||||
assert.NoError(t, err)
|
||||
if len(tt.expected) == 0 {
|
||||
|
|
|
|||
|
|
@ -2,14 +2,15 @@ package signing
|
|||
|
||||
import (
|
||||
"context"
|
||||
"os/exec"
|
||||
"runtime"
|
||||
|
||||
"dappco.re/go/core/build/internal/ax"
|
||||
"dappco.re/go/core/io"
|
||||
coreerr "dappco.re/go/core/log"
|
||||
)
|
||||
|
||||
// MacOSSigner signs binaries using macOS codesign.
|
||||
// Usage example: declare a value of type signing.MacOSSigner in integrating code.
|
||||
type MacOSSigner struct {
|
||||
config MacOSConfig
|
||||
}
|
||||
|
|
@ -18,16 +19,19 @@ type MacOSSigner struct {
|
|||
var _ Signer = (*MacOSSigner)(nil)
|
||||
|
||||
// NewMacOSSigner creates a new macOS signer.
|
||||
// Usage example: call signing.NewMacOSSigner(...) from integrating code.
|
||||
func NewMacOSSigner(cfg MacOSConfig) *MacOSSigner {
|
||||
return &MacOSSigner{config: cfg}
|
||||
}
|
||||
|
||||
// Name returns "codesign".
|
||||
// Usage example: call value.Name(...) from integrating code.
|
||||
func (s *MacOSSigner) Name() string {
|
||||
return "codesign"
|
||||
}
|
||||
|
||||
// Available checks if running on macOS with codesign and identity configured.
|
||||
// Usage example: call value.Available(...) from integrating code.
|
||||
func (s *MacOSSigner) Available() bool {
|
||||
if runtime.GOOS != "darwin" {
|
||||
return false
|
||||
|
|
@ -35,11 +39,12 @@ func (s *MacOSSigner) Available() bool {
|
|||
if s.config.Identity == "" {
|
||||
return false
|
||||
}
|
||||
_, err := exec.LookPath("codesign")
|
||||
_, err := ax.LookPath("codesign")
|
||||
return err == nil
|
||||
}
|
||||
|
||||
// Sign codesigns a binary with hardened runtime.
|
||||
// Usage example: call value.Sign(...) from integrating code.
|
||||
func (s *MacOSSigner) Sign(ctx context.Context, fs io.Medium, binary string) error {
|
||||
if !s.Available() {
|
||||
if runtime.GOOS != "darwin" {
|
||||
|
|
@ -51,17 +56,15 @@ func (s *MacOSSigner) Sign(ctx context.Context, fs io.Medium, binary string) err
|
|||
return coreerr.E("codesign.Sign", "codesign tool not found in PATH", nil)
|
||||
}
|
||||
|
||||
cmd := exec.CommandContext(ctx, "codesign",
|
||||
output, err := ax.CombinedOutput(ctx, "", nil, "codesign",
|
||||
"--sign", s.config.Identity,
|
||||
"--timestamp",
|
||||
"--options", "runtime", // Hardened runtime for notarization
|
||||
"--force",
|
||||
binary,
|
||||
)
|
||||
|
||||
output, err := cmd.CombinedOutput()
|
||||
if err != nil {
|
||||
return coreerr.E("codesign.Sign", string(output), err)
|
||||
return coreerr.E("codesign.Sign", output, err)
|
||||
}
|
||||
|
||||
return nil
|
||||
|
|
@ -69,6 +72,7 @@ func (s *MacOSSigner) Sign(ctx context.Context, fs io.Medium, binary string) err
|
|||
|
||||
// Notarize submits binary to Apple for notarization and staples the ticket.
|
||||
// This blocks until Apple responds (typically 1-5 minutes).
|
||||
// Usage example: call value.Notarize(...) from integrating code.
|
||||
func (s *MacOSSigner) Notarize(ctx context.Context, fs io.Medium, binary string) error {
|
||||
if s.config.AppleID == "" || s.config.TeamID == "" || s.config.AppPassword == "" {
|
||||
return coreerr.E("codesign.Notarize", "missing Apple credentials (apple_id, team_id, app_password)", nil)
|
||||
|
|
@ -76,34 +80,32 @@ func (s *MacOSSigner) Notarize(ctx context.Context, fs io.Medium, binary string)
|
|||
|
||||
// Create ZIP for submission
|
||||
zipPath := binary + ".zip"
|
||||
zipCmd := exec.CommandContext(ctx, "zip", "-j", zipPath, binary)
|
||||
if output, err := zipCmd.CombinedOutput(); err != nil {
|
||||
return coreerr.E("codesign.Notarize", "failed to create zip: "+string(output), err)
|
||||
if output, err := ax.CombinedOutput(ctx, "", nil, "zip", "-j", zipPath, binary); err != nil {
|
||||
return coreerr.E("codesign.Notarize", "failed to create zip: "+output, err)
|
||||
}
|
||||
defer func() { _ = fs.Delete(zipPath) }()
|
||||
|
||||
// Submit to Apple and wait
|
||||
submitCmd := exec.CommandContext(ctx, "xcrun", "notarytool", "submit",
|
||||
if output, err := ax.CombinedOutput(ctx, "", nil, "xcrun", "notarytool", "submit",
|
||||
zipPath,
|
||||
"--apple-id", s.config.AppleID,
|
||||
"--team-id", s.config.TeamID,
|
||||
"--password", s.config.AppPassword,
|
||||
"--wait",
|
||||
)
|
||||
if output, err := submitCmd.CombinedOutput(); err != nil {
|
||||
return coreerr.E("codesign.Notarize", "notarization failed: "+string(output), err)
|
||||
); err != nil {
|
||||
return coreerr.E("codesign.Notarize", "notarization failed: "+output, err)
|
||||
}
|
||||
|
||||
// Staple the ticket
|
||||
stapleCmd := exec.CommandContext(ctx, "xcrun", "stapler", "staple", binary)
|
||||
if output, err := stapleCmd.CombinedOutput(); err != nil {
|
||||
return coreerr.E("codesign.Notarize", "failed to staple: "+string(output), err)
|
||||
if output, err := ax.CombinedOutput(ctx, "", nil, "xcrun", "stapler", "staple", binary); err != nil {
|
||||
return coreerr.E("codesign.Notarize", "failed to staple: "+output, err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// ShouldNotarize returns true if notarization is enabled.
|
||||
// Usage example: call value.ShouldNotarize(...) from integrating code.
|
||||
func (s *MacOSSigner) ShouldNotarize() bool {
|
||||
return s.config.Notarize
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,12 +9,12 @@ import (
|
|||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestMacOSSigner_Good_Name(t *testing.T) {
|
||||
func TestCodesign_MacOSSignerName_Good(t *testing.T) {
|
||||
s := NewMacOSSigner(MacOSConfig{Identity: "Developer ID Application: Test"})
|
||||
assert.Equal(t, "codesign", s.Name())
|
||||
}
|
||||
|
||||
func TestMacOSSigner_Good_Available(t *testing.T) {
|
||||
func TestCodesign_MacOSSignerAvailable_Good(t *testing.T) {
|
||||
s := NewMacOSSigner(MacOSConfig{Identity: "Developer ID Application: Test"})
|
||||
|
||||
if runtime.GOOS == "darwin" {
|
||||
|
|
@ -25,12 +25,12 @@ func TestMacOSSigner_Good_Available(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestMacOSSigner_Bad_NoIdentity(t *testing.T) {
|
||||
func TestCodesign_MacOSSignerNoIdentity_Bad(t *testing.T) {
|
||||
s := NewMacOSSigner(MacOSConfig{})
|
||||
assert.False(t, s.Available())
|
||||
}
|
||||
|
||||
func TestMacOSSigner_Sign_Bad(t *testing.T) {
|
||||
func TestCodesign_MacOSSignerSign_Bad(t *testing.T) {
|
||||
t.Run("fails when not available", func(t *testing.T) {
|
||||
if runtime.GOOS == "darwin" {
|
||||
t.Skip("skipping on macOS")
|
||||
|
|
@ -39,11 +39,11 @@ func TestMacOSSigner_Sign_Bad(t *testing.T) {
|
|||
s := NewMacOSSigner(MacOSConfig{Identity: "test"})
|
||||
err := s.Sign(context.Background(), fs, "test")
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "not available")
|
||||
assert.Contains(t, err.Error(), "only available on macOS")
|
||||
})
|
||||
}
|
||||
|
||||
func TestMacOSSigner_Notarize_Bad(t *testing.T) {
|
||||
func TestCodesign_MacOSSignerNotarize_Bad(t *testing.T) {
|
||||
fs := io.Local
|
||||
t.Run("fails with missing credentials", func(t *testing.T) {
|
||||
s := NewMacOSSigner(MacOSConfig{})
|
||||
|
|
@ -53,7 +53,7 @@ func TestMacOSSigner_Notarize_Bad(t *testing.T) {
|
|||
})
|
||||
}
|
||||
|
||||
func TestMacOSSigner_ShouldNotarize(t *testing.T) {
|
||||
func TestCodesign_MacOSSignerShouldNotarize_Good(t *testing.T) {
|
||||
s := NewMacOSSigner(MacOSConfig{Notarize: true})
|
||||
assert.True(t, s.ShouldNotarize())
|
||||
|
||||
|
|
|
|||
|
|
@ -2,13 +2,14 @@ package signing
|
|||
|
||||
import (
|
||||
"context"
|
||||
"os/exec"
|
||||
|
||||
"dappco.re/go/core/build/internal/ax"
|
||||
"dappco.re/go/core/io"
|
||||
coreerr "dappco.re/go/core/log"
|
||||
)
|
||||
|
||||
// GPGSigner signs files using GPG.
|
||||
// Usage example: declare a value of type signing.GPGSigner in integrating code.
|
||||
type GPGSigner struct {
|
||||
KeyID string
|
||||
}
|
||||
|
|
@ -17,42 +18,44 @@ type GPGSigner struct {
|
|||
var _ Signer = (*GPGSigner)(nil)
|
||||
|
||||
// NewGPGSigner creates a new GPG signer.
|
||||
// Usage example: call signing.NewGPGSigner(...) from integrating code.
|
||||
func NewGPGSigner(keyID string) *GPGSigner {
|
||||
return &GPGSigner{KeyID: keyID}
|
||||
}
|
||||
|
||||
// Name returns "gpg".
|
||||
// Usage example: call value.Name(...) from integrating code.
|
||||
func (s *GPGSigner) Name() string {
|
||||
return "gpg"
|
||||
}
|
||||
|
||||
// Available checks if gpg is installed and key is configured.
|
||||
// Usage example: call value.Available(...) from integrating code.
|
||||
func (s *GPGSigner) Available() bool {
|
||||
if s.KeyID == "" {
|
||||
return false
|
||||
}
|
||||
_, err := exec.LookPath("gpg")
|
||||
_, err := ax.LookPath("gpg")
|
||||
return err == nil
|
||||
}
|
||||
|
||||
// Sign creates a detached ASCII-armored signature.
|
||||
// For file.txt, creates file.txt.asc
|
||||
// Usage example: call value.Sign(...) from integrating code.
|
||||
func (s *GPGSigner) Sign(ctx context.Context, fs io.Medium, file string) error {
|
||||
if !s.Available() {
|
||||
return coreerr.E("gpg.Sign", "gpg not available or key not configured", nil)
|
||||
}
|
||||
|
||||
cmd := exec.CommandContext(ctx, "gpg",
|
||||
output, err := ax.CombinedOutput(ctx, "", nil, "gpg",
|
||||
"--detach-sign",
|
||||
"--armor",
|
||||
"--local-user", s.KeyID,
|
||||
"--output", file+".asc",
|
||||
file,
|
||||
)
|
||||
|
||||
output, err := cmd.CombinedOutput()
|
||||
if err != nil {
|
||||
return coreerr.E("gpg.Sign", string(output), err)
|
||||
return coreerr.E("gpg.Sign", output, err)
|
||||
}
|
||||
|
||||
return nil
|
||||
|
|
|
|||
|
|
@ -8,22 +8,22 @@ import (
|
|||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestGPGSigner_Good_Name(t *testing.T) {
|
||||
func TestGPG_GPGSignerName_Good(t *testing.T) {
|
||||
s := NewGPGSigner("ABCD1234")
|
||||
assert.Equal(t, "gpg", s.Name())
|
||||
}
|
||||
|
||||
func TestGPGSigner_Good_Available(t *testing.T) {
|
||||
func TestGPG_GPGSignerAvailable_Good(t *testing.T) {
|
||||
s := NewGPGSigner("ABCD1234")
|
||||
_ = s.Available()
|
||||
}
|
||||
|
||||
func TestGPGSigner_Bad_NoKey(t *testing.T) {
|
||||
func TestGPG_GPGSignerNoKey_Bad(t *testing.T) {
|
||||
s := NewGPGSigner("")
|
||||
assert.False(t, s.Available())
|
||||
}
|
||||
|
||||
func TestGPGSigner_Sign_Bad(t *testing.T) {
|
||||
func TestGPG_GPGSignerSign_Bad(t *testing.T) {
|
||||
fs := io.Local
|
||||
t.Run("fails when no key", func(t *testing.T) {
|
||||
s := NewGPGSigner("")
|
||||
|
|
|
|||
|
|
@ -2,15 +2,16 @@ package signing
|
|||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"runtime"
|
||||
|
||||
"dappco.re/go/core"
|
||||
"dappco.re/go/core/io"
|
||||
coreerr "dappco.re/go/core/log"
|
||||
)
|
||||
|
||||
// Artifact represents a build output that can be signed.
|
||||
// This mirrors build.Artifact to avoid import cycles.
|
||||
// Usage example: declare a value of type signing.Artifact in integrating code.
|
||||
type Artifact struct {
|
||||
Path string
|
||||
OS string
|
||||
|
|
@ -19,6 +20,7 @@ type Artifact struct {
|
|||
|
||||
// SignBinaries signs macOS binaries in the artifacts list.
|
||||
// Only signs darwin binaries when running on macOS with a configured identity.
|
||||
// Usage example: call signing.SignBinaries(...) from integrating code.
|
||||
func SignBinaries(ctx context.Context, fs io.Medium, cfg SignConfig, artifacts []Artifact) error {
|
||||
if !cfg.Enabled {
|
||||
return nil
|
||||
|
|
@ -39,7 +41,7 @@ func SignBinaries(ctx context.Context, fs io.Medium, cfg SignConfig, artifacts [
|
|||
continue
|
||||
}
|
||||
|
||||
fmt.Printf(" Signing %s...\n", artifact.Path)
|
||||
core.Print(nil, " Signing %s...", artifact.Path)
|
||||
if err := signer.Sign(ctx, fs, artifact.Path); err != nil {
|
||||
return coreerr.E("signing.SignBinaries", "failed to sign "+artifact.Path, err)
|
||||
}
|
||||
|
|
@ -49,6 +51,7 @@ func SignBinaries(ctx context.Context, fs io.Medium, cfg SignConfig, artifacts [
|
|||
}
|
||||
|
||||
// NotarizeBinaries notarizes macOS binaries if enabled.
|
||||
// Usage example: call signing.NotarizeBinaries(...) from integrating code.
|
||||
func NotarizeBinaries(ctx context.Context, fs io.Medium, cfg SignConfig, artifacts []Artifact) error {
|
||||
if !cfg.Enabled || !cfg.MacOS.Notarize {
|
||||
return nil
|
||||
|
|
@ -68,7 +71,7 @@ func NotarizeBinaries(ctx context.Context, fs io.Medium, cfg SignConfig, artifac
|
|||
continue
|
||||
}
|
||||
|
||||
fmt.Printf(" Notarizing %s (this may take a few minutes)...\n", artifact.Path)
|
||||
core.Print(nil, " Notarizing %s (this may take a few minutes)...", artifact.Path)
|
||||
if err := signer.Notarize(ctx, fs, artifact.Path); err != nil {
|
||||
return coreerr.E("signing.NotarizeBinaries", "failed to notarize "+artifact.Path, err)
|
||||
}
|
||||
|
|
@ -78,6 +81,7 @@ func NotarizeBinaries(ctx context.Context, fs io.Medium, cfg SignConfig, artifac
|
|||
}
|
||||
|
||||
// SignChecksums signs the checksums file with GPG.
|
||||
// Usage example: call signing.SignChecksums(...) from integrating code.
|
||||
func SignChecksums(ctx context.Context, fs io.Medium, cfg SignConfig, checksumFile string) error {
|
||||
if !cfg.Enabled {
|
||||
return nil
|
||||
|
|
@ -88,7 +92,7 @@ func SignChecksums(ctx context.Context, fs io.Medium, cfg SignConfig, checksumFi
|
|||
return nil // Silently skip if not configured
|
||||
}
|
||||
|
||||
fmt.Printf(" Signing %s with GPG...\n", checksumFile)
|
||||
core.Print(nil, " Signing %s with GPG...", checksumFile)
|
||||
if err := signer.Sign(ctx, fs, checksumFile); err != nil {
|
||||
return coreerr.E("signing.SignChecksums", "failed to sign checksums file "+checksumFile, err)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,13 +3,13 @@ package signing
|
|||
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"dappco.re/go/core"
|
||||
"dappco.re/go/core/io"
|
||||
)
|
||||
|
||||
// Signer defines the interface for code signing implementations.
|
||||
// Usage example: declare a value of type signing.Signer in integrating code.
|
||||
type Signer interface {
|
||||
// Name returns the signer's identifier.
|
||||
Name() string
|
||||
|
|
@ -20,6 +20,7 @@ type Signer interface {
|
|||
}
|
||||
|
||||
// SignConfig holds signing configuration from .core/build.yaml.
|
||||
// Usage example: declare a value of type signing.SignConfig in integrating code.
|
||||
type SignConfig struct {
|
||||
Enabled bool `yaml:"enabled"`
|
||||
GPG GPGConfig `yaml:"gpg,omitempty"`
|
||||
|
|
@ -28,11 +29,13 @@ type SignConfig struct {
|
|||
}
|
||||
|
||||
// GPGConfig holds GPG signing configuration.
|
||||
// Usage example: declare a value of type signing.GPGConfig in integrating code.
|
||||
type GPGConfig struct {
|
||||
Key string `yaml:"key"` // Key ID or fingerprint, supports $ENV
|
||||
}
|
||||
|
||||
// MacOSConfig holds macOS codesign configuration.
|
||||
// Usage example: declare a value of type signing.MacOSConfig in integrating code.
|
||||
type MacOSConfig struct {
|
||||
Identity string `yaml:"identity"` // Developer ID Application: ...
|
||||
Notarize bool `yaml:"notarize"` // Submit to Apple for notarization
|
||||
|
|
@ -42,28 +45,31 @@ type MacOSConfig struct {
|
|||
}
|
||||
|
||||
// WindowsConfig holds Windows signtool configuration (placeholder).
|
||||
// Usage example: declare a value of type signing.WindowsConfig in integrating code.
|
||||
type WindowsConfig struct {
|
||||
Certificate string `yaml:"certificate"` // Path to .pfx
|
||||
Password string `yaml:"password"` // Certificate password
|
||||
}
|
||||
|
||||
// DefaultSignConfig returns sensible defaults.
|
||||
// Usage example: call signing.DefaultSignConfig(...) from integrating code.
|
||||
func DefaultSignConfig() SignConfig {
|
||||
return SignConfig{
|
||||
Enabled: true,
|
||||
GPG: GPGConfig{
|
||||
Key: os.Getenv("GPG_KEY_ID"),
|
||||
Key: core.Env("GPG_KEY_ID"),
|
||||
},
|
||||
MacOS: MacOSConfig{
|
||||
Identity: os.Getenv("CODESIGN_IDENTITY"),
|
||||
AppleID: os.Getenv("APPLE_ID"),
|
||||
TeamID: os.Getenv("APPLE_TEAM_ID"),
|
||||
AppPassword: os.Getenv("APPLE_APP_PASSWORD"),
|
||||
Identity: core.Env("CODESIGN_IDENTITY"),
|
||||
AppleID: core.Env("APPLE_ID"),
|
||||
TeamID: core.Env("APPLE_TEAM_ID"),
|
||||
AppPassword: core.Env("APPLE_APP_PASSWORD"),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// ExpandEnv expands environment variables in config values.
|
||||
// Usage example: call value.ExpandEnv(...) from integrating code.
|
||||
func (c *SignConfig) ExpandEnv() {
|
||||
c.GPG.Key = expandEnv(c.GPG.Key)
|
||||
c.MacOS.Identity = expandEnv(c.MacOS.Identity)
|
||||
|
|
@ -76,8 +82,47 @@ func (c *SignConfig) ExpandEnv() {
|
|||
|
||||
// expandEnv expands $VAR or ${VAR} in a string.
|
||||
func expandEnv(s string) string {
|
||||
if strings.HasPrefix(s, "$") {
|
||||
return os.ExpandEnv(s)
|
||||
if !core.Contains(s, "$") {
|
||||
return s
|
||||
}
|
||||
return s
|
||||
|
||||
buf := core.NewBuilder()
|
||||
for i := 0; i < len(s); {
|
||||
if s[i] != '$' {
|
||||
buf.WriteByte(s[i])
|
||||
i++
|
||||
continue
|
||||
}
|
||||
|
||||
if i+1 < len(s) && s[i+1] == '{' {
|
||||
j := i + 2
|
||||
for j < len(s) && s[j] != '}' {
|
||||
j++
|
||||
}
|
||||
if j < len(s) {
|
||||
buf.WriteString(core.Env(s[i+2 : j]))
|
||||
i = j + 1
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
j := i + 1
|
||||
for j < len(s) {
|
||||
c := s[j]
|
||||
if c != '_' && (c < '0' || c > '9') && (c < 'A' || c > 'Z') && (c < 'a' || c > 'z') {
|
||||
break
|
||||
}
|
||||
j++
|
||||
}
|
||||
if j > i+1 {
|
||||
buf.WriteString(core.Env(s[i+1 : j]))
|
||||
i = j
|
||||
continue
|
||||
}
|
||||
|
||||
buf.WriteByte(s[i])
|
||||
i++
|
||||
}
|
||||
|
||||
return buf.String()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ import (
|
|||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestSignBinaries_Good_SkipsNonDarwin(t *testing.T) {
|
||||
func TestSigning_SignBinariesSkipsNonDarwin_Good(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
fs := io.Local
|
||||
cfg := SignConfig{
|
||||
|
|
@ -31,7 +31,7 @@ func TestSignBinaries_Good_SkipsNonDarwin(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestSignBinaries_Good_DisabledConfig(t *testing.T) {
|
||||
func TestSigning_SignBinariesDisabledConfig_Good(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
fs := io.Local
|
||||
cfg := SignConfig{
|
||||
|
|
@ -48,7 +48,7 @@ func TestSignBinaries_Good_DisabledConfig(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestSignBinaries_Good_SkipsOnNonMacOS(t *testing.T) {
|
||||
func TestSigning_SignBinariesSkipsOnNonMacOS_Good(t *testing.T) {
|
||||
if runtime.GOOS == "darwin" {
|
||||
t.Skip("Skipping on macOS - this tests non-macOS behavior")
|
||||
}
|
||||
|
|
@ -72,7 +72,7 @@ func TestSignBinaries_Good_SkipsOnNonMacOS(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestNotarizeBinaries_Good_DisabledConfig(t *testing.T) {
|
||||
func TestSigning_NotarizeBinariesDisabledConfig_Good(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
fs := io.Local
|
||||
cfg := SignConfig{
|
||||
|
|
@ -89,7 +89,7 @@ func TestNotarizeBinaries_Good_DisabledConfig(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestNotarizeBinaries_Good_NotarizeDisabled(t *testing.T) {
|
||||
func TestSigning_NotarizeBinariesNotarizeDisabled_Good(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
fs := io.Local
|
||||
cfg := SignConfig{
|
||||
|
|
@ -109,7 +109,7 @@ func TestNotarizeBinaries_Good_NotarizeDisabled(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestSignChecksums_Good_SkipsNoKey(t *testing.T) {
|
||||
func TestSigning_SignChecksumsSkipsNoKey_Good(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
fs := io.Local
|
||||
cfg := SignConfig{
|
||||
|
|
@ -126,7 +126,7 @@ func TestSignChecksums_Good_SkipsNoKey(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestSignChecksums_Good_Disabled(t *testing.T) {
|
||||
func TestSigning_SignChecksumsDisabled_Good(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
fs := io.Local
|
||||
cfg := SignConfig{
|
||||
|
|
@ -139,12 +139,12 @@ func TestSignChecksums_Good_Disabled(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestDefaultSignConfig(t *testing.T) {
|
||||
func TestSigning_DefaultSignConfig_Good(t *testing.T) {
|
||||
cfg := DefaultSignConfig()
|
||||
assert.True(t, cfg.Enabled)
|
||||
}
|
||||
|
||||
func TestSignConfig_ExpandEnv(t *testing.T) {
|
||||
func TestSigning_SignConfigExpandEnv_Good(t *testing.T) {
|
||||
t.Setenv("TEST_KEY", "ABC")
|
||||
cfg := SignConfig{
|
||||
GPG: GPGConfig{Key: "$TEST_KEY"},
|
||||
|
|
@ -153,7 +153,7 @@ func TestSignConfig_ExpandEnv(t *testing.T) {
|
|||
assert.Equal(t, "ABC", cfg.GPG.Key)
|
||||
}
|
||||
|
||||
func TestWindowsSigner_Good(t *testing.T) {
|
||||
func TestSigning_WindowsSigner_Good(t *testing.T) {
|
||||
fs := io.Local
|
||||
s := NewWindowsSigner(WindowsConfig{})
|
||||
assert.Equal(t, "signtool", s.Name())
|
||||
|
|
@ -163,10 +163,10 @@ func TestWindowsSigner_Good(t *testing.T) {
|
|||
|
||||
// mockSigner is a test double that records calls to Sign.
|
||||
type mockSigner struct {
|
||||
name string
|
||||
available bool
|
||||
name string
|
||||
available bool
|
||||
signedPaths []string
|
||||
signError error
|
||||
signError error
|
||||
}
|
||||
|
||||
func (m *mockSigner) Name() string {
|
||||
|
|
@ -185,7 +185,7 @@ func (m *mockSigner) Sign(ctx context.Context, fs io.Medium, path string) error
|
|||
// Verify mockSigner implements Signer
|
||||
var _ Signer = (*mockSigner)(nil)
|
||||
|
||||
func TestSignBinaries_Good_MockSigner(t *testing.T) {
|
||||
func TestSigning_SignBinariesMockSigner_Good(t *testing.T) {
|
||||
t.Run("signs only darwin artifacts", func(t *testing.T) {
|
||||
artifacts := []Artifact{
|
||||
{Path: "/dist/linux_amd64/myapp", OS: "linux", Arch: "amd64"},
|
||||
|
|
@ -230,7 +230,7 @@ func TestSignBinaries_Good_MockSigner(t *testing.T) {
|
|||
})
|
||||
}
|
||||
|
||||
func TestSignChecksums_Good_MockSigner(t *testing.T) {
|
||||
func TestSigning_SignChecksumsMockSigner_Good(t *testing.T) {
|
||||
t.Run("skips when GPG key is empty", func(t *testing.T) {
|
||||
cfg := SignConfig{
|
||||
Enabled: true,
|
||||
|
|
@ -252,7 +252,7 @@ func TestSignChecksums_Good_MockSigner(t *testing.T) {
|
|||
})
|
||||
}
|
||||
|
||||
func TestNotarizeBinaries_Good_MockSigner(t *testing.T) {
|
||||
func TestSigning_NotarizeBinariesMockSigner_Good(t *testing.T) {
|
||||
t.Run("skips when notarize is false", func(t *testing.T) {
|
||||
cfg := SignConfig{
|
||||
Enabled: true,
|
||||
|
|
@ -292,7 +292,7 @@ func TestNotarizeBinaries_Good_MockSigner(t *testing.T) {
|
|||
})
|
||||
}
|
||||
|
||||
func TestExpandEnv_Good(t *testing.T) {
|
||||
func TestSigning_ExpandEnv_Good(t *testing.T) {
|
||||
t.Run("expands all config fields", func(t *testing.T) {
|
||||
t.Setenv("TEST_GPG_KEY", "GPG123")
|
||||
t.Setenv("TEST_IDENTITY", "Developer ID Application: Test")
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import (
|
|||
)
|
||||
|
||||
// WindowsSigner signs binaries using Windows signtool (placeholder).
|
||||
// Usage example: declare a value of type signing.WindowsSigner in integrating code.
|
||||
type WindowsSigner struct {
|
||||
config WindowsConfig
|
||||
}
|
||||
|
|
@ -15,21 +16,25 @@ type WindowsSigner struct {
|
|||
var _ Signer = (*WindowsSigner)(nil)
|
||||
|
||||
// NewWindowsSigner creates a new Windows signer.
|
||||
// Usage example: call signing.NewWindowsSigner(...) from integrating code.
|
||||
func NewWindowsSigner(cfg WindowsConfig) *WindowsSigner {
|
||||
return &WindowsSigner{config: cfg}
|
||||
}
|
||||
|
||||
// Name returns "signtool".
|
||||
// Usage example: call value.Name(...) from integrating code.
|
||||
func (s *WindowsSigner) Name() string {
|
||||
return "signtool"
|
||||
}
|
||||
|
||||
// Available returns false (not yet implemented).
|
||||
// Usage example: call value.Available(...) from integrating code.
|
||||
func (s *WindowsSigner) Available() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
// Sign is a placeholder that does nothing.
|
||||
// Usage example: call value.Sign(...) from integrating code.
|
||||
func (s *WindowsSigner) Sign(ctx context.Context, fs io.Medium, binary string) error {
|
||||
// TODO: Implement Windows signing
|
||||
return nil
|
||||
|
|
|
|||
|
|
@ -4,18 +4,19 @@ package release
|
|||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"fmt"
|
||||
"os/exec"
|
||||
"context"
|
||||
"regexp"
|
||||
"slices"
|
||||
"strings"
|
||||
"sort"
|
||||
|
||||
"dappco.re/go/core"
|
||||
"dappco.re/go/core/build/internal/ax"
|
||||
coreerr "dappco.re/go/core/log"
|
||||
"golang.org/x/text/cases"
|
||||
"golang.org/x/text/language"
|
||||
)
|
||||
|
||||
// ConventionalCommit represents a parsed conventional commit.
|
||||
// Usage example: declare a value of type release.ConventionalCommit in integrating code.
|
||||
type ConventionalCommit struct {
|
||||
Type string // feat, fix, etc.
|
||||
Scope string // optional scope in parentheses
|
||||
|
|
@ -61,6 +62,7 @@ var conventionalCommitRegex = regexp.MustCompile(`^(\w+)(?:\(([^)]+)\))?(!)?:\s*
|
|||
// Generate generates a markdown changelog from git commits between two refs.
|
||||
// If fromRef is empty, it uses the previous tag or initial commit.
|
||||
// If toRef is empty, it uses HEAD.
|
||||
// Usage example: call release.Generate(...) from integrating code.
|
||||
func Generate(dir, fromRef, toRef string) (string, error) {
|
||||
if toRef == "" {
|
||||
toRef = "HEAD"
|
||||
|
|
@ -97,6 +99,7 @@ func Generate(dir, fromRef, toRef string) (string, error) {
|
|||
}
|
||||
|
||||
// GenerateWithConfig generates a changelog with filtering based on config.
|
||||
// Usage example: call release.GenerateWithConfig(...) from integrating code.
|
||||
func GenerateWithConfig(dir, fromRef, toRef string, cfg *ChangelogConfig) (string, error) {
|
||||
if toRef == "" {
|
||||
toRef = "HEAD"
|
||||
|
|
@ -152,13 +155,11 @@ func GenerateWithConfig(dir, fromRef, toRef string, cfg *ChangelogConfig) (strin
|
|||
|
||||
// getPreviousTag returns the tag before the given ref.
|
||||
func getPreviousTag(dir, ref string) (string, error) {
|
||||
cmd := exec.Command("git", "describe", "--tags", "--abbrev=0", ref+"^")
|
||||
cmd.Dir = dir
|
||||
output, err := cmd.Output()
|
||||
output, err := ax.RunDir(context.Background(), dir, "git", "describe", "--tags", "--abbrev=0", ref+"^")
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return strings.TrimSpace(string(output)), nil
|
||||
return core.Trim(output), nil
|
||||
}
|
||||
|
||||
// getCommits returns a slice of commit strings between two refs.
|
||||
|
|
@ -173,15 +174,13 @@ func getCommits(dir, fromRef, toRef string) ([]string, error) {
|
|||
args = []string{"log", "--oneline", "--no-merges", fromRef + ".." + toRef}
|
||||
}
|
||||
|
||||
cmd := exec.Command("git", args...)
|
||||
cmd.Dir = dir
|
||||
output, err := cmd.Output()
|
||||
output, err := ax.RunDir(context.Background(), dir, "git", args...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var commits []string
|
||||
scanner := bufio.NewScanner(bytes.NewReader(output))
|
||||
scanner := bufio.NewScanner(bytes.NewReader([]byte(output)))
|
||||
for scanner.Scan() {
|
||||
line := scanner.Text()
|
||||
if line != "" {
|
||||
|
|
@ -196,7 +195,7 @@ func getCommits(dir, fromRef, toRef string) ([]string, error) {
|
|||
// Returns nil if the commit doesn't follow conventional commit format.
|
||||
func parseConventionalCommit(commitLine string) *ConventionalCommit {
|
||||
// Split hash and subject
|
||||
parts := strings.SplitN(commitLine, " ", 2)
|
||||
parts := core.SplitN(commitLine, " ", 2)
|
||||
if len(parts) != 2 {
|
||||
return nil
|
||||
}
|
||||
|
|
@ -211,7 +210,7 @@ func parseConventionalCommit(commitLine string) *ConventionalCommit {
|
|||
}
|
||||
|
||||
return &ConventionalCommit{
|
||||
Type: strings.ToLower(matches[1]),
|
||||
Type: core.Lower(matches[1]),
|
||||
Scope: matches[2],
|
||||
Breaking: matches[3] == "!",
|
||||
Description: matches[4],
|
||||
|
|
@ -222,7 +221,7 @@ func parseConventionalCommit(commitLine string) *ConventionalCommit {
|
|||
// formatChangelog formats parsed commits into markdown.
|
||||
func formatChangelog(commits []ConventionalCommit, version string) string {
|
||||
if len(commits) == 0 {
|
||||
return fmt.Sprintf("## %s\n\nNo notable changes.", version)
|
||||
return core.Sprintf("## %s\n\nNo notable changes.", version)
|
||||
}
|
||||
|
||||
// Group commits by type
|
||||
|
|
@ -236,8 +235,8 @@ func formatChangelog(commits []ConventionalCommit, version string) string {
|
|||
grouped[commit.Type] = append(grouped[commit.Type], commit)
|
||||
}
|
||||
|
||||
var buf strings.Builder
|
||||
buf.WriteString(fmt.Sprintf("## %s\n\n", version))
|
||||
buf := core.NewBuilder()
|
||||
buf.WriteString(core.Sprintf("## %s\n\n", version))
|
||||
|
||||
// Breaking changes first
|
||||
if len(breaking) > 0 {
|
||||
|
|
@ -260,7 +259,7 @@ func formatChangelog(commits []ConventionalCommit, version string) string {
|
|||
label = cases.Title(language.English).String(commitType)
|
||||
}
|
||||
|
||||
buf.WriteString(fmt.Sprintf("### %s\n\n", label))
|
||||
buf.WriteString(core.Sprintf("### %s\n\n", label))
|
||||
for _, commit := range commits {
|
||||
buf.WriteString(formatCommitLine(commit))
|
||||
}
|
||||
|
|
@ -270,46 +269,56 @@ func formatChangelog(commits []ConventionalCommit, version string) string {
|
|||
// Any remaining types not in the order list
|
||||
var remainingTypes []string
|
||||
for commitType := range grouped {
|
||||
if !slices.Contains(commitTypeOrder, commitType) {
|
||||
if !containsCommitType(commitTypeOrder, commitType) {
|
||||
remainingTypes = append(remainingTypes, commitType)
|
||||
}
|
||||
}
|
||||
slices.Sort(remainingTypes)
|
||||
sort.Strings(remainingTypes)
|
||||
|
||||
for _, commitType := range remainingTypes {
|
||||
commits := grouped[commitType]
|
||||
label := cases.Title(language.English).String(commitType)
|
||||
buf.WriteString(fmt.Sprintf("### %s\n\n", label))
|
||||
buf.WriteString(core.Sprintf("### %s\n\n", label))
|
||||
for _, commit := range commits {
|
||||
buf.WriteString(formatCommitLine(commit))
|
||||
}
|
||||
buf.WriteString("\n")
|
||||
}
|
||||
|
||||
return strings.TrimSuffix(buf.String(), "\n")
|
||||
return core.TrimSuffix(buf.String(), "\n")
|
||||
}
|
||||
|
||||
// formatCommitLine formats a single commit as a changelog line.
|
||||
func formatCommitLine(commit ConventionalCommit) string {
|
||||
var buf strings.Builder
|
||||
buf := core.NewBuilder()
|
||||
buf.WriteString("- ")
|
||||
|
||||
if commit.Scope != "" {
|
||||
buf.WriteString(fmt.Sprintf("**%s**: ", commit.Scope))
|
||||
buf.WriteString(core.Sprintf("**%s**: ", commit.Scope))
|
||||
}
|
||||
|
||||
buf.WriteString(commit.Description)
|
||||
buf.WriteString(fmt.Sprintf(" (%s)\n", commit.Hash))
|
||||
buf.WriteString(core.Sprintf(" (%s)\n", commit.Hash))
|
||||
|
||||
return buf.String()
|
||||
}
|
||||
|
||||
// ParseCommitType extracts the type from a conventional commit subject.
|
||||
// Returns empty string if not a conventional commit.
|
||||
// Usage example: call release.ParseCommitType(...) from integrating code.
|
||||
func ParseCommitType(subject string) string {
|
||||
matches := conventionalCommitRegex.FindStringSubmatch(subject)
|
||||
if matches == nil {
|
||||
return ""
|
||||
}
|
||||
return strings.ToLower(matches[1])
|
||||
return core.Lower(matches[1])
|
||||
}
|
||||
|
||||
func containsCommitType(types []string, target string) bool {
|
||||
for _, item := range types {
|
||||
if item == target {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,16 +1,15 @@
|
|||
package release
|
||||
|
||||
import (
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"dappco.re/go/core/build/internal/ax"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestParseConventionalCommit_Good(t *testing.T) {
|
||||
func TestChangelog_ParseConventionalCommit_Good(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
input string
|
||||
|
|
@ -108,7 +107,7 @@ func TestParseConventionalCommit_Good(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestParseConventionalCommit_Bad(t *testing.T) {
|
||||
func TestChangelog_ParseConventionalCommit_Bad(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
input string
|
||||
|
|
@ -143,7 +142,7 @@ func TestParseConventionalCommit_Bad(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestFormatChangelog_Good(t *testing.T) {
|
||||
func TestChangelog_FormatChangelog_Good(t *testing.T) {
|
||||
t.Run("formats commits by type", func(t *testing.T) {
|
||||
commits := []ConventionalCommit{
|
||||
{Type: "feat", Description: "add feature A", Hash: "abc1234"},
|
||||
|
|
@ -194,7 +193,7 @@ func TestFormatChangelog_Good(t *testing.T) {
|
|||
})
|
||||
}
|
||||
|
||||
func TestParseCommitType_Good(t *testing.T) {
|
||||
func TestChangelog_ParseCommitType_Good(t *testing.T) {
|
||||
tests := []struct {
|
||||
input string
|
||||
expected string
|
||||
|
|
@ -213,7 +212,7 @@ func TestParseCommitType_Good(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestParseCommitType_Bad(t *testing.T) {
|
||||
func TestChangelog_ParseCommitType_Bad(t *testing.T) {
|
||||
tests := []struct {
|
||||
input string
|
||||
}{
|
||||
|
|
@ -230,7 +229,7 @@ func TestParseCommitType_Bad(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestGenerateWithConfig_ConfigValues(t *testing.T) {
|
||||
func TestChangelog_GenerateWithConfigConfigValues_Good(t *testing.T) {
|
||||
t.Run("config filters are parsed correctly", func(t *testing.T) {
|
||||
cfg := &ChangelogConfig{
|
||||
Include: []string{"feat", "fix"},
|
||||
|
|
@ -261,18 +260,11 @@ func setupChangelogGitRepo(t *testing.T) string {
|
|||
dir := t.TempDir()
|
||||
|
||||
// Initialize git repo
|
||||
cmd := exec.Command("git", "init")
|
||||
cmd.Dir = dir
|
||||
require.NoError(t, cmd.Run())
|
||||
runGit(t, dir, "init")
|
||||
|
||||
// Configure git user for commits
|
||||
cmd = exec.Command("git", "config", "user.email", "test@example.com")
|
||||
cmd.Dir = dir
|
||||
require.NoError(t, cmd.Run())
|
||||
|
||||
cmd = exec.Command("git", "config", "user.name", "Test User")
|
||||
cmd.Dir = dir
|
||||
require.NoError(t, cmd.Run())
|
||||
runGit(t, dir, "config", "user.email", "test@example.com")
|
||||
runGit(t, dir, "config", "user.name", "Test User")
|
||||
|
||||
return dir
|
||||
}
|
||||
|
|
@ -282,30 +274,23 @@ func createChangelogCommit(t *testing.T, dir, message string) {
|
|||
t.Helper()
|
||||
|
||||
// Create or modify a file
|
||||
filePath := filepath.Join(dir, "changelog_test.txt")
|
||||
content, _ := os.ReadFile(filePath)
|
||||
filePath := ax.Join(dir, "changelog_test.txt")
|
||||
content, _ := ax.ReadFile(filePath)
|
||||
content = append(content, []byte(message+"\n")...)
|
||||
require.NoError(t, os.WriteFile(filePath, content, 0644))
|
||||
require.NoError(t, ax.WriteFile(filePath, content, 0644))
|
||||
|
||||
// Stage and commit
|
||||
cmd := exec.Command("git", "add", ".")
|
||||
cmd.Dir = dir
|
||||
require.NoError(t, cmd.Run())
|
||||
|
||||
cmd = exec.Command("git", "commit", "-m", message)
|
||||
cmd.Dir = dir
|
||||
require.NoError(t, cmd.Run())
|
||||
runGit(t, dir, "add", ".")
|
||||
runGit(t, dir, "commit", "-m", message)
|
||||
}
|
||||
|
||||
// createChangelogTag creates a tag in the given directory.
|
||||
func createChangelogTag(t *testing.T, dir, tag string) {
|
||||
t.Helper()
|
||||
cmd := exec.Command("git", "tag", tag)
|
||||
cmd.Dir = dir
|
||||
require.NoError(t, cmd.Run())
|
||||
runGit(t, dir, "tag", tag)
|
||||
}
|
||||
|
||||
func TestGenerate_Good(t *testing.T) {
|
||||
func TestChangelog_Generate_Good(t *testing.T) {
|
||||
t.Run("generates changelog from commits", func(t *testing.T) {
|
||||
dir := setupChangelogGitRepo(t)
|
||||
createChangelogCommit(t, dir, "feat: add new feature")
|
||||
|
|
@ -386,7 +371,7 @@ func TestGenerate_Good(t *testing.T) {
|
|||
})
|
||||
}
|
||||
|
||||
func TestGenerate_Bad(t *testing.T) {
|
||||
func TestChangelog_Generate_Bad(t *testing.T) {
|
||||
t.Run("returns error for non-git directory", func(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
|
||||
|
|
@ -395,7 +380,7 @@ func TestGenerate_Bad(t *testing.T) {
|
|||
})
|
||||
}
|
||||
|
||||
func TestGenerateWithConfig_Good(t *testing.T) {
|
||||
func TestChangelog_GenerateWithConfig_Good(t *testing.T) {
|
||||
t.Run("filters commits by include list", func(t *testing.T) {
|
||||
dir := setupChangelogGitRepo(t)
|
||||
createChangelogCommit(t, dir, "feat: new feature")
|
||||
|
|
@ -452,7 +437,7 @@ func TestGenerateWithConfig_Good(t *testing.T) {
|
|||
})
|
||||
}
|
||||
|
||||
func TestGetCommits_Good(t *testing.T) {
|
||||
func TestChangelog_GetCommits_Good(t *testing.T) {
|
||||
t.Run("returns all commits when fromRef is empty", func(t *testing.T) {
|
||||
dir := setupChangelogGitRepo(t)
|
||||
createChangelogCommit(t, dir, "feat: first")
|
||||
|
|
@ -503,7 +488,7 @@ func TestGetCommits_Good(t *testing.T) {
|
|||
})
|
||||
}
|
||||
|
||||
func TestGetCommits_Bad(t *testing.T) {
|
||||
func TestChangelog_GetCommits_Bad(t *testing.T) {
|
||||
t.Run("returns error for invalid ref", func(t *testing.T) {
|
||||
dir := setupChangelogGitRepo(t)
|
||||
createChangelogCommit(t, dir, "feat: commit")
|
||||
|
|
@ -520,7 +505,7 @@ func TestGetCommits_Bad(t *testing.T) {
|
|||
})
|
||||
}
|
||||
|
||||
func TestGetPreviousTag_Good(t *testing.T) {
|
||||
func TestChangelog_GetPreviousTag_Good(t *testing.T) {
|
||||
t.Run("returns previous tag", func(t *testing.T) {
|
||||
dir := setupChangelogGitRepo(t)
|
||||
createChangelogCommit(t, dir, "feat: first")
|
||||
|
|
@ -545,7 +530,7 @@ func TestGetPreviousTag_Good(t *testing.T) {
|
|||
})
|
||||
}
|
||||
|
||||
func TestGetPreviousTag_Bad(t *testing.T) {
|
||||
func TestChangelog_GetPreviousTag_Bad(t *testing.T) {
|
||||
t.Run("returns error when no previous tag exists", func(t *testing.T) {
|
||||
dir := setupChangelogGitRepo(t)
|
||||
createChangelogCommit(t, dir, "feat: first")
|
||||
|
|
@ -565,7 +550,7 @@ func TestGetPreviousTag_Bad(t *testing.T) {
|
|||
})
|
||||
}
|
||||
|
||||
func TestFormatCommitLine_Good(t *testing.T) {
|
||||
func TestChangelog_FormatCommitLine_Good(t *testing.T) {
|
||||
t.Run("formats commit without scope", func(t *testing.T) {
|
||||
commit := ConventionalCommit{
|
||||
Type: "feat",
|
||||
|
|
@ -590,7 +575,7 @@ func TestFormatCommitLine_Good(t *testing.T) {
|
|||
})
|
||||
}
|
||||
|
||||
func TestFormatChangelog_Ugly(t *testing.T) {
|
||||
func TestChangelog_FormatChangelog_Ugly(t *testing.T) {
|
||||
t.Run("handles custom commit type not in order", func(t *testing.T) {
|
||||
commits := []ConventionalCommit{
|
||||
{Type: "custom", Description: "custom type", Hash: "abc1234"},
|
||||
|
|
@ -616,7 +601,7 @@ func TestFormatChangelog_Ugly(t *testing.T) {
|
|||
})
|
||||
}
|
||||
|
||||
func TestGenerateWithConfig_Bad(t *testing.T) {
|
||||
func TestChangelog_GenerateWithConfig_Bad(t *testing.T) {
|
||||
t.Run("returns error for non-git directory", func(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
cfg := &ChangelogConfig{
|
||||
|
|
@ -628,7 +613,7 @@ func TestGenerateWithConfig_Bad(t *testing.T) {
|
|||
})
|
||||
}
|
||||
|
||||
func TestGenerateWithConfig_EdgeCases(t *testing.T) {
|
||||
func TestChangelog_GenerateWithConfigEdgeCases_Ugly(t *testing.T) {
|
||||
t.Run("uses HEAD when toRef is empty", func(t *testing.T) {
|
||||
dir := setupChangelogGitRepo(t)
|
||||
createChangelogCommit(t, dir, "feat: new feature")
|
||||
|
|
|
|||
|
|
@ -3,21 +3,23 @@ package release
|
|||
|
||||
import (
|
||||
"iter"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"dappco.re/go/core/build/internal/ax"
|
||||
"dappco.re/go/core/io"
|
||||
coreerr "dappco.re/go/core/log"
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
// ConfigFileName is the name of the release configuration file.
|
||||
// Usage example: reference release.ConfigFileName from package consumers.
|
||||
const ConfigFileName = "release.yaml"
|
||||
|
||||
// ConfigDir is the directory where release configuration is stored.
|
||||
// Usage example: reference release.ConfigDir from package consumers.
|
||||
const ConfigDir = ".core"
|
||||
|
||||
// Config holds the complete release configuration loaded from .core/release.yaml.
|
||||
// Usage example: declare a value of type release.Config in integrating code.
|
||||
type Config struct {
|
||||
// Version is the config file format version.
|
||||
Version int `yaml:"version"`
|
||||
|
|
@ -38,6 +40,7 @@ type Config struct {
|
|||
}
|
||||
|
||||
// ProjectConfig holds project metadata for releases.
|
||||
// Usage example: declare a value of type release.ProjectConfig in integrating code.
|
||||
type ProjectConfig struct {
|
||||
// Name is the project name.
|
||||
Name string `yaml:"name"`
|
||||
|
|
@ -46,12 +49,14 @@ type ProjectConfig struct {
|
|||
}
|
||||
|
||||
// BuildConfig holds build settings for releases.
|
||||
// Usage example: declare a value of type release.BuildConfig in integrating code.
|
||||
type BuildConfig struct {
|
||||
// Targets defines the build targets.
|
||||
Targets []TargetConfig `yaml:"targets"`
|
||||
}
|
||||
|
||||
// TargetConfig defines a build target.
|
||||
// Usage example: declare a value of type release.TargetConfig in integrating code.
|
||||
type TargetConfig struct {
|
||||
// OS is the target operating system (e.g., "linux", "darwin", "windows").
|
||||
OS string `yaml:"os"`
|
||||
|
|
@ -60,6 +65,7 @@ type TargetConfig struct {
|
|||
}
|
||||
|
||||
// PublisherConfig holds configuration for a publisher.
|
||||
// Usage example: declare a value of type release.PublisherConfig in integrating code.
|
||||
type PublisherConfig struct {
|
||||
// Type is the publisher type (e.g., "github", "linuxkit", "docker").
|
||||
Type string `yaml:"type"`
|
||||
|
|
@ -118,6 +124,7 @@ type PublisherConfig struct {
|
|||
}
|
||||
|
||||
// OfficialConfig holds configuration for generating files for official repo PRs.
|
||||
// Usage example: declare a value of type release.OfficialConfig in integrating code.
|
||||
type OfficialConfig struct {
|
||||
// Enabled determines whether to generate files for official repos.
|
||||
Enabled bool `yaml:"enabled"`
|
||||
|
|
@ -126,6 +133,7 @@ type OfficialConfig struct {
|
|||
}
|
||||
|
||||
// SDKConfig holds SDK generation configuration.
|
||||
// Usage example: declare a value of type release.SDKConfig in integrating code.
|
||||
type SDKConfig struct {
|
||||
// Spec is the path to the OpenAPI spec file.
|
||||
Spec string `yaml:"spec,omitempty"`
|
||||
|
|
@ -142,24 +150,28 @@ type SDKConfig struct {
|
|||
}
|
||||
|
||||
// SDKPackageConfig holds package naming configuration.
|
||||
// Usage example: declare a value of type release.SDKPackageConfig in integrating code.
|
||||
type SDKPackageConfig struct {
|
||||
Name string `yaml:"name,omitempty"`
|
||||
Version string `yaml:"version,omitempty"`
|
||||
}
|
||||
|
||||
// SDKDiffConfig holds diff configuration.
|
||||
// Usage example: declare a value of type release.SDKDiffConfig in integrating code.
|
||||
type SDKDiffConfig struct {
|
||||
Enabled bool `yaml:"enabled,omitempty"`
|
||||
FailOnBreaking bool `yaml:"fail_on_breaking,omitempty"`
|
||||
}
|
||||
|
||||
// SDKPublishConfig holds monorepo publish configuration.
|
||||
// Usage example: declare a value of type release.SDKPublishConfig in integrating code.
|
||||
type SDKPublishConfig struct {
|
||||
Repo string `yaml:"repo,omitempty"`
|
||||
Path string `yaml:"path,omitempty"`
|
||||
}
|
||||
|
||||
// ChangelogConfig holds changelog generation settings.
|
||||
// Usage example: declare a value of type release.ChangelogConfig in integrating code.
|
||||
type ChangelogConfig struct {
|
||||
// Include specifies commit types to include in the changelog.
|
||||
Include []string `yaml:"include"`
|
||||
|
|
@ -168,6 +180,7 @@ type ChangelogConfig struct {
|
|||
}
|
||||
|
||||
// PublishersIter returns an iterator for the publishers.
|
||||
// Usage example: call value.PublishersIter(...) from integrating code.
|
||||
func (c *Config) PublishersIter() iter.Seq[PublisherConfig] {
|
||||
return func(yield func(PublisherConfig) bool) {
|
||||
for _, p := range c.Publishers {
|
||||
|
|
@ -181,18 +194,19 @@ func (c *Config) PublishersIter() iter.Seq[PublisherConfig] {
|
|||
// LoadConfig loads release configuration from the .core/release.yaml file in the given directory.
|
||||
// If the config file does not exist, it returns DefaultConfig().
|
||||
// Returns an error if the file exists but cannot be parsed.
|
||||
// Usage example: call release.LoadConfig(...) from integrating code.
|
||||
func LoadConfig(dir string) (*Config, error) {
|
||||
configPath := filepath.Join(dir, ConfigDir, ConfigFileName)
|
||||
configPath := ax.Join(dir, ConfigDir, ConfigFileName)
|
||||
|
||||
// Convert to absolute path for io.Local
|
||||
absPath, err := filepath.Abs(configPath)
|
||||
absPath, err := ax.Abs(configPath)
|
||||
if err != nil {
|
||||
return nil, coreerr.E("release.LoadConfig", "failed to resolve path", err)
|
||||
}
|
||||
|
||||
content, err := io.Local.Read(absPath)
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
if !io.Local.IsFile(absPath) {
|
||||
cfg := DefaultConfig()
|
||||
cfg.projectDir = dir
|
||||
return cfg, nil
|
||||
|
|
@ -213,6 +227,7 @@ func LoadConfig(dir string) (*Config, error) {
|
|||
}
|
||||
|
||||
// DefaultConfig returns sensible defaults for release configuration.
|
||||
// Usage example: call release.DefaultConfig(...) from integrating code.
|
||||
func DefaultConfig() *Config {
|
||||
return &Config{
|
||||
Version: 1,
|
||||
|
|
@ -265,24 +280,28 @@ func applyDefaults(cfg *Config) {
|
|||
}
|
||||
|
||||
// SetProjectDir sets the project directory on the config.
|
||||
// Usage example: call value.SetProjectDir(...) from integrating code.
|
||||
func (c *Config) SetProjectDir(dir string) {
|
||||
c.projectDir = dir
|
||||
}
|
||||
|
||||
// SetVersion sets the version override on the config.
|
||||
// Usage example: call value.SetVersion(...) from integrating code.
|
||||
func (c *Config) SetVersion(version string) {
|
||||
c.version = version
|
||||
}
|
||||
|
||||
// ConfigPath returns the path to the release config file for a given directory.
|
||||
// Usage example: call release.ConfigPath(...) from integrating code.
|
||||
func ConfigPath(dir string) string {
|
||||
return filepath.Join(dir, ConfigDir, ConfigFileName)
|
||||
return ax.Join(dir, ConfigDir, ConfigFileName)
|
||||
}
|
||||
|
||||
// ConfigExists checks if a release config file exists in the given directory.
|
||||
// Usage example: call release.ConfigExists(...) from integrating code.
|
||||
func ConfigExists(dir string) bool {
|
||||
configPath := ConfigPath(dir)
|
||||
absPath, err := filepath.Abs(configPath)
|
||||
absPath, err := ax.Abs(configPath)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
|
|
@ -290,27 +309,30 @@ func ConfigExists(dir string) bool {
|
|||
}
|
||||
|
||||
// GetRepository returns the repository from the config.
|
||||
// Usage example: call value.GetRepository(...) from integrating code.
|
||||
func (c *Config) GetRepository() string {
|
||||
return c.Project.Repository
|
||||
}
|
||||
|
||||
// GetProjectName returns the project name from the config.
|
||||
// Usage example: call value.GetProjectName(...) from integrating code.
|
||||
func (c *Config) GetProjectName() string {
|
||||
return c.Project.Name
|
||||
}
|
||||
|
||||
// WriteConfig writes the config to the .core/release.yaml file.
|
||||
// Usage example: call release.WriteConfig(...) from integrating code.
|
||||
func WriteConfig(cfg *Config, dir string) error {
|
||||
configPath := ConfigPath(dir)
|
||||
|
||||
// Convert to absolute path for io.Local
|
||||
absPath, err := filepath.Abs(configPath)
|
||||
absPath, err := ax.Abs(configPath)
|
||||
if err != nil {
|
||||
return coreerr.E("release.WriteConfig", "failed to resolve path", err)
|
||||
}
|
||||
|
||||
// Ensure directory exists
|
||||
configDir := filepath.Dir(absPath)
|
||||
configDir := ax.Dir(absPath)
|
||||
if err := io.Local.EnsureDir(configDir); err != nil {
|
||||
return coreerr.E("release.WriteConfig", "failed to create directory", err)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,10 +1,9 @@
|
|||
package release
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"dappco.re/go/core/build/internal/ax"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
|
@ -15,19 +14,19 @@ func setupConfigTestDir(t *testing.T, configContent string) string {
|
|||
dir := t.TempDir()
|
||||
|
||||
if configContent != "" {
|
||||
coreDir := filepath.Join(dir, ConfigDir)
|
||||
err := os.MkdirAll(coreDir, 0755)
|
||||
coreDir := ax.Join(dir, ConfigDir)
|
||||
err := ax.MkdirAll(coreDir, 0755)
|
||||
require.NoError(t, err)
|
||||
|
||||
configPath := filepath.Join(coreDir, ConfigFileName)
|
||||
err = os.WriteFile(configPath, []byte(configContent), 0644)
|
||||
configPath := ax.Join(coreDir, ConfigFileName)
|
||||
err = ax.WriteFile(configPath, []byte(configContent), 0644)
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
return dir
|
||||
}
|
||||
|
||||
func TestLoadConfig_Good(t *testing.T) {
|
||||
func TestConfig_LoadConfig_Good(t *testing.T) {
|
||||
t.Run("loads valid config", func(t *testing.T) {
|
||||
content := `
|
||||
version: 1
|
||||
|
|
@ -119,7 +118,7 @@ project:
|
|||
})
|
||||
}
|
||||
|
||||
func TestLoadConfig_Bad(t *testing.T) {
|
||||
func TestConfig_LoadConfig_Bad(t *testing.T) {
|
||||
t.Run("returns error for invalid YAML", func(t *testing.T) {
|
||||
content := `
|
||||
version: 1
|
||||
|
|
@ -134,25 +133,26 @@ project:
|
|||
assert.Contains(t, err.Error(), "failed to parse config file")
|
||||
})
|
||||
|
||||
t.Run("returns error for unreadable file", func(t *testing.T) {
|
||||
t.Run("returns default config when config path is a directory", func(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
coreDir := filepath.Join(dir, ConfigDir)
|
||||
err := os.MkdirAll(coreDir, 0755)
|
||||
coreDir := ax.Join(dir, ConfigDir)
|
||||
err := ax.MkdirAll(coreDir, 0755)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Create config as a directory instead of file
|
||||
configPath := filepath.Join(coreDir, ConfigFileName)
|
||||
err = os.Mkdir(configPath, 0755)
|
||||
configPath := ax.Join(coreDir, ConfigFileName)
|
||||
err = ax.Mkdir(configPath, 0755)
|
||||
require.NoError(t, err)
|
||||
|
||||
cfg, err := LoadConfig(dir)
|
||||
assert.Error(t, err)
|
||||
assert.Nil(t, cfg)
|
||||
assert.Contains(t, err.Error(), "failed to read config file")
|
||||
assert.NoError(t, err)
|
||||
require.NotNil(t, cfg)
|
||||
assert.Equal(t, 1, cfg.Version)
|
||||
assert.Equal(t, dir, cfg.projectDir)
|
||||
})
|
||||
}
|
||||
|
||||
func TestDefaultConfig_Good(t *testing.T) {
|
||||
func TestConfig_DefaultConfig_Good(t *testing.T) {
|
||||
t.Run("returns sensible defaults", func(t *testing.T) {
|
||||
cfg := DefaultConfig()
|
||||
|
||||
|
|
@ -194,14 +194,14 @@ func TestDefaultConfig_Good(t *testing.T) {
|
|||
})
|
||||
}
|
||||
|
||||
func TestConfigPath_Good(t *testing.T) {
|
||||
func TestConfig_ConfigPath_Good(t *testing.T) {
|
||||
t.Run("returns correct path", func(t *testing.T) {
|
||||
path := ConfigPath("/project/root")
|
||||
assert.Equal(t, "/project/root/.core/release.yaml", path)
|
||||
})
|
||||
}
|
||||
|
||||
func TestConfigExists_Good(t *testing.T) {
|
||||
func TestConfig_ConfigExists_Good(t *testing.T) {
|
||||
t.Run("returns true when config exists", func(t *testing.T) {
|
||||
dir := setupConfigTestDir(t, "version: 1")
|
||||
assert.True(t, ConfigExists(dir))
|
||||
|
|
@ -218,7 +218,7 @@ func TestConfigExists_Good(t *testing.T) {
|
|||
})
|
||||
}
|
||||
|
||||
func TestWriteConfig_Good(t *testing.T) {
|
||||
func TestConfig_WriteConfig_Good(t *testing.T) {
|
||||
t.Run("writes config to file", func(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
|
||||
|
|
@ -247,8 +247,8 @@ func TestWriteConfig_Good(t *testing.T) {
|
|||
require.NoError(t, err)
|
||||
|
||||
// Check directory was created
|
||||
coreDir := filepath.Join(dir, ConfigDir)
|
||||
info, err := os.Stat(coreDir)
|
||||
coreDir := ax.Join(dir, ConfigDir)
|
||||
info, err := ax.Stat(coreDir)
|
||||
require.NoError(t, err)
|
||||
assert.True(t, info.IsDir())
|
||||
})
|
||||
|
|
@ -302,22 +302,22 @@ func TestConfig_SetProjectDir_Good(t *testing.T) {
|
|||
})
|
||||
}
|
||||
|
||||
func TestWriteConfig_Bad(t *testing.T) {
|
||||
func TestConfig_WriteConfig_Bad(t *testing.T) {
|
||||
t.Run("returns error for unwritable directory", func(t *testing.T) {
|
||||
if os.Geteuid() == 0 {
|
||||
if ax.Geteuid() == 0 {
|
||||
t.Skip("root can write to any directory")
|
||||
}
|
||||
dir := t.TempDir()
|
||||
|
||||
// Create .core directory and make it unwritable
|
||||
coreDir := filepath.Join(dir, ConfigDir)
|
||||
err := os.MkdirAll(coreDir, 0755)
|
||||
coreDir := ax.Join(dir, ConfigDir)
|
||||
err := ax.MkdirAll(coreDir, 0755)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Make directory read-only
|
||||
err = os.Chmod(coreDir, 0555)
|
||||
err = ax.Chmod(coreDir, 0555)
|
||||
require.NoError(t, err)
|
||||
defer func() { _ = os.Chmod(coreDir, 0755) }()
|
||||
defer func() { _ = ax.Chmod(coreDir, 0755) }()
|
||||
|
||||
cfg := DefaultConfig()
|
||||
err = WriteConfig(cfg, dir)
|
||||
|
|
@ -326,7 +326,7 @@ func TestWriteConfig_Bad(t *testing.T) {
|
|||
})
|
||||
|
||||
t.Run("returns error when directory creation fails", func(t *testing.T) {
|
||||
if os.Geteuid() == 0 {
|
||||
if ax.Geteuid() == 0 {
|
||||
t.Skip("root can create directories anywhere")
|
||||
}
|
||||
// Use a path that doesn't exist and can't be created
|
||||
|
|
@ -336,7 +336,7 @@ func TestWriteConfig_Bad(t *testing.T) {
|
|||
})
|
||||
}
|
||||
|
||||
func TestApplyDefaults_Good(t *testing.T) {
|
||||
func TestConfig_ApplyDefaults_Good(t *testing.T) {
|
||||
t.Run("applies version default when zero", func(t *testing.T) {
|
||||
cfg := &Config{Version: 0}
|
||||
applyDefaults(cfg)
|
||||
|
|
|
|||
|
|
@ -5,13 +5,10 @@ import (
|
|||
"bytes"
|
||||
"context"
|
||||
"embed"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"text/template"
|
||||
|
||||
"dappco.re/go/core"
|
||||
"dappco.re/go/core/build/internal/ax"
|
||||
"dappco.re/go/core/build/pkg/build"
|
||||
coreio "dappco.re/go/core/io"
|
||||
coreerr "dappco.re/go/core/log"
|
||||
|
|
@ -21,6 +18,7 @@ import (
|
|||
var aurTemplates embed.FS
|
||||
|
||||
// AURConfig holds AUR-specific configuration.
|
||||
// Usage example: declare a value of type publishers.AURConfig in integrating code.
|
||||
type AURConfig struct {
|
||||
// Package is the AUR package name.
|
||||
Package string
|
||||
|
|
@ -31,19 +29,23 @@ type AURConfig struct {
|
|||
}
|
||||
|
||||
// AURPublisher publishes releases to AUR.
|
||||
// Usage example: declare a value of type publishers.AURPublisher in integrating code.
|
||||
type AURPublisher struct{}
|
||||
|
||||
// NewAURPublisher creates a new AUR publisher.
|
||||
// Usage example: call publishers.NewAURPublisher(...) from integrating code.
|
||||
func NewAURPublisher() *AURPublisher {
|
||||
return &AURPublisher{}
|
||||
}
|
||||
|
||||
// Name returns the publisher's identifier.
|
||||
// Usage example: call value.Name(...) from integrating code.
|
||||
func (p *AURPublisher) Name() string {
|
||||
return "aur"
|
||||
}
|
||||
|
||||
// Publish publishes the release to AUR.
|
||||
// Usage example: call value.Publish(...) from integrating code.
|
||||
func (p *AURPublisher) Publish(ctx context.Context, release *Release, pubCfg PublisherConfig, relCfg ReleaseConfig, dryRun bool) error {
|
||||
cfg := p.parseConfig(pubCfg, relCfg)
|
||||
|
||||
|
|
@ -68,7 +70,7 @@ func (p *AURPublisher) Publish(ctx context.Context, release *Release, pubCfg Pub
|
|||
projectName = relCfg.GetProjectName()
|
||||
}
|
||||
if projectName == "" {
|
||||
parts := strings.Split(repo, "/")
|
||||
parts := core.Split(repo, "/")
|
||||
projectName = parts[len(parts)-1]
|
||||
}
|
||||
|
||||
|
|
@ -77,12 +79,12 @@ func (p *AURPublisher) Publish(ctx context.Context, release *Release, pubCfg Pub
|
|||
packageName = projectName
|
||||
}
|
||||
|
||||
version := strings.TrimPrefix(release.Version, "v")
|
||||
version := core.TrimPrefix(release.Version, "v")
|
||||
checksums := buildChecksumMap(release.Artifacts)
|
||||
|
||||
data := aurTemplateData{
|
||||
PackageName: packageName,
|
||||
Description: fmt.Sprintf("%s CLI", projectName),
|
||||
Description: core.Sprintf("%s CLI", projectName),
|
||||
Repository: repo,
|
||||
Version: version,
|
||||
License: "MIT",
|
||||
|
|
@ -134,38 +136,38 @@ func (p *AURPublisher) parseConfig(pubCfg PublisherConfig, relCfg ReleaseConfig)
|
|||
}
|
||||
|
||||
func (p *AURPublisher) dryRunPublish(m coreio.Medium, data aurTemplateData, cfg AURConfig) error {
|
||||
fmt.Println()
|
||||
fmt.Println("=== DRY RUN: AUR Publish ===")
|
||||
fmt.Println()
|
||||
fmt.Printf("Package: %s-bin\n", data.PackageName)
|
||||
fmt.Printf("Version: %s\n", data.Version)
|
||||
fmt.Printf("Maintainer: %s\n", data.Maintainer)
|
||||
fmt.Printf("Repository: %s\n", data.Repository)
|
||||
fmt.Println()
|
||||
publisherPrintln()
|
||||
publisherPrintln("=== DRY RUN: AUR Publish ===")
|
||||
publisherPrintln()
|
||||
publisherPrint("Package: %s-bin", data.PackageName)
|
||||
publisherPrint("Version: %s", data.Version)
|
||||
publisherPrint("Maintainer: %s", data.Maintainer)
|
||||
publisherPrint("Repository: %s", data.Repository)
|
||||
publisherPrintln()
|
||||
|
||||
pkgbuild, err := p.renderTemplate(m, "templates/aur/PKGBUILD.tmpl", data)
|
||||
if err != nil {
|
||||
return coreerr.E("aur.dryRunPublish", "failed to render PKGBUILD template", err)
|
||||
}
|
||||
fmt.Println("Generated PKGBUILD:")
|
||||
fmt.Println("---")
|
||||
fmt.Println(pkgbuild)
|
||||
fmt.Println("---")
|
||||
fmt.Println()
|
||||
publisherPrintln("Generated PKGBUILD:")
|
||||
publisherPrintln("---")
|
||||
publisherPrintln(pkgbuild)
|
||||
publisherPrintln("---")
|
||||
publisherPrintln()
|
||||
|
||||
srcinfo, err := p.renderTemplate(m, "templates/aur/.SRCINFO.tmpl", data)
|
||||
if err != nil {
|
||||
return coreerr.E("aur.dryRunPublish", "failed to render .SRCINFO template", err)
|
||||
}
|
||||
fmt.Println("Generated .SRCINFO:")
|
||||
fmt.Println("---")
|
||||
fmt.Println(srcinfo)
|
||||
fmt.Println("---")
|
||||
fmt.Println()
|
||||
publisherPrintln("Generated .SRCINFO:")
|
||||
publisherPrintln("---")
|
||||
publisherPrintln(srcinfo)
|
||||
publisherPrintln("---")
|
||||
publisherPrintln()
|
||||
|
||||
fmt.Printf("Would push to AUR: ssh://aur@aur.archlinux.org/%s-bin.git\n", data.PackageName)
|
||||
fmt.Println()
|
||||
fmt.Println("=== END DRY RUN ===")
|
||||
publisherPrint("Would push to AUR: ssh://aur@aur.archlinux.org/%s-bin.git", data.PackageName)
|
||||
publisherPrintln()
|
||||
publisherPrintln("=== END DRY RUN ===")
|
||||
|
||||
return nil
|
||||
}
|
||||
|
|
@ -185,25 +187,25 @@ func (p *AURPublisher) executePublish(ctx context.Context, projectDir string, da
|
|||
if cfg.Official != nil && cfg.Official.Enabled {
|
||||
output := cfg.Official.Output
|
||||
if output == "" {
|
||||
output = filepath.Join(projectDir, "dist", "aur")
|
||||
} else if !filepath.IsAbs(output) {
|
||||
output = filepath.Join(projectDir, output)
|
||||
output = ax.Join(projectDir, "dist", "aur")
|
||||
} else if !ax.IsAbs(output) {
|
||||
output = ax.Join(projectDir, output)
|
||||
}
|
||||
|
||||
if err := release.FS.EnsureDir(output); err != nil {
|
||||
return coreerr.E("aur.Publish", "failed to create output directory", err)
|
||||
}
|
||||
|
||||
pkgbuildPath := filepath.Join(output, "PKGBUILD")
|
||||
pkgbuildPath := ax.Join(output, "PKGBUILD")
|
||||
if err := release.FS.Write(pkgbuildPath, pkgbuild); err != nil {
|
||||
return coreerr.E("aur.Publish", "failed to write PKGBUILD", err)
|
||||
}
|
||||
|
||||
srcinfoPath := filepath.Join(output, ".SRCINFO")
|
||||
srcinfoPath := ax.Join(output, ".SRCINFO")
|
||||
if err := release.FS.Write(srcinfoPath, srcinfo); err != nil {
|
||||
return coreerr.E("aur.Publish", "failed to write .SRCINFO", err)
|
||||
}
|
||||
fmt.Printf("Wrote AUR files: %s\n", output)
|
||||
publisherPrint("Wrote AUR files: %s", output)
|
||||
}
|
||||
|
||||
// Push to AUR if not in official-only mode
|
||||
|
|
@ -217,62 +219,49 @@ func (p *AURPublisher) executePublish(ctx context.Context, projectDir string, da
|
|||
}
|
||||
|
||||
func (p *AURPublisher) pushToAUR(ctx context.Context, data aurTemplateData, pkgbuild, srcinfo string) error {
|
||||
aurURL := fmt.Sprintf("ssh://aur@aur.archlinux.org/%s-bin.git", data.PackageName)
|
||||
aurURL := core.Sprintf("ssh://aur@aur.archlinux.org/%s-bin.git", data.PackageName)
|
||||
|
||||
tmpDir, err := os.MkdirTemp("", "aur-package-*")
|
||||
tmpDir, err := ax.TempDir("aur-package-*")
|
||||
if err != nil {
|
||||
return coreerr.E("aur.pushToAUR", "failed to create temp directory", err)
|
||||
}
|
||||
defer func() { _ = coreio.Local.DeleteAll(tmpDir) }()
|
||||
defer func() { _ = ax.RemoveAll(tmpDir) }()
|
||||
|
||||
// Clone existing AUR repo (or initialise new one)
|
||||
fmt.Printf("Cloning AUR package %s-bin...\n", data.PackageName)
|
||||
cmd := exec.CommandContext(ctx, "git", "clone", aurURL, tmpDir)
|
||||
if err := cmd.Run(); err != nil {
|
||||
publisherPrint("Cloning AUR package %s-bin...", data.PackageName)
|
||||
if err := ax.Exec(ctx, "git", "clone", aurURL, tmpDir); err != nil {
|
||||
// If clone fails, init a new repo
|
||||
cmd = exec.CommandContext(ctx, "git", "init", tmpDir)
|
||||
if err := cmd.Run(); err != nil {
|
||||
if err := ax.Exec(ctx, "git", "init", tmpDir); err != nil {
|
||||
return coreerr.E("aur.pushToAUR", "failed to initialise repo", err)
|
||||
}
|
||||
cmd = exec.CommandContext(ctx, "git", "-C", tmpDir, "remote", "add", "origin", aurURL)
|
||||
if err := cmd.Run(); err != nil {
|
||||
if err := ax.Exec(ctx, "git", "-C", tmpDir, "remote", "add", "origin", aurURL); err != nil {
|
||||
return coreerr.E("aur.pushToAUR", "failed to add remote", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Write files
|
||||
if err := coreio.Local.Write(filepath.Join(tmpDir, "PKGBUILD"), pkgbuild); err != nil {
|
||||
if err := coreio.Local.Write(ax.Join(tmpDir, "PKGBUILD"), pkgbuild); err != nil {
|
||||
return coreerr.E("aur.pushToAUR", "failed to write PKGBUILD", err)
|
||||
}
|
||||
if err := coreio.Local.Write(filepath.Join(tmpDir, ".SRCINFO"), srcinfo); err != nil {
|
||||
if err := coreio.Local.Write(ax.Join(tmpDir, ".SRCINFO"), srcinfo); err != nil {
|
||||
return coreerr.E("aur.pushToAUR", "failed to write .SRCINFO", err)
|
||||
}
|
||||
|
||||
commitMsg := fmt.Sprintf("Update to %s", data.Version)
|
||||
commitMsg := core.Sprintf("Update to %s", data.Version)
|
||||
|
||||
cmd = exec.CommandContext(ctx, "git", "add", ".")
|
||||
cmd.Dir = tmpDir
|
||||
if err := cmd.Run(); err != nil {
|
||||
if err := ax.ExecDir(ctx, tmpDir, "git", "add", "."); err != nil {
|
||||
return coreerr.E("aur.pushToAUR", "git add failed", err)
|
||||
}
|
||||
|
||||
cmd = exec.CommandContext(ctx, "git", "commit", "-m", commitMsg)
|
||||
cmd.Dir = tmpDir
|
||||
cmd.Stdout = os.Stdout
|
||||
cmd.Stderr = os.Stderr
|
||||
if err := cmd.Run(); err != nil {
|
||||
if err := publisherRun(ctx, tmpDir, nil, "git", "commit", "-m", commitMsg); err != nil {
|
||||
return coreerr.E("aur.pushToAUR", "git commit failed", err)
|
||||
}
|
||||
|
||||
cmd = exec.CommandContext(ctx, "git", "push", "origin", "master")
|
||||
cmd.Dir = tmpDir
|
||||
cmd.Stdout = os.Stdout
|
||||
cmd.Stderr = os.Stderr
|
||||
if err := cmd.Run(); err != nil {
|
||||
if err := publisherRun(ctx, tmpDir, nil, "git", "push", "origin", "master"); err != nil {
|
||||
return coreerr.E("aur.pushToAUR", "git push failed", err)
|
||||
}
|
||||
|
||||
fmt.Printf("Published to AUR: https://aur.archlinux.org/packages/%s-bin\n", data.PackageName)
|
||||
publisherPrint("Published to AUR: https://aur.archlinux.org/packages/%s-bin", data.PackageName)
|
||||
return nil
|
||||
}
|
||||
|
||||
|
|
@ -281,7 +270,7 @@ func (p *AURPublisher) renderTemplate(m coreio.Medium, name string, data aurTemp
|
|||
var err error
|
||||
|
||||
// Try custom template from medium
|
||||
customPath := filepath.Join(".core", name)
|
||||
customPath := ax.Join(".core", name)
|
||||
if m != nil && m.IsFile(customPath) {
|
||||
customContent, err := m.Read(customPath)
|
||||
if err == nil {
|
||||
|
|
@ -297,7 +286,7 @@ func (p *AURPublisher) renderTemplate(m coreio.Medium, name string, data aurTemp
|
|||
}
|
||||
}
|
||||
|
||||
tmpl, err := template.New(filepath.Base(name)).Parse(string(content))
|
||||
tmpl, err := template.New(ax.Base(name)).Parse(string(content))
|
||||
if err != nil {
|
||||
return "", coreerr.E("aur.renderTemplate", "failed to parse template "+name, err)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,9 +1,7 @@
|
|||
package publishers
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"dappco.re/go/core/io"
|
||||
|
|
@ -11,14 +9,14 @@ import (
|
|||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestAURPublisher_Name_Good(t *testing.T) {
|
||||
func TestAUR_AURPublisherName_Good(t *testing.T) {
|
||||
t.Run("returns aur", func(t *testing.T) {
|
||||
p := NewAURPublisher()
|
||||
assert.Equal(t, "aur", p.Name())
|
||||
})
|
||||
}
|
||||
|
||||
func TestAURPublisher_ParseConfig_Good(t *testing.T) {
|
||||
func TestAUR_AURPublisherParseConfig_Good(t *testing.T) {
|
||||
p := NewAURPublisher()
|
||||
|
||||
t.Run("uses defaults when no extended config", func(t *testing.T) {
|
||||
|
|
@ -80,7 +78,7 @@ func TestAURPublisher_ParseConfig_Good(t *testing.T) {
|
|||
})
|
||||
}
|
||||
|
||||
func TestAURPublisher_RenderTemplate_Good(t *testing.T) {
|
||||
func TestAUR_AURPublisherRenderTemplate_Good(t *testing.T) {
|
||||
p := NewAURPublisher()
|
||||
|
||||
t.Run("renders PKGBUILD template with data", func(t *testing.T) {
|
||||
|
|
@ -140,7 +138,7 @@ func TestAURPublisher_RenderTemplate_Good(t *testing.T) {
|
|||
})
|
||||
}
|
||||
|
||||
func TestAURPublisher_RenderTemplate_Bad(t *testing.T) {
|
||||
func TestAUR_AURPublisherRenderTemplate_Bad(t *testing.T) {
|
||||
p := NewAURPublisher()
|
||||
|
||||
t.Run("returns error for non-existent template", func(t *testing.T) {
|
||||
|
|
@ -151,14 +149,10 @@ func TestAURPublisher_RenderTemplate_Bad(t *testing.T) {
|
|||
})
|
||||
}
|
||||
|
||||
func TestAURPublisher_DryRunPublish_Good(t *testing.T) {
|
||||
func TestAUR_AURPublisherDryRunPublish_Good(t *testing.T) {
|
||||
p := NewAURPublisher()
|
||||
|
||||
t.Run("outputs expected dry run information", func(t *testing.T) {
|
||||
oldStdout := os.Stdout
|
||||
r, w, _ := os.Pipe()
|
||||
os.Stdout = w
|
||||
|
||||
data := aurTemplateData{
|
||||
PackageName: "myapp",
|
||||
Version: "1.0.0",
|
||||
|
|
@ -171,15 +165,11 @@ func TestAURPublisher_DryRunPublish_Good(t *testing.T) {
|
|||
Maintainer: "John Doe <john@example.com>",
|
||||
}
|
||||
|
||||
err := p.dryRunPublish(io.Local, data, cfg)
|
||||
|
||||
_ = w.Close()
|
||||
var buf bytes.Buffer
|
||||
_, _ = buf.ReadFrom(r)
|
||||
os.Stdout = oldStdout
|
||||
|
||||
var err error
|
||||
output := capturePublisherOutput(t, func() {
|
||||
err = p.dryRunPublish(io.Local, data, cfg)
|
||||
})
|
||||
require.NoError(t, err)
|
||||
output := buf.String()
|
||||
|
||||
assert.Contains(t, output, "DRY RUN: AUR Publish")
|
||||
assert.Contains(t, output, "Package: myapp-bin")
|
||||
|
|
@ -193,7 +183,7 @@ func TestAURPublisher_DryRunPublish_Good(t *testing.T) {
|
|||
})
|
||||
}
|
||||
|
||||
func TestAURPublisher_Publish_Bad(t *testing.T) {
|
||||
func TestAUR_AURPublisherPublish_Bad(t *testing.T) {
|
||||
p := NewAURPublisher()
|
||||
|
||||
t.Run("fails when maintainer not configured", func(t *testing.T) {
|
||||
|
|
@ -211,7 +201,7 @@ func TestAURPublisher_Publish_Bad(t *testing.T) {
|
|||
})
|
||||
}
|
||||
|
||||
func TestAURConfig_Defaults_Good(t *testing.T) {
|
||||
func TestAUR_AURConfigDefaults_Good(t *testing.T) {
|
||||
t.Run("has sensible defaults", func(t *testing.T) {
|
||||
p := NewAURPublisher()
|
||||
pubCfg := PublisherConfig{Type: "aur"}
|
||||
|
|
|
|||
|
|
@ -5,13 +5,10 @@ import (
|
|||
"bytes"
|
||||
"context"
|
||||
"embed"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"text/template"
|
||||
|
||||
"dappco.re/go/core"
|
||||
"dappco.re/go/core/build/internal/ax"
|
||||
"dappco.re/go/core/build/pkg/build"
|
||||
"dappco.re/go/core/i18n"
|
||||
"dappco.re/go/core/io"
|
||||
|
|
@ -22,6 +19,7 @@ import (
|
|||
var chocolateyTemplates embed.FS
|
||||
|
||||
// ChocolateyConfig holds Chocolatey-specific configuration.
|
||||
// Usage example: declare a value of type publishers.ChocolateyConfig in integrating code.
|
||||
type ChocolateyConfig struct {
|
||||
// Package is the Chocolatey package name.
|
||||
Package string
|
||||
|
|
@ -32,19 +30,23 @@ type ChocolateyConfig struct {
|
|||
}
|
||||
|
||||
// ChocolateyPublisher publishes releases to Chocolatey.
|
||||
// Usage example: declare a value of type publishers.ChocolateyPublisher in integrating code.
|
||||
type ChocolateyPublisher struct{}
|
||||
|
||||
// NewChocolateyPublisher creates a new Chocolatey publisher.
|
||||
// Usage example: call publishers.NewChocolateyPublisher(...) from integrating code.
|
||||
func NewChocolateyPublisher() *ChocolateyPublisher {
|
||||
return &ChocolateyPublisher{}
|
||||
}
|
||||
|
||||
// Name returns the publisher's identifier.
|
||||
// Usage example: call value.Name(...) from integrating code.
|
||||
func (p *ChocolateyPublisher) Name() string {
|
||||
return "chocolatey"
|
||||
}
|
||||
|
||||
// Publish publishes the release to Chocolatey.
|
||||
// Usage example: call value.Publish(...) from integrating code.
|
||||
func (p *ChocolateyPublisher) Publish(ctx context.Context, release *Release, pubCfg PublisherConfig, relCfg ReleaseConfig, dryRun bool) error {
|
||||
cfg := p.parseConfig(pubCfg, relCfg)
|
||||
|
||||
|
|
@ -65,7 +67,7 @@ func (p *ChocolateyPublisher) Publish(ctx context.Context, release *Release, pub
|
|||
projectName = relCfg.GetProjectName()
|
||||
}
|
||||
if projectName == "" {
|
||||
parts := strings.Split(repo, "/")
|
||||
parts := core.Split(repo, "/")
|
||||
projectName = parts[len(parts)-1]
|
||||
}
|
||||
|
||||
|
|
@ -74,22 +76,22 @@ func (p *ChocolateyPublisher) Publish(ctx context.Context, release *Release, pub
|
|||
packageName = projectName
|
||||
}
|
||||
|
||||
version := strings.TrimPrefix(release.Version, "v")
|
||||
version := core.TrimPrefix(release.Version, "v")
|
||||
checksums := buildChecksumMap(release.Artifacts)
|
||||
|
||||
// Extract authors from repository
|
||||
authors := strings.Split(repo, "/")[0]
|
||||
authors := core.Split(repo, "/")[0]
|
||||
|
||||
data := chocolateyTemplateData{
|
||||
PackageName: packageName,
|
||||
Title: fmt.Sprintf("%s CLI", i18n.Title(projectName)),
|
||||
Description: fmt.Sprintf("%s CLI", projectName),
|
||||
Title: core.Sprintf("%s CLI", i18n.Title(projectName)),
|
||||
Description: core.Sprintf("%s CLI", projectName),
|
||||
Repository: repo,
|
||||
Version: version,
|
||||
License: "MIT",
|
||||
BinaryName: projectName,
|
||||
Authors: authors,
|
||||
Tags: fmt.Sprintf("cli %s", projectName),
|
||||
Tags: core.Sprintf("cli %s", projectName),
|
||||
Checksums: checksums,
|
||||
}
|
||||
|
||||
|
|
@ -140,42 +142,42 @@ func (p *ChocolateyPublisher) parseConfig(pubCfg PublisherConfig, relCfg Release
|
|||
}
|
||||
|
||||
func (p *ChocolateyPublisher) dryRunPublish(m io.Medium, data chocolateyTemplateData, cfg ChocolateyConfig) error {
|
||||
fmt.Println()
|
||||
fmt.Println("=== DRY RUN: Chocolatey Publish ===")
|
||||
fmt.Println()
|
||||
fmt.Printf("Package: %s\n", data.PackageName)
|
||||
fmt.Printf("Version: %s\n", data.Version)
|
||||
fmt.Printf("Push: %t\n", cfg.Push)
|
||||
fmt.Printf("Repository: %s\n", data.Repository)
|
||||
fmt.Println()
|
||||
publisherPrintln()
|
||||
publisherPrintln("=== DRY RUN: Chocolatey Publish ===")
|
||||
publisherPrintln()
|
||||
publisherPrint("Package: %s", data.PackageName)
|
||||
publisherPrint("Version: %s", data.Version)
|
||||
publisherPrint("Push: %t", cfg.Push)
|
||||
publisherPrint("Repository: %s", data.Repository)
|
||||
publisherPrintln()
|
||||
|
||||
nuspec, err := p.renderTemplate(m, "templates/chocolatey/package.nuspec.tmpl", data)
|
||||
if err != nil {
|
||||
return coreerr.E("chocolatey.dryRunPublish", "failed to render nuspec", err)
|
||||
}
|
||||
fmt.Println("Generated package.nuspec:")
|
||||
fmt.Println("---")
|
||||
fmt.Println(nuspec)
|
||||
fmt.Println("---")
|
||||
fmt.Println()
|
||||
publisherPrintln("Generated package.nuspec:")
|
||||
publisherPrintln("---")
|
||||
publisherPrintln(nuspec)
|
||||
publisherPrintln("---")
|
||||
publisherPrintln()
|
||||
|
||||
install, err := p.renderTemplate(m, "templates/chocolatey/tools/chocolateyinstall.ps1.tmpl", data)
|
||||
if err != nil {
|
||||
return coreerr.E("chocolatey.dryRunPublish", "failed to render install script", err)
|
||||
}
|
||||
fmt.Println("Generated chocolateyinstall.ps1:")
|
||||
fmt.Println("---")
|
||||
fmt.Println(install)
|
||||
fmt.Println("---")
|
||||
fmt.Println()
|
||||
publisherPrintln("Generated chocolateyinstall.ps1:")
|
||||
publisherPrintln("---")
|
||||
publisherPrintln(install)
|
||||
publisherPrintln("---")
|
||||
publisherPrintln()
|
||||
|
||||
if cfg.Push {
|
||||
fmt.Println("Would push to Chocolatey community repo")
|
||||
publisherPrintln("Would push to Chocolatey community repo")
|
||||
} else {
|
||||
fmt.Println("Would generate package files only (push=false)")
|
||||
publisherPrintln("Would generate package files only (push=false)")
|
||||
}
|
||||
fmt.Println()
|
||||
fmt.Println("=== END DRY RUN ===")
|
||||
publisherPrintln()
|
||||
publisherPrintln("=== END DRY RUN ===")
|
||||
|
||||
return nil
|
||||
}
|
||||
|
|
@ -192,31 +194,31 @@ func (p *ChocolateyPublisher) executePublish(ctx context.Context, projectDir str
|
|||
}
|
||||
|
||||
// Create package directory
|
||||
output := filepath.Join(projectDir, "dist", "chocolatey")
|
||||
output := ax.Join(projectDir, "dist", "chocolatey")
|
||||
if cfg.Official != nil && cfg.Official.Enabled && cfg.Official.Output != "" {
|
||||
output = cfg.Official.Output
|
||||
if !filepath.IsAbs(output) {
|
||||
output = filepath.Join(projectDir, output)
|
||||
if !ax.IsAbs(output) {
|
||||
output = ax.Join(projectDir, output)
|
||||
}
|
||||
}
|
||||
|
||||
toolsDir := filepath.Join(output, "tools")
|
||||
toolsDir := ax.Join(output, "tools")
|
||||
if err := release.FS.EnsureDir(toolsDir); err != nil {
|
||||
return coreerr.E("chocolatey.Publish", "failed to create output directory", err)
|
||||
}
|
||||
|
||||
// Write files
|
||||
nuspecPath := filepath.Join(output, fmt.Sprintf("%s.nuspec", data.PackageName))
|
||||
nuspecPath := ax.Join(output, core.Sprintf("%s.nuspec", data.PackageName))
|
||||
if err := release.FS.Write(nuspecPath, nuspec); err != nil {
|
||||
return coreerr.E("chocolatey.Publish", "failed to write nuspec", err)
|
||||
}
|
||||
|
||||
installPath := filepath.Join(toolsDir, "chocolateyinstall.ps1")
|
||||
installPath := ax.Join(toolsDir, "chocolateyinstall.ps1")
|
||||
if err := release.FS.Write(installPath, install); err != nil {
|
||||
return coreerr.E("chocolatey.Publish", "failed to write install script", err)
|
||||
}
|
||||
|
||||
fmt.Printf("Wrote Chocolatey package files: %s\n", output)
|
||||
publisherPrint("Wrote Chocolatey package files: %s", output)
|
||||
|
||||
// Push to Chocolatey if configured
|
||||
if cfg.Push {
|
||||
|
|
@ -230,31 +232,24 @@ func (p *ChocolateyPublisher) executePublish(ctx context.Context, projectDir str
|
|||
|
||||
func (p *ChocolateyPublisher) pushToChocolatey(ctx context.Context, packageDir string, data chocolateyTemplateData) error {
|
||||
// Check for CHOCOLATEY_API_KEY
|
||||
apiKey := os.Getenv("CHOCOLATEY_API_KEY")
|
||||
apiKey := core.Env("CHOCOLATEY_API_KEY")
|
||||
if apiKey == "" {
|
||||
return coreerr.E("chocolatey.Publish", "CHOCOLATEY_API_KEY environment variable is required for push", nil)
|
||||
}
|
||||
|
||||
// Pack the package
|
||||
nupkgPath := filepath.Join(packageDir, fmt.Sprintf("%s.%s.nupkg", data.PackageName, data.Version))
|
||||
nupkgPath := ax.Join(packageDir, core.Sprintf("%s.%s.nupkg", data.PackageName, data.Version))
|
||||
|
||||
cmd := exec.CommandContext(ctx, "choco", "pack", filepath.Join(packageDir, fmt.Sprintf("%s.nuspec", data.PackageName)), "-OutputDirectory", packageDir)
|
||||
cmd.Stdout = os.Stdout
|
||||
cmd.Stderr = os.Stderr
|
||||
if err := cmd.Run(); err != nil {
|
||||
if err := publisherRun(ctx, "", nil, "choco", "pack", ax.Join(packageDir, core.Sprintf("%s.nuspec", data.PackageName)), "-OutputDirectory", packageDir); err != nil {
|
||||
return coreerr.E("chocolatey.Publish", "choco pack failed", err)
|
||||
}
|
||||
|
||||
// Push the package — pass API key via environment variable to avoid exposing it in process listings
|
||||
cmd = exec.CommandContext(ctx, "choco", "push", nupkgPath, "--source", "https://push.chocolatey.org/")
|
||||
cmd.Env = append(os.Environ(), "chocolateyApiKey="+apiKey)
|
||||
cmd.Stdout = os.Stdout
|
||||
cmd.Stderr = os.Stderr
|
||||
if err := cmd.Run(); err != nil {
|
||||
if err := publisherRun(ctx, "", []string{"chocolateyApiKey=" + apiKey}, "choco", "push", nupkgPath, "--source", "https://push.chocolatey.org/"); err != nil {
|
||||
return coreerr.E("chocolatey.Publish", "choco push failed", err)
|
||||
}
|
||||
|
||||
fmt.Printf("Published to Chocolatey: https://community.chocolatey.org/packages/%s\n", data.PackageName)
|
||||
publisherPrint("Published to Chocolatey: https://community.chocolatey.org/packages/%s", data.PackageName)
|
||||
return nil
|
||||
}
|
||||
|
||||
|
|
@ -263,7 +258,7 @@ func (p *ChocolateyPublisher) renderTemplate(m io.Medium, name string, data choc
|
|||
var err error
|
||||
|
||||
// Try custom template from medium
|
||||
customPath := filepath.Join(".core", name)
|
||||
customPath := ax.Join(".core", name)
|
||||
if m != nil && m.IsFile(customPath) {
|
||||
customContent, err := m.Read(customPath)
|
||||
if err == nil {
|
||||
|
|
@ -279,7 +274,7 @@ func (p *ChocolateyPublisher) renderTemplate(m io.Medium, name string, data choc
|
|||
}
|
||||
}
|
||||
|
||||
tmpl, err := template.New(filepath.Base(name)).Parse(string(content))
|
||||
tmpl, err := template.New(ax.Base(name)).Parse(string(content))
|
||||
if err != nil {
|
||||
return "", coreerr.E("chocolatey.renderTemplate", "failed to parse template "+name, err)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,25 +1,24 @@
|
|||
package publishers
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"dappco.re/go/core/build/internal/ax"
|
||||
"dappco.re/go/core/io"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestChocolateyPublisher_Name_Good(t *testing.T) {
|
||||
func TestChocolatey_ChocolateyPublisherName_Good(t *testing.T) {
|
||||
t.Run("returns chocolatey", func(t *testing.T) {
|
||||
p := NewChocolateyPublisher()
|
||||
assert.Equal(t, "chocolatey", p.Name())
|
||||
})
|
||||
}
|
||||
|
||||
func TestChocolateyPublisher_ParseConfig_Good(t *testing.T) {
|
||||
func TestChocolatey_ChocolateyPublisherParseConfig_Good(t *testing.T) {
|
||||
p := NewChocolateyPublisher()
|
||||
|
||||
t.Run("uses defaults when no extended config", func(t *testing.T) {
|
||||
|
|
@ -107,7 +106,7 @@ func TestChocolateyPublisher_ParseConfig_Good(t *testing.T) {
|
|||
})
|
||||
}
|
||||
|
||||
func TestChocolateyPublisher_RenderTemplate_Good(t *testing.T) {
|
||||
func TestChocolatey_ChocolateyPublisherRenderTemplate_Good(t *testing.T) {
|
||||
p := NewChocolateyPublisher()
|
||||
|
||||
t.Run("renders nuspec template with data", func(t *testing.T) {
|
||||
|
|
@ -160,7 +159,7 @@ func TestChocolateyPublisher_RenderTemplate_Good(t *testing.T) {
|
|||
})
|
||||
}
|
||||
|
||||
func TestChocolateyPublisher_RenderTemplate_Bad(t *testing.T) {
|
||||
func TestChocolatey_ChocolateyPublisherRenderTemplate_Bad(t *testing.T) {
|
||||
p := NewChocolateyPublisher()
|
||||
|
||||
t.Run("returns error for non-existent template", func(t *testing.T) {
|
||||
|
|
@ -171,14 +170,10 @@ func TestChocolateyPublisher_RenderTemplate_Bad(t *testing.T) {
|
|||
})
|
||||
}
|
||||
|
||||
func TestChocolateyPublisher_DryRunPublish_Good(t *testing.T) {
|
||||
func TestChocolatey_ChocolateyPublisherDryRunPublish_Good(t *testing.T) {
|
||||
p := NewChocolateyPublisher()
|
||||
|
||||
t.Run("outputs expected dry run information", func(t *testing.T) {
|
||||
oldStdout := os.Stdout
|
||||
r, w, _ := os.Pipe()
|
||||
os.Stdout = w
|
||||
|
||||
data := chocolateyTemplateData{
|
||||
PackageName: "myapp",
|
||||
Version: "1.0.0",
|
||||
|
|
@ -192,15 +187,11 @@ func TestChocolateyPublisher_DryRunPublish_Good(t *testing.T) {
|
|||
Push: false,
|
||||
}
|
||||
|
||||
err := p.dryRunPublish(io.Local, data, cfg)
|
||||
|
||||
_ = w.Close()
|
||||
var buf bytes.Buffer
|
||||
_, _ = buf.ReadFrom(r)
|
||||
os.Stdout = oldStdout
|
||||
|
||||
var err error
|
||||
output := capturePublisherOutput(t, func() {
|
||||
err = p.dryRunPublish(io.Local, data, cfg)
|
||||
})
|
||||
require.NoError(t, err)
|
||||
output := buf.String()
|
||||
|
||||
assert.Contains(t, output, "DRY RUN: Chocolatey Publish")
|
||||
assert.Contains(t, output, "Package: myapp")
|
||||
|
|
@ -214,10 +205,6 @@ func TestChocolateyPublisher_DryRunPublish_Good(t *testing.T) {
|
|||
})
|
||||
|
||||
t.Run("shows push message when push is enabled", func(t *testing.T) {
|
||||
oldStdout := os.Stdout
|
||||
r, w, _ := os.Pipe()
|
||||
os.Stdout = w
|
||||
|
||||
data := chocolateyTemplateData{
|
||||
PackageName: "myapp",
|
||||
Version: "1.0.0",
|
||||
|
|
@ -230,37 +217,25 @@ func TestChocolateyPublisher_DryRunPublish_Good(t *testing.T) {
|
|||
Push: true,
|
||||
}
|
||||
|
||||
err := p.dryRunPublish(io.Local, data, cfg)
|
||||
|
||||
_ = w.Close()
|
||||
var buf bytes.Buffer
|
||||
_, _ = buf.ReadFrom(r)
|
||||
os.Stdout = oldStdout
|
||||
|
||||
var err error
|
||||
output := capturePublisherOutput(t, func() {
|
||||
err = p.dryRunPublish(io.Local, data, cfg)
|
||||
})
|
||||
require.NoError(t, err)
|
||||
output := buf.String()
|
||||
assert.Contains(t, output, "Push: true")
|
||||
assert.Contains(t, output, "Would push to Chocolatey community repo")
|
||||
})
|
||||
}
|
||||
|
||||
func TestChocolateyPublisher_ExecutePublish_Bad(t *testing.T) {
|
||||
func TestChocolatey_ChocolateyPublisherExecutePublish_Bad(t *testing.T) {
|
||||
p := NewChocolateyPublisher()
|
||||
|
||||
t.Run("fails when CHOCOLATEY_API_KEY not set for push", func(t *testing.T) {
|
||||
// Ensure CHOCOLATEY_API_KEY is not set
|
||||
oldKey := os.Getenv("CHOCOLATEY_API_KEY")
|
||||
_ = os.Unsetenv("CHOCOLATEY_API_KEY")
|
||||
defer func() {
|
||||
if oldKey != "" {
|
||||
_ = os.Setenv("CHOCOLATEY_API_KEY", oldKey)
|
||||
}
|
||||
}()
|
||||
t.Setenv("CHOCOLATEY_API_KEY", "")
|
||||
|
||||
// Create a temp directory for the test
|
||||
tmpDir, err := os.MkdirTemp("", "choco-test-*")
|
||||
require.NoError(t, err)
|
||||
defer func() { _ = os.RemoveAll(tmpDir) }()
|
||||
tmpDir := t.TempDir()
|
||||
require.True(t, ax.IsDir(tmpDir))
|
||||
|
||||
data := chocolateyTemplateData{
|
||||
PackageName: "testpkg",
|
||||
|
|
@ -272,13 +247,13 @@ func TestChocolateyPublisher_ExecutePublish_Bad(t *testing.T) {
|
|||
Checksums: ChecksumMap{},
|
||||
}
|
||||
|
||||
err = p.pushToChocolatey(context.TODO(), tmpDir, data)
|
||||
err := p.pushToChocolatey(context.TODO(), tmpDir, data)
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "CHOCOLATEY_API_KEY environment variable is required")
|
||||
})
|
||||
}
|
||||
|
||||
func TestChocolateyConfig_Defaults_Good(t *testing.T) {
|
||||
func TestChocolatey_ChocolateyConfigDefaults_Good(t *testing.T) {
|
||||
t.Run("has sensible defaults", func(t *testing.T) {
|
||||
p := NewChocolateyPublisher()
|
||||
pubCfg := PublisherConfig{Type: "chocolatey"}
|
||||
|
|
@ -292,7 +267,7 @@ func TestChocolateyConfig_Defaults_Good(t *testing.T) {
|
|||
})
|
||||
}
|
||||
|
||||
func TestChocolateyTemplateData_Good(t *testing.T) {
|
||||
func TestChocolatey_ChocolateyTemplateData_Good(t *testing.T) {
|
||||
t.Run("struct has all expected fields", func(t *testing.T) {
|
||||
data := chocolateyTemplateData{
|
||||
PackageName: "myapp",
|
||||
|
|
|
|||
|
|
@ -3,16 +3,14 @@ package publishers
|
|||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"dappco.re/go/core"
|
||||
"dappco.re/go/core/build/internal/ax"
|
||||
coreerr "dappco.re/go/core/log"
|
||||
)
|
||||
|
||||
// DockerConfig holds configuration for the Docker publisher.
|
||||
// Usage example: declare a value of type publishers.DockerConfig in integrating code.
|
||||
type DockerConfig struct {
|
||||
// Registry is the container registry (default: ghcr.io).
|
||||
Registry string `yaml:"registry"`
|
||||
|
|
@ -29,19 +27,23 @@ type DockerConfig struct {
|
|||
}
|
||||
|
||||
// DockerPublisher builds and publishes Docker images.
|
||||
// Usage example: declare a value of type publishers.DockerPublisher in integrating code.
|
||||
type DockerPublisher struct{}
|
||||
|
||||
// NewDockerPublisher creates a new Docker publisher.
|
||||
// Usage example: call publishers.NewDockerPublisher(...) from integrating code.
|
||||
func NewDockerPublisher() *DockerPublisher {
|
||||
return &DockerPublisher{}
|
||||
}
|
||||
|
||||
// Name returns the publisher's identifier.
|
||||
// Usage example: call value.Name(...) from integrating code.
|
||||
func (p *DockerPublisher) Name() string {
|
||||
return "docker"
|
||||
}
|
||||
|
||||
// Publish builds and pushes Docker images.
|
||||
// Usage example: call value.Publish(...) from integrating code.
|
||||
func (p *DockerPublisher) Publish(ctx context.Context, release *Release, pubCfg PublisherConfig, relCfg ReleaseConfig, dryRun bool) error {
|
||||
// Validate docker CLI is available
|
||||
if err := validateDockerCli(); err != nil {
|
||||
|
|
@ -68,7 +70,7 @@ func (p *DockerPublisher) parseConfig(pubCfg PublisherConfig, relCfg ReleaseConf
|
|||
cfg := DockerConfig{
|
||||
Registry: "ghcr.io",
|
||||
Image: "",
|
||||
Dockerfile: filepath.Join(projectDir, "Dockerfile"),
|
||||
Dockerfile: ax.Join(projectDir, "Dockerfile"),
|
||||
Platforms: []string{"linux/amd64", "linux/arm64"},
|
||||
Tags: []string{"latest", "{{.Version}}"},
|
||||
BuildArgs: make(map[string]string),
|
||||
|
|
@ -88,10 +90,10 @@ func (p *DockerPublisher) parseConfig(pubCfg PublisherConfig, relCfg ReleaseConf
|
|||
cfg.Image = image
|
||||
}
|
||||
if dockerfile, ok := ext["dockerfile"].(string); ok && dockerfile != "" {
|
||||
if filepath.IsAbs(dockerfile) {
|
||||
if ax.IsAbs(dockerfile) {
|
||||
cfg.Dockerfile = dockerfile
|
||||
} else {
|
||||
cfg.Dockerfile = filepath.Join(projectDir, dockerfile)
|
||||
cfg.Dockerfile = ax.Join(projectDir, dockerfile)
|
||||
}
|
||||
}
|
||||
if platforms, ok := ext["platforms"].([]any); ok && len(platforms) > 0 {
|
||||
|
|
@ -124,39 +126,39 @@ func (p *DockerPublisher) parseConfig(pubCfg PublisherConfig, relCfg ReleaseConf
|
|||
|
||||
// dryRunPublish shows what would be done without actually building.
|
||||
func (p *DockerPublisher) dryRunPublish(release *Release, cfg DockerConfig) error {
|
||||
fmt.Println()
|
||||
fmt.Println("=== DRY RUN: Docker Build & Push ===")
|
||||
fmt.Println()
|
||||
fmt.Printf("Version: %s\n", release.Version)
|
||||
fmt.Printf("Registry: %s\n", cfg.Registry)
|
||||
fmt.Printf("Image: %s\n", cfg.Image)
|
||||
fmt.Printf("Dockerfile: %s\n", cfg.Dockerfile)
|
||||
fmt.Printf("Platforms: %s\n", strings.Join(cfg.Platforms, ", "))
|
||||
fmt.Println()
|
||||
publisherPrintln()
|
||||
publisherPrintln("=== DRY RUN: Docker Build & Push ===")
|
||||
publisherPrintln()
|
||||
publisherPrint("Version: %s", release.Version)
|
||||
publisherPrint("Registry: %s", cfg.Registry)
|
||||
publisherPrint("Image: %s", cfg.Image)
|
||||
publisherPrint("Dockerfile: %s", cfg.Dockerfile)
|
||||
publisherPrint("Platforms: %s", core.Join(", ", cfg.Platforms...))
|
||||
publisherPrintln()
|
||||
|
||||
// Resolve tags
|
||||
tags := p.resolveTags(cfg.Tags, release.Version)
|
||||
fmt.Println("Tags to be applied:")
|
||||
publisherPrintln("Tags to be applied:")
|
||||
for _, tag := range tags {
|
||||
fullTag := p.buildFullTag(cfg.Registry, cfg.Image, tag)
|
||||
fmt.Printf(" - %s\n", fullTag)
|
||||
publisherPrint(" - %s", fullTag)
|
||||
}
|
||||
fmt.Println()
|
||||
publisherPrintln()
|
||||
|
||||
fmt.Println("Would execute command:")
|
||||
publisherPrintln("Would execute command:")
|
||||
args := p.buildBuildxArgs(cfg, tags, release.Version)
|
||||
fmt.Printf(" docker %s\n", strings.Join(args, " "))
|
||||
publisherPrint(" docker %s", core.Join(" ", args...))
|
||||
|
||||
if len(cfg.BuildArgs) > 0 {
|
||||
fmt.Println()
|
||||
fmt.Println("Build arguments:")
|
||||
publisherPrintln()
|
||||
publisherPrintln("Build arguments:")
|
||||
for k, v := range cfg.BuildArgs {
|
||||
fmt.Printf(" %s=%s\n", k, v)
|
||||
publisherPrint(" %s=%s", k, v)
|
||||
}
|
||||
}
|
||||
|
||||
fmt.Println()
|
||||
fmt.Println("=== END DRY RUN ===")
|
||||
publisherPrintln()
|
||||
publisherPrintln("=== END DRY RUN ===")
|
||||
|
||||
return nil
|
||||
}
|
||||
|
|
@ -174,13 +176,8 @@ func (p *DockerPublisher) executePublish(ctx context.Context, release *Release,
|
|||
// Build the docker buildx command
|
||||
args := p.buildBuildxArgs(cfg, tags, release.Version)
|
||||
|
||||
cmd := exec.CommandContext(ctx, "docker", args...)
|
||||
cmd.Dir = release.ProjectDir
|
||||
cmd.Stdout = os.Stdout
|
||||
cmd.Stderr = os.Stderr
|
||||
|
||||
fmt.Printf("Building and pushing Docker image: %s\n", cfg.Image)
|
||||
if err := cmd.Run(); err != nil {
|
||||
publisherPrint("Building and pushing Docker image: %s", cfg.Image)
|
||||
if err := publisherRun(ctx, release.ProjectDir, nil, "docker", args...); err != nil {
|
||||
return coreerr.E("docker.Publish", "buildx build failed", err)
|
||||
}
|
||||
|
||||
|
|
@ -192,9 +189,9 @@ func (p *DockerPublisher) resolveTags(tags []string, version string) []string {
|
|||
resolved := make([]string, 0, len(tags))
|
||||
for _, tag := range tags {
|
||||
// Replace {{.Version}} with actual version
|
||||
resolvedTag := strings.ReplaceAll(tag, "{{.Version}}", version)
|
||||
resolvedTag := core.Replace(tag, "{{.Version}}", version)
|
||||
// Also support simpler {{Version}} syntax
|
||||
resolvedTag = strings.ReplaceAll(resolvedTag, "{{Version}}", version)
|
||||
resolvedTag = core.Replace(resolvedTag, "{{Version}}", version)
|
||||
resolved = append(resolved, resolvedTag)
|
||||
}
|
||||
return resolved
|
||||
|
|
@ -203,9 +200,9 @@ func (p *DockerPublisher) resolveTags(tags []string, version string) []string {
|
|||
// buildFullTag builds the full image tag including registry.
|
||||
func (p *DockerPublisher) buildFullTag(registry, image, tag string) string {
|
||||
if registry != "" {
|
||||
return fmt.Sprintf("%s/%s:%s", registry, image, tag)
|
||||
return core.Sprintf("%s/%s:%s", registry, image, tag)
|
||||
}
|
||||
return fmt.Sprintf("%s:%s", image, tag)
|
||||
return core.Sprintf("%s:%s", image, tag)
|
||||
}
|
||||
|
||||
// buildBuildxArgs builds the arguments for docker buildx build command.
|
||||
|
|
@ -214,7 +211,7 @@ func (p *DockerPublisher) buildBuildxArgs(cfg DockerConfig, tags []string, versi
|
|||
|
||||
// Multi-platform support
|
||||
if len(cfg.Platforms) > 0 {
|
||||
args = append(args, "--platform", strings.Join(cfg.Platforms, ","))
|
||||
args = append(args, "--platform", core.Join(",", cfg.Platforms...))
|
||||
}
|
||||
|
||||
// Add all tags
|
||||
|
|
@ -230,13 +227,13 @@ func (p *DockerPublisher) buildBuildxArgs(cfg DockerConfig, tags []string, versi
|
|||
// Build arguments
|
||||
for k, v := range cfg.BuildArgs {
|
||||
// Expand version in build args
|
||||
expandedValue := strings.ReplaceAll(v, "{{.Version}}", version)
|
||||
expandedValue = strings.ReplaceAll(expandedValue, "{{Version}}", version)
|
||||
args = append(args, "--build-arg", fmt.Sprintf("%s=%s", k, expandedValue))
|
||||
expandedValue := core.Replace(v, "{{.Version}}", version)
|
||||
expandedValue = core.Replace(expandedValue, "{{Version}}", version)
|
||||
args = append(args, "--build-arg", core.Sprintf("%s=%s", k, expandedValue))
|
||||
}
|
||||
|
||||
// Always add VERSION build arg
|
||||
args = append(args, "--build-arg", fmt.Sprintf("VERSION=%s", version))
|
||||
args = append(args, "--build-arg", core.Sprintf("VERSION=%s", version))
|
||||
|
||||
// Push the image
|
||||
args = append(args, "--push")
|
||||
|
|
@ -250,19 +247,14 @@ func (p *DockerPublisher) buildBuildxArgs(cfg DockerConfig, tags []string, versi
|
|||
// ensureBuildx ensures docker buildx is available and has a builder.
|
||||
func (p *DockerPublisher) ensureBuildx(ctx context.Context) error {
|
||||
// Check if buildx is available
|
||||
cmd := exec.CommandContext(ctx, "docker", "buildx", "version")
|
||||
if err := cmd.Run(); err != nil {
|
||||
if err := ax.Exec(ctx, "docker", "buildx", "version"); err != nil {
|
||||
return coreerr.E("docker.ensureBuildx", "buildx is not available. Install it from https://docs.docker.com/buildx/working-with-buildx/", nil)
|
||||
}
|
||||
|
||||
// Check if we have a builder, create one if not
|
||||
cmd = exec.CommandContext(ctx, "docker", "buildx", "inspect", "--bootstrap")
|
||||
if err := cmd.Run(); err != nil {
|
||||
if err := ax.Exec(ctx, "docker", "buildx", "inspect", "--bootstrap"); err != nil {
|
||||
// Try to create a builder
|
||||
cmd = exec.CommandContext(ctx, "docker", "buildx", "create", "--use", "--bootstrap")
|
||||
cmd.Stdout = os.Stdout
|
||||
cmd.Stderr = os.Stderr
|
||||
if err := cmd.Run(); err != nil {
|
||||
if err := publisherRun(ctx, "", nil, "docker", "buildx", "create", "--use", "--bootstrap"); err != nil {
|
||||
return coreerr.E("docker.ensureBuildx", "failed to create buildx builder", err)
|
||||
}
|
||||
}
|
||||
|
|
@ -272,8 +264,7 @@ func (p *DockerPublisher) ensureBuildx(ctx context.Context) error {
|
|||
|
||||
// validateDockerCli checks if the docker CLI is available.
|
||||
func validateDockerCli() error {
|
||||
cmd := exec.Command("docker", "--version")
|
||||
if err := cmd.Run(); err != nil {
|
||||
if _, err := ax.LookPath("docker"); err != nil {
|
||||
return coreerr.E("docker.validateDockerCli", "docker CLI not found. Install it from https://docs.docker.com/get-docker/", nil)
|
||||
}
|
||||
return nil
|
||||
|
|
|
|||
|
|
@ -1,25 +1,23 @@
|
|||
package publishers
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"dappco.re/go/core/build/internal/ax"
|
||||
"dappco.re/go/core/io"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestDockerPublisher_Name_Good(t *testing.T) {
|
||||
func TestDocker_DockerPublisherName_Good(t *testing.T) {
|
||||
t.Run("returns docker", func(t *testing.T) {
|
||||
p := NewDockerPublisher()
|
||||
assert.Equal(t, "docker", p.Name())
|
||||
})
|
||||
}
|
||||
|
||||
func TestDockerPublisher_ParseConfig_Good(t *testing.T) {
|
||||
func TestDocker_DockerPublisherParseConfig_Good(t *testing.T) {
|
||||
p := NewDockerPublisher()
|
||||
|
||||
t.Run("uses defaults when no extended config", func(t *testing.T) {
|
||||
|
|
@ -73,7 +71,7 @@ func TestDockerPublisher_ParseConfig_Good(t *testing.T) {
|
|||
})
|
||||
}
|
||||
|
||||
func TestDockerPublisher_ResolveTags_Good(t *testing.T) {
|
||||
func TestDocker_DockerPublisherResolveTags_Good(t *testing.T) {
|
||||
p := NewDockerPublisher()
|
||||
|
||||
t.Run("resolves version template", func(t *testing.T) {
|
||||
|
|
@ -95,7 +93,7 @@ func TestDockerPublisher_ResolveTags_Good(t *testing.T) {
|
|||
})
|
||||
}
|
||||
|
||||
func TestDockerPublisher_BuildFullTag_Good(t *testing.T) {
|
||||
func TestDocker_DockerPublisherBuildFullTag_Good(t *testing.T) {
|
||||
p := NewDockerPublisher()
|
||||
|
||||
tests := []struct {
|
||||
|
|
@ -136,7 +134,7 @@ func TestDockerPublisher_BuildFullTag_Good(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestDockerPublisher_BuildBuildxArgs_Good(t *testing.T) {
|
||||
func TestDocker_DockerPublisherBuildBuildxArgs_Good(t *testing.T) {
|
||||
p := NewDockerPublisher()
|
||||
|
||||
t.Run("builds basic args", func(t *testing.T) {
|
||||
|
|
@ -228,7 +226,7 @@ func TestDockerPublisher_BuildBuildxArgs_Good(t *testing.T) {
|
|||
})
|
||||
}
|
||||
|
||||
func TestDockerPublisher_Publish_Bad(t *testing.T) {
|
||||
func TestDocker_DockerPublisherPublish_Bad(t *testing.T) {
|
||||
if testing.Short() {
|
||||
t.Skip("skipping integration test in short mode")
|
||||
}
|
||||
|
|
@ -255,7 +253,7 @@ func TestDockerPublisher_Publish_Bad(t *testing.T) {
|
|||
})
|
||||
}
|
||||
|
||||
func TestDockerConfig_Defaults_Good(t *testing.T) {
|
||||
func TestDocker_DockerConfigDefaults_Good(t *testing.T) {
|
||||
t.Run("has sensible defaults", func(t *testing.T) {
|
||||
p := NewDockerPublisher()
|
||||
pubCfg := PublisherConfig{Type: "docker"}
|
||||
|
|
@ -273,14 +271,10 @@ func TestDockerConfig_Defaults_Good(t *testing.T) {
|
|||
})
|
||||
}
|
||||
|
||||
func TestDockerPublisher_DryRunPublish_Good(t *testing.T) {
|
||||
func TestDocker_DockerPublisherDryRunPublish_Good(t *testing.T) {
|
||||
p := NewDockerPublisher()
|
||||
|
||||
t.Run("outputs expected dry run information", func(t *testing.T) {
|
||||
oldStdout := os.Stdout
|
||||
r, w, _ := os.Pipe()
|
||||
os.Stdout = w
|
||||
|
||||
release := &Release{
|
||||
Version: "v1.0.0",
|
||||
ProjectDir: "/project",
|
||||
|
|
@ -295,15 +289,11 @@ func TestDockerPublisher_DryRunPublish_Good(t *testing.T) {
|
|||
BuildArgs: make(map[string]string),
|
||||
}
|
||||
|
||||
err := p.dryRunPublish(release, cfg)
|
||||
|
||||
_ = w.Close()
|
||||
var buf bytes.Buffer
|
||||
_, _ = buf.ReadFrom(r)
|
||||
os.Stdout = oldStdout
|
||||
|
||||
var err error
|
||||
output := capturePublisherOutput(t, func() {
|
||||
err = p.dryRunPublish(release, cfg)
|
||||
})
|
||||
require.NoError(t, err)
|
||||
output := buf.String()
|
||||
|
||||
assert.Contains(t, output, "DRY RUN: Docker Build & Push")
|
||||
assert.Contains(t, output, "Version: v1.0.0")
|
||||
|
|
@ -320,10 +310,6 @@ func TestDockerPublisher_DryRunPublish_Good(t *testing.T) {
|
|||
})
|
||||
|
||||
t.Run("shows build args when present", func(t *testing.T) {
|
||||
oldStdout := os.Stdout
|
||||
r, w, _ := os.Pipe()
|
||||
os.Stdout = w
|
||||
|
||||
release := &Release{
|
||||
Version: "v1.0.0",
|
||||
ProjectDir: "/project",
|
||||
|
|
@ -341,15 +327,11 @@ func TestDockerPublisher_DryRunPublish_Good(t *testing.T) {
|
|||
},
|
||||
}
|
||||
|
||||
err := p.dryRunPublish(release, cfg)
|
||||
|
||||
_ = w.Close()
|
||||
var buf bytes.Buffer
|
||||
_, _ = buf.ReadFrom(r)
|
||||
os.Stdout = oldStdout
|
||||
|
||||
var err error
|
||||
output := capturePublisherOutput(t, func() {
|
||||
err = p.dryRunPublish(release, cfg)
|
||||
})
|
||||
require.NoError(t, err)
|
||||
output := buf.String()
|
||||
|
||||
assert.Contains(t, output, "Build arguments:")
|
||||
assert.Contains(t, output, "GO_VERSION=1.21")
|
||||
|
|
@ -357,10 +339,6 @@ func TestDockerPublisher_DryRunPublish_Good(t *testing.T) {
|
|||
})
|
||||
|
||||
t.Run("handles single platform", func(t *testing.T) {
|
||||
oldStdout := os.Stdout
|
||||
r, w, _ := os.Pipe()
|
||||
os.Stdout = w
|
||||
|
||||
release := &Release{
|
||||
Version: "v2.0.0",
|
||||
ProjectDir: "/project",
|
||||
|
|
@ -375,22 +353,18 @@ func TestDockerPublisher_DryRunPublish_Good(t *testing.T) {
|
|||
BuildArgs: make(map[string]string),
|
||||
}
|
||||
|
||||
err := p.dryRunPublish(release, cfg)
|
||||
|
||||
_ = w.Close()
|
||||
var buf bytes.Buffer
|
||||
_, _ = buf.ReadFrom(r)
|
||||
os.Stdout = oldStdout
|
||||
|
||||
var err error
|
||||
output := capturePublisherOutput(t, func() {
|
||||
err = p.dryRunPublish(release, cfg)
|
||||
})
|
||||
require.NoError(t, err)
|
||||
output := buf.String()
|
||||
|
||||
assert.Contains(t, output, "Platforms: linux/amd64")
|
||||
assert.Contains(t, output, "ghcr.io/owner/repo:stable")
|
||||
})
|
||||
}
|
||||
|
||||
func TestDockerPublisher_ParseConfig_EdgeCases_Good(t *testing.T) {
|
||||
func TestDocker_DockerPublisherParseConfigEdgeCases_Good(t *testing.T) {
|
||||
p := NewDockerPublisher()
|
||||
|
||||
t.Run("handles nil release config", func(t *testing.T) {
|
||||
|
|
@ -455,7 +429,7 @@ func TestDockerPublisher_ParseConfig_EdgeCases_Good(t *testing.T) {
|
|||
})
|
||||
}
|
||||
|
||||
func TestDockerPublisher_ResolveTags_EdgeCases_Good(t *testing.T) {
|
||||
func TestDocker_DockerPublisherResolveTagsEdgeCases_Good(t *testing.T) {
|
||||
p := NewDockerPublisher()
|
||||
|
||||
t.Run("handles empty tags", func(t *testing.T) {
|
||||
|
|
@ -474,7 +448,7 @@ func TestDockerPublisher_ResolveTags_EdgeCases_Good(t *testing.T) {
|
|||
})
|
||||
}
|
||||
|
||||
func TestDockerPublisher_BuildBuildxArgs_EdgeCases_Good(t *testing.T) {
|
||||
func TestDocker_DockerPublisherBuildBuildxArgsEdgeCases_Good(t *testing.T) {
|
||||
p := NewDockerPublisher()
|
||||
|
||||
t.Run("handles empty platforms", func(t *testing.T) {
|
||||
|
|
@ -563,7 +537,7 @@ func TestDockerPublisher_BuildBuildxArgs_EdgeCases_Good(t *testing.T) {
|
|||
})
|
||||
}
|
||||
|
||||
func TestDockerPublisher_Publish_DryRun_Good(t *testing.T) {
|
||||
func TestDocker_DockerPublisherPublishDryRun_Good(t *testing.T) {
|
||||
// Skip if docker CLI is not available - dry run still validates docker is installed
|
||||
if err := validateDockerCli(); err != nil {
|
||||
t.Skip("skipping test: docker CLI not available")
|
||||
|
|
@ -573,17 +547,8 @@ func TestDockerPublisher_Publish_DryRun_Good(t *testing.T) {
|
|||
|
||||
t.Run("dry run succeeds with valid Dockerfile", func(t *testing.T) {
|
||||
// Create temp directory with Dockerfile
|
||||
tmpDir, err := os.MkdirTemp("", "docker-test")
|
||||
require.NoError(t, err)
|
||||
defer func() { _ = os.RemoveAll(tmpDir) }()
|
||||
|
||||
dockerfilePath := filepath.Join(tmpDir, "Dockerfile")
|
||||
err = os.WriteFile(dockerfilePath, []byte("FROM alpine:latest\n"), 0644)
|
||||
require.NoError(t, err)
|
||||
|
||||
oldStdout := os.Stdout
|
||||
r, w, _ := os.Pipe()
|
||||
os.Stdout = w
|
||||
tmpDir := t.TempDir()
|
||||
require.NoError(t, ax.WriteFile(ax.Join(tmpDir, "Dockerfile"), []byte("FROM alpine:latest\n"), 0o644))
|
||||
|
||||
release := &Release{
|
||||
Version: "v1.0.0",
|
||||
|
|
@ -593,35 +558,20 @@ func TestDockerPublisher_Publish_DryRun_Good(t *testing.T) {
|
|||
pubCfg := PublisherConfig{Type: "docker"}
|
||||
relCfg := &mockReleaseConfig{repository: "owner/repo"}
|
||||
|
||||
err = p.Publish(context.TODO(), release, pubCfg, relCfg, true)
|
||||
|
||||
_ = w.Close()
|
||||
var buf bytes.Buffer
|
||||
_, _ = buf.ReadFrom(r)
|
||||
os.Stdout = oldStdout
|
||||
|
||||
var err error
|
||||
output := capturePublisherOutput(t, func() {
|
||||
err = p.Publish(context.TODO(), release, pubCfg, relCfg, true)
|
||||
})
|
||||
require.NoError(t, err)
|
||||
output := buf.String()
|
||||
assert.Contains(t, output, "DRY RUN: Docker Build & Push")
|
||||
})
|
||||
|
||||
t.Run("dry run uses custom dockerfile path", func(t *testing.T) {
|
||||
// Create temp directory with custom Dockerfile
|
||||
tmpDir, err := os.MkdirTemp("", "docker-test")
|
||||
require.NoError(t, err)
|
||||
defer func() { _ = os.RemoveAll(tmpDir) }()
|
||||
|
||||
customDir := filepath.Join(tmpDir, "docker")
|
||||
err = os.MkdirAll(customDir, 0755)
|
||||
require.NoError(t, err)
|
||||
|
||||
dockerfilePath := filepath.Join(customDir, "Dockerfile.prod")
|
||||
err = os.WriteFile(dockerfilePath, []byte("FROM alpine:latest\n"), 0644)
|
||||
require.NoError(t, err)
|
||||
|
||||
oldStdout := os.Stdout
|
||||
r, w, _ := os.Pipe()
|
||||
os.Stdout = w
|
||||
tmpDir := t.TempDir()
|
||||
customDir := ax.Join(tmpDir, "docker")
|
||||
require.NoError(t, ax.MkdirAll(customDir, 0o755))
|
||||
require.NoError(t, ax.WriteFile(ax.Join(customDir, "Dockerfile.prod"), []byte("FROM alpine:latest\n"), 0o644))
|
||||
|
||||
release := &Release{
|
||||
Version: "v1.0.0",
|
||||
|
|
@ -636,20 +586,16 @@ func TestDockerPublisher_Publish_DryRun_Good(t *testing.T) {
|
|||
}
|
||||
relCfg := &mockReleaseConfig{repository: "owner/repo"}
|
||||
|
||||
err = p.Publish(context.TODO(), release, pubCfg, relCfg, true)
|
||||
|
||||
_ = w.Close()
|
||||
var buf bytes.Buffer
|
||||
_, _ = buf.ReadFrom(r)
|
||||
os.Stdout = oldStdout
|
||||
|
||||
var err error
|
||||
output := capturePublisherOutput(t, func() {
|
||||
err = p.Publish(context.TODO(), release, pubCfg, relCfg, true)
|
||||
})
|
||||
require.NoError(t, err)
|
||||
output := buf.String()
|
||||
assert.Contains(t, output, "Dockerfile.prod")
|
||||
})
|
||||
}
|
||||
|
||||
func TestDockerPublisher_Publish_Validation_Bad(t *testing.T) {
|
||||
func TestDocker_DockerPublisherPublishValidation_Bad(t *testing.T) {
|
||||
p := NewDockerPublisher()
|
||||
|
||||
t.Run("fails when Dockerfile not found with docker installed", func(t *testing.T) {
|
||||
|
|
@ -689,7 +635,7 @@ func TestDockerPublisher_Publish_Validation_Bad(t *testing.T) {
|
|||
})
|
||||
}
|
||||
|
||||
func TestValidateDockerCli_Good(t *testing.T) {
|
||||
func TestDocker_ValidateDockerCli_Good(t *testing.T) {
|
||||
t.Run("returns nil when docker is installed", func(t *testing.T) {
|
||||
err := validateDockerCli()
|
||||
if err != nil {
|
||||
|
|
@ -700,7 +646,7 @@ func TestValidateDockerCli_Good(t *testing.T) {
|
|||
})
|
||||
}
|
||||
|
||||
func TestDockerPublisher_Publish_WithCLI_Good(t *testing.T) {
|
||||
func TestDocker_DockerPublisherPublishWithCLI_Good(t *testing.T) {
|
||||
// These tests run only when docker CLI is available
|
||||
if err := validateDockerCli(); err != nil {
|
||||
t.Skip("skipping test: docker CLI not available")
|
||||
|
|
@ -709,17 +655,8 @@ func TestDockerPublisher_Publish_WithCLI_Good(t *testing.T) {
|
|||
p := NewDockerPublisher()
|
||||
|
||||
t.Run("dry run succeeds with all config options", func(t *testing.T) {
|
||||
tmpDir, err := os.MkdirTemp("", "docker-test")
|
||||
require.NoError(t, err)
|
||||
defer func() { _ = os.RemoveAll(tmpDir) }()
|
||||
|
||||
dockerfilePath := filepath.Join(tmpDir, "Dockerfile")
|
||||
err = os.WriteFile(dockerfilePath, []byte("FROM alpine:latest\n"), 0644)
|
||||
require.NoError(t, err)
|
||||
|
||||
oldStdout := os.Stdout
|
||||
r, w, _ := os.Pipe()
|
||||
os.Stdout = w
|
||||
tmpDir := t.TempDir()
|
||||
require.NoError(t, ax.WriteFile(ax.Join(tmpDir, "Dockerfile"), []byte("FROM alpine:latest\n"), 0o644))
|
||||
|
||||
release := &Release{
|
||||
Version: "v1.0.0",
|
||||
|
|
@ -738,32 +675,19 @@ func TestDockerPublisher_Publish_WithCLI_Good(t *testing.T) {
|
|||
}
|
||||
relCfg := &mockReleaseConfig{repository: "owner/repo"}
|
||||
|
||||
err = p.Publish(context.TODO(), release, pubCfg, relCfg, true)
|
||||
|
||||
_ = w.Close()
|
||||
var buf bytes.Buffer
|
||||
_, _ = buf.ReadFrom(r)
|
||||
os.Stdout = oldStdout
|
||||
|
||||
var err error
|
||||
output := capturePublisherOutput(t, func() {
|
||||
err = p.Publish(context.TODO(), release, pubCfg, relCfg, true)
|
||||
})
|
||||
require.NoError(t, err)
|
||||
output := buf.String()
|
||||
assert.Contains(t, output, "DRY RUN: Docker Build & Push")
|
||||
assert.Contains(t, output, "docker.io")
|
||||
assert.Contains(t, output, "myorg/myapp")
|
||||
})
|
||||
|
||||
t.Run("dry run with nil relCfg uses extended image", func(t *testing.T) {
|
||||
tmpDir, err := os.MkdirTemp("", "docker-test")
|
||||
require.NoError(t, err)
|
||||
defer func() { _ = os.RemoveAll(tmpDir) }()
|
||||
|
||||
dockerfilePath := filepath.Join(tmpDir, "Dockerfile")
|
||||
err = os.WriteFile(dockerfilePath, []byte("FROM alpine:latest\n"), 0644)
|
||||
require.NoError(t, err)
|
||||
|
||||
oldStdout := os.Stdout
|
||||
r, w, _ := os.Pipe()
|
||||
os.Stdout = w
|
||||
tmpDir := t.TempDir()
|
||||
require.NoError(t, ax.WriteFile(ax.Join(tmpDir, "Dockerfile"), []byte("FROM alpine:latest\n"), 0o644))
|
||||
|
||||
release := &Release{
|
||||
Version: "v1.0.0",
|
||||
|
|
@ -777,22 +701,16 @@ func TestDockerPublisher_Publish_WithCLI_Good(t *testing.T) {
|
|||
},
|
||||
}
|
||||
|
||||
err = p.Publish(context.TODO(), release, pubCfg, nil, true) // nil relCfg
|
||||
|
||||
_ = w.Close()
|
||||
var buf bytes.Buffer
|
||||
_, _ = buf.ReadFrom(r)
|
||||
os.Stdout = oldStdout
|
||||
|
||||
var err error
|
||||
output := capturePublisherOutput(t, func() {
|
||||
err = p.Publish(context.TODO(), release, pubCfg, nil, true)
|
||||
})
|
||||
require.NoError(t, err)
|
||||
output := buf.String()
|
||||
assert.Contains(t, output, "standalone/image")
|
||||
})
|
||||
|
||||
t.Run("fails with non-existent Dockerfile in non-dry-run", func(t *testing.T) {
|
||||
tmpDir, err := os.MkdirTemp("", "docker-test")
|
||||
require.NoError(t, err)
|
||||
defer func() { _ = os.RemoveAll(tmpDir) }()
|
||||
tmpDir := t.TempDir()
|
||||
|
||||
// Don't create a Dockerfile
|
||||
release := &Release{
|
||||
|
|
@ -803,7 +721,7 @@ func TestDockerPublisher_Publish_WithCLI_Good(t *testing.T) {
|
|||
pubCfg := PublisherConfig{Type: "docker"}
|
||||
relCfg := &mockReleaseConfig{repository: "owner/repo"}
|
||||
|
||||
err = p.Publish(context.TODO(), release, pubCfg, relCfg, false)
|
||||
err := p.Publish(context.TODO(), release, pubCfg, relCfg, false)
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "Dockerfile not found")
|
||||
})
|
||||
|
|
|
|||
|
|
@ -3,30 +3,31 @@ package publishers
|
|||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"dappco.re/go/core"
|
||||
"dappco.re/go/core/build/internal/ax"
|
||||
coreerr "dappco.re/go/core/log"
|
||||
)
|
||||
|
||||
// GitHubPublisher publishes releases to GitHub using the gh CLI.
|
||||
// Usage example: declare a value of type publishers.GitHubPublisher in integrating code.
|
||||
type GitHubPublisher struct{}
|
||||
|
||||
// NewGitHubPublisher creates a new GitHub publisher.
|
||||
// Usage example: call publishers.NewGitHubPublisher(...) from integrating code.
|
||||
func NewGitHubPublisher() *GitHubPublisher {
|
||||
return &GitHubPublisher{}
|
||||
}
|
||||
|
||||
// Name returns the publisher's identifier.
|
||||
// Usage example: call value.Name(...) from integrating code.
|
||||
func (p *GitHubPublisher) Name() string {
|
||||
return "github"
|
||||
}
|
||||
|
||||
// Publish publishes the release to GitHub.
|
||||
// Uses the gh CLI for creating releases and uploading assets.
|
||||
// Usage example: call value.Publish(...) from integrating code.
|
||||
func (p *GitHubPublisher) Publish(ctx context.Context, release *Release, pubCfg PublisherConfig, relCfg ReleaseConfig, dryRun bool) error {
|
||||
// Determine repository
|
||||
repo := ""
|
||||
|
|
@ -56,34 +57,34 @@ func (p *GitHubPublisher) Publish(ctx context.Context, release *Release, pubCfg
|
|||
|
||||
// dryRunPublish shows what would be done without actually publishing.
|
||||
func (p *GitHubPublisher) dryRunPublish(release *Release, pubCfg PublisherConfig, repo string) error {
|
||||
fmt.Println()
|
||||
fmt.Println("=== DRY RUN: GitHub Release ===")
|
||||
fmt.Println()
|
||||
fmt.Printf("Repository: %s\n", repo)
|
||||
fmt.Printf("Version: %s\n", release.Version)
|
||||
fmt.Printf("Draft: %t\n", pubCfg.Draft)
|
||||
fmt.Printf("Prerelease: %t\n", pubCfg.Prerelease)
|
||||
fmt.Println()
|
||||
publisherPrintln()
|
||||
publisherPrintln("=== DRY RUN: GitHub Release ===")
|
||||
publisherPrintln()
|
||||
publisherPrint("Repository: %s", repo)
|
||||
publisherPrint("Version: %s", release.Version)
|
||||
publisherPrint("Draft: %t", pubCfg.Draft)
|
||||
publisherPrint("Prerelease: %t", pubCfg.Prerelease)
|
||||
publisherPrintln()
|
||||
|
||||
fmt.Println("Would create release with command:")
|
||||
publisherPrintln("Would create release with command:")
|
||||
args := p.buildCreateArgs(release, pubCfg, repo)
|
||||
fmt.Printf(" gh %s\n", strings.Join(args, " "))
|
||||
fmt.Println()
|
||||
publisherPrint(" gh %s", core.Join(" ", args...))
|
||||
publisherPrintln()
|
||||
|
||||
if len(release.Artifacts) > 0 {
|
||||
fmt.Println("Would upload artifacts:")
|
||||
publisherPrintln("Would upload artifacts:")
|
||||
for _, artifact := range release.Artifacts {
|
||||
fmt.Printf(" - %s\n", filepath.Base(artifact.Path))
|
||||
publisherPrint(" - %s", ax.Base(artifact.Path))
|
||||
}
|
||||
}
|
||||
|
||||
fmt.Println()
|
||||
fmt.Println("Changelog:")
|
||||
fmt.Println("---")
|
||||
fmt.Println(release.Changelog)
|
||||
fmt.Println("---")
|
||||
fmt.Println()
|
||||
fmt.Println("=== END DRY RUN ===")
|
||||
publisherPrintln()
|
||||
publisherPrintln("Changelog:")
|
||||
publisherPrintln("---")
|
||||
publisherPrintln(release.Changelog)
|
||||
publisherPrintln("---")
|
||||
publisherPrintln()
|
||||
publisherPrintln("=== END DRY RUN ===")
|
||||
|
||||
return nil
|
||||
}
|
||||
|
|
@ -99,12 +100,7 @@ func (p *GitHubPublisher) executePublish(ctx context.Context, release *Release,
|
|||
}
|
||||
|
||||
// Execute gh release create
|
||||
cmd := exec.CommandContext(ctx, "gh", args...)
|
||||
cmd.Dir = release.ProjectDir
|
||||
cmd.Stdout = os.Stdout
|
||||
cmd.Stderr = os.Stderr
|
||||
|
||||
if err := cmd.Run(); err != nil {
|
||||
if err := publisherRun(ctx, release.ProjectDir, nil, "gh", args...); err != nil {
|
||||
return coreerr.E("github.Publish", "gh release create failed", err)
|
||||
}
|
||||
|
||||
|
|
@ -145,20 +141,16 @@ func (p *GitHubPublisher) buildCreateArgs(release *Release, pubCfg PublisherConf
|
|||
|
||||
// validateGhCli checks if the gh CLI is available and authenticated.
|
||||
func validateGhCli() error {
|
||||
// Check if gh is installed
|
||||
cmd := exec.Command("gh", "--version")
|
||||
if err := cmd.Run(); err != nil {
|
||||
if _, err := ax.LookPath("gh"); err != nil {
|
||||
return coreerr.E("github.validateGhCli", "gh CLI not found. Install it from https://cli.github.com", err)
|
||||
}
|
||||
|
||||
// Check if authenticated
|
||||
cmd = exec.Command("gh", "auth", "status")
|
||||
output, err := cmd.CombinedOutput()
|
||||
output, err := ax.CombinedOutput(context.Background(), "", nil, "gh", "auth", "status")
|
||||
if err != nil {
|
||||
return coreerr.E("github.validateGhCli", "not authenticated with gh CLI. Run 'gh auth login' first", err)
|
||||
}
|
||||
|
||||
if !strings.Contains(string(output), "Logged in") {
|
||||
if !core.Contains(output, "Logged in") {
|
||||
return coreerr.E("github.validateGhCli", "not authenticated with gh CLI. Run 'gh auth login' first", nil)
|
||||
}
|
||||
|
||||
|
|
@ -167,15 +159,12 @@ func validateGhCli() error {
|
|||
|
||||
// detectRepository detects the GitHub repository from git remote.
|
||||
func detectRepository(dir string) (string, error) {
|
||||
cmd := exec.Command("git", "remote", "get-url", "origin")
|
||||
cmd.Dir = dir
|
||||
output, err := cmd.Output()
|
||||
output, err := ax.RunDir(context.Background(), dir, "git", "remote", "get-url", "origin")
|
||||
if err != nil {
|
||||
return "", coreerr.E("github.detectRepository", "failed to get git remote", err)
|
||||
}
|
||||
|
||||
url := strings.TrimSpace(string(output))
|
||||
return parseGitHubRepo(url)
|
||||
return parseGitHubRepo(core.Trim(output))
|
||||
}
|
||||
|
||||
// parseGitHubRepo extracts owner/repo from a GitHub URL.
|
||||
|
|
@ -185,16 +174,16 @@ func detectRepository(dir string) (string, error) {
|
|||
// - https://github.com/owner/repo
|
||||
func parseGitHubRepo(url string) (string, error) {
|
||||
// SSH format
|
||||
if strings.HasPrefix(url, "git@github.com:") {
|
||||
repo := strings.TrimPrefix(url, "git@github.com:")
|
||||
repo = strings.TrimSuffix(repo, ".git")
|
||||
if core.HasPrefix(url, "git@github.com:") {
|
||||
repo := core.TrimPrefix(url, "git@github.com:")
|
||||
repo = core.TrimSuffix(repo, ".git")
|
||||
return repo, nil
|
||||
}
|
||||
|
||||
// HTTPS format
|
||||
if strings.HasPrefix(url, "https://github.com/") {
|
||||
repo := strings.TrimPrefix(url, "https://github.com/")
|
||||
repo = strings.TrimSuffix(repo, ".git")
|
||||
if core.HasPrefix(url, "https://github.com/") {
|
||||
repo := core.TrimPrefix(url, "https://github.com/")
|
||||
repo = core.TrimSuffix(repo, ".git")
|
||||
return repo, nil
|
||||
}
|
||||
|
||||
|
|
@ -203,12 +192,9 @@ func parseGitHubRepo(url string) (string, error) {
|
|||
|
||||
// UploadArtifact uploads a single artifact to an existing release.
|
||||
// This can be used to add artifacts to a release after creation.
|
||||
// Usage example: call publishers.UploadArtifact(...) from integrating code.
|
||||
func UploadArtifact(ctx context.Context, repo, version, artifactPath string) error {
|
||||
cmd := exec.CommandContext(ctx, "gh", "release", "upload", version, artifactPath, "--repo", repo)
|
||||
cmd.Stdout = os.Stdout
|
||||
cmd.Stderr = os.Stderr
|
||||
|
||||
if err := cmd.Run(); err != nil {
|
||||
if err := publisherRun(ctx, "", nil, "gh", "release", "upload", version, artifactPath, "--repo", repo); err != nil {
|
||||
return coreerr.E("github.UploadArtifact", "failed to upload "+artifactPath, err)
|
||||
}
|
||||
|
||||
|
|
@ -216,12 +202,9 @@ func UploadArtifact(ctx context.Context, repo, version, artifactPath string) err
|
|||
}
|
||||
|
||||
// DeleteRelease deletes a release by tag name.
|
||||
// Usage example: call publishers.DeleteRelease(...) from integrating code.
|
||||
func DeleteRelease(ctx context.Context, repo, version string) error {
|
||||
cmd := exec.CommandContext(ctx, "gh", "release", "delete", version, "--repo", repo, "--yes")
|
||||
cmd.Stdout = os.Stdout
|
||||
cmd.Stderr = os.Stderr
|
||||
|
||||
if err := cmd.Run(); err != nil {
|
||||
if err := publisherRun(ctx, "", nil, "gh", "release", "delete", version, "--repo", repo, "--yes"); err != nil {
|
||||
return coreerr.E("github.DeleteRelease", "failed to delete "+version, err)
|
||||
}
|
||||
|
||||
|
|
@ -229,7 +212,7 @@ func DeleteRelease(ctx context.Context, repo, version string) error {
|
|||
}
|
||||
|
||||
// ReleaseExists checks if a release exists for the given version.
|
||||
// Usage example: call publishers.ReleaseExists(...) from integrating code.
|
||||
func ReleaseExists(ctx context.Context, repo, version string) bool {
|
||||
cmd := exec.CommandContext(ctx, "gh", "release", "view", version, "--repo", repo)
|
||||
return cmd.Run() == nil
|
||||
return ax.Exec(ctx, "gh", "release", "view", version, "--repo", repo) == nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,20 +1,17 @@
|
|||
package publishers
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"os"
|
||||
"os/exec"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"dappco.re/go/core"
|
||||
"dappco.re/go/core/build/pkg/build"
|
||||
"dappco.re/go/core/io"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestParseGitHubRepo_Good(t *testing.T) {
|
||||
func TestGitHub_ParseGitHubRepo_Good(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
input string
|
||||
|
|
@ -51,7 +48,7 @@ func TestParseGitHubRepo_Good(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestParseGitHubRepo_Bad(t *testing.T) {
|
||||
func TestGitHub_ParseGitHubRepo_Bad(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
input string
|
||||
|
|
@ -82,14 +79,14 @@ func TestParseGitHubRepo_Bad(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestGitHubPublisher_Name_Good(t *testing.T) {
|
||||
func TestGitHub_GitHubPublisherName_Good(t *testing.T) {
|
||||
t.Run("returns github", func(t *testing.T) {
|
||||
p := NewGitHubPublisher()
|
||||
assert.Equal(t, "github", p.Name())
|
||||
})
|
||||
}
|
||||
|
||||
func TestNewRelease_Good(t *testing.T) {
|
||||
func TestGitHub_NewRelease_Good(t *testing.T) {
|
||||
t.Run("creates release struct", func(t *testing.T) {
|
||||
r := NewRelease("v1.0.0", nil, "changelog", "/project", io.Local)
|
||||
assert.Equal(t, "v1.0.0", r.Version)
|
||||
|
|
@ -99,7 +96,7 @@ func TestNewRelease_Good(t *testing.T) {
|
|||
})
|
||||
}
|
||||
|
||||
func TestNewPublisherConfig_Good(t *testing.T) {
|
||||
func TestGitHub_NewPublisherConfig_Good(t *testing.T) {
|
||||
t.Run("creates config struct", func(t *testing.T) {
|
||||
cfg := NewPublisherConfig("github", true, false, nil)
|
||||
assert.Equal(t, "github", cfg.Type)
|
||||
|
|
@ -116,7 +113,7 @@ func TestNewPublisherConfig_Good(t *testing.T) {
|
|||
})
|
||||
}
|
||||
|
||||
func TestBuildCreateArgs_Good(t *testing.T) {
|
||||
func TestGitHub_BuildCreateArgs_Good(t *testing.T) {
|
||||
p := NewGitHubPublisher()
|
||||
|
||||
t.Run("basic args", func(t *testing.T) {
|
||||
|
|
@ -221,14 +218,10 @@ func TestBuildCreateArgs_Good(t *testing.T) {
|
|||
})
|
||||
}
|
||||
|
||||
func TestGitHubPublisher_DryRunPublish_Good(t *testing.T) {
|
||||
func TestGitHub_GitHubPublisherDryRunPublish_Good(t *testing.T) {
|
||||
p := NewGitHubPublisher()
|
||||
|
||||
t.Run("outputs expected dry run information", func(t *testing.T) {
|
||||
oldStdout := os.Stdout
|
||||
r, w, _ := os.Pipe()
|
||||
os.Stdout = w
|
||||
|
||||
release := &Release{
|
||||
Version: "v1.0.0",
|
||||
Changelog: "## Changes\n\n- Feature A\n- Bug fix B",
|
||||
|
|
@ -241,15 +234,11 @@ func TestGitHubPublisher_DryRunPublish_Good(t *testing.T) {
|
|||
Prerelease: false,
|
||||
}
|
||||
|
||||
err := p.dryRunPublish(release, cfg, "owner/repo")
|
||||
|
||||
_ = w.Close()
|
||||
var buf bytes.Buffer
|
||||
_, _ = buf.ReadFrom(r)
|
||||
os.Stdout = oldStdout
|
||||
|
||||
var err error
|
||||
output := capturePublisherOutput(t, func() {
|
||||
err = p.dryRunPublish(release, cfg, "owner/repo")
|
||||
})
|
||||
require.NoError(t, err)
|
||||
output := buf.String()
|
||||
|
||||
assert.Contains(t, output, "DRY RUN: GitHub Release")
|
||||
assert.Contains(t, output, "Repository: owner/repo")
|
||||
|
|
@ -264,10 +253,6 @@ func TestGitHubPublisher_DryRunPublish_Good(t *testing.T) {
|
|||
})
|
||||
|
||||
t.Run("shows artifacts when present", func(t *testing.T) {
|
||||
oldStdout := os.Stdout
|
||||
r, w, _ := os.Pipe()
|
||||
os.Stdout = w
|
||||
|
||||
release := &Release{
|
||||
Version: "v1.0.0",
|
||||
Changelog: "Changes",
|
||||
|
|
@ -280,15 +265,11 @@ func TestGitHubPublisher_DryRunPublish_Good(t *testing.T) {
|
|||
}
|
||||
cfg := PublisherConfig{Type: "github"}
|
||||
|
||||
err := p.dryRunPublish(release, cfg, "owner/repo")
|
||||
|
||||
_ = w.Close()
|
||||
var buf bytes.Buffer
|
||||
_, _ = buf.ReadFrom(r)
|
||||
os.Stdout = oldStdout
|
||||
|
||||
var err error
|
||||
output := capturePublisherOutput(t, func() {
|
||||
err = p.dryRunPublish(release, cfg, "owner/repo")
|
||||
})
|
||||
require.NoError(t, err)
|
||||
output := buf.String()
|
||||
|
||||
assert.Contains(t, output, "Would upload artifacts:")
|
||||
assert.Contains(t, output, "myapp-darwin-amd64.tar.gz")
|
||||
|
|
@ -296,10 +277,6 @@ func TestGitHubPublisher_DryRunPublish_Good(t *testing.T) {
|
|||
})
|
||||
|
||||
t.Run("shows draft and prerelease flags", func(t *testing.T) {
|
||||
oldStdout := os.Stdout
|
||||
r, w, _ := os.Pipe()
|
||||
os.Stdout = w
|
||||
|
||||
release := &Release{
|
||||
Version: "v1.0.0-beta",
|
||||
Changelog: "Beta release",
|
||||
|
|
@ -312,15 +289,11 @@ func TestGitHubPublisher_DryRunPublish_Good(t *testing.T) {
|
|||
Prerelease: true,
|
||||
}
|
||||
|
||||
err := p.dryRunPublish(release, cfg, "owner/repo")
|
||||
|
||||
_ = w.Close()
|
||||
var buf bytes.Buffer
|
||||
_, _ = buf.ReadFrom(r)
|
||||
os.Stdout = oldStdout
|
||||
|
||||
var err error
|
||||
output := capturePublisherOutput(t, func() {
|
||||
err = p.dryRunPublish(release, cfg, "owner/repo")
|
||||
})
|
||||
require.NoError(t, err)
|
||||
output := buf.String()
|
||||
|
||||
assert.Contains(t, output, "Draft: true")
|
||||
assert.Contains(t, output, "Prerelease: true")
|
||||
|
|
@ -329,14 +302,10 @@ func TestGitHubPublisher_DryRunPublish_Good(t *testing.T) {
|
|||
})
|
||||
}
|
||||
|
||||
func TestGitHubPublisher_Publish_Good(t *testing.T) {
|
||||
func TestGitHub_GitHubPublisherPublish_Good(t *testing.T) {
|
||||
p := NewGitHubPublisher()
|
||||
|
||||
t.Run("dry run uses repository from config", func(t *testing.T) {
|
||||
oldStdout := os.Stdout
|
||||
r, w, _ := os.Pipe()
|
||||
os.Stdout = w
|
||||
|
||||
release := &Release{
|
||||
Version: "v1.0.0",
|
||||
Changelog: "Changes",
|
||||
|
|
@ -347,20 +316,16 @@ func TestGitHubPublisher_Publish_Good(t *testing.T) {
|
|||
relCfg := &mockReleaseConfig{repository: "custom/repo"}
|
||||
|
||||
// Dry run should succeed without needing gh CLI
|
||||
err := p.Publish(context.TODO(), release, pubCfg, relCfg, true)
|
||||
|
||||
_ = w.Close()
|
||||
var buf bytes.Buffer
|
||||
_, _ = buf.ReadFrom(r)
|
||||
os.Stdout = oldStdout
|
||||
|
||||
var err error
|
||||
output := capturePublisherOutput(t, func() {
|
||||
err = p.Publish(context.TODO(), release, pubCfg, relCfg, true)
|
||||
})
|
||||
require.NoError(t, err)
|
||||
output := buf.String()
|
||||
assert.Contains(t, output, "Repository: custom/repo")
|
||||
})
|
||||
}
|
||||
|
||||
func TestGitHubPublisher_Publish_Bad(t *testing.T) {
|
||||
func TestGitHub_GitHubPublisherPublish_Bad(t *testing.T) {
|
||||
if testing.Short() {
|
||||
t.Skip("skipping integration test in short mode")
|
||||
}
|
||||
|
|
@ -387,9 +352,7 @@ func TestGitHubPublisher_Publish_Bad(t *testing.T) {
|
|||
|
||||
t.Run("fails when repository cannot be detected", func(t *testing.T) {
|
||||
// Create a temp directory that is NOT a git repo
|
||||
tmpDir, err := os.MkdirTemp("", "github-test")
|
||||
require.NoError(t, err)
|
||||
defer func() { _ = os.RemoveAll(tmpDir) }()
|
||||
tmpDir := t.TempDir()
|
||||
|
||||
release := &Release{
|
||||
Version: "v1.0.0",
|
||||
|
|
@ -400,7 +363,7 @@ func TestGitHubPublisher_Publish_Bad(t *testing.T) {
|
|||
pubCfg := PublisherConfig{Type: "github"}
|
||||
relCfg := &mockReleaseConfig{repository: ""} // Empty repository
|
||||
|
||||
err = p.Publish(context.Background(), release, pubCfg, relCfg, true)
|
||||
err := p.Publish(context.Background(), release, pubCfg, relCfg, true)
|
||||
|
||||
// Should fail because detectRepository will fail on non-git dir
|
||||
assert.Error(t, err)
|
||||
|
|
@ -408,21 +371,14 @@ func TestGitHubPublisher_Publish_Bad(t *testing.T) {
|
|||
})
|
||||
}
|
||||
|
||||
func TestDetectRepository_Good(t *testing.T) {
|
||||
func TestGitHub_DetectRepository_Good(t *testing.T) {
|
||||
t.Run("detects repository from git remote", func(t *testing.T) {
|
||||
// Create a temp git repo
|
||||
tmpDir, err := os.MkdirTemp("", "git-test")
|
||||
require.NoError(t, err)
|
||||
defer func() { _ = os.RemoveAll(tmpDir) }()
|
||||
tmpDir := t.TempDir()
|
||||
|
||||
// Initialize git repo and set remote
|
||||
cmd := exec.Command("git", "init")
|
||||
cmd.Dir = tmpDir
|
||||
require.NoError(t, cmd.Run())
|
||||
|
||||
cmd = exec.Command("git", "remote", "add", "origin", "git@github.com:test-owner/test-repo.git")
|
||||
cmd.Dir = tmpDir
|
||||
require.NoError(t, cmd.Run())
|
||||
runPublisherCommand(t, tmpDir, "git", "init")
|
||||
runPublisherCommand(t, tmpDir, "git", "remote", "add", "origin", "git@github.com:test-owner/test-repo.git")
|
||||
|
||||
repo, err := detectRepository(tmpDir)
|
||||
require.NoError(t, err)
|
||||
|
|
@ -430,17 +386,9 @@ func TestDetectRepository_Good(t *testing.T) {
|
|||
})
|
||||
|
||||
t.Run("detects repository from HTTPS remote", func(t *testing.T) {
|
||||
tmpDir, err := os.MkdirTemp("", "git-test")
|
||||
require.NoError(t, err)
|
||||
defer func() { _ = os.RemoveAll(tmpDir) }()
|
||||
|
||||
cmd := exec.Command("git", "init")
|
||||
cmd.Dir = tmpDir
|
||||
require.NoError(t, cmd.Run())
|
||||
|
||||
cmd = exec.Command("git", "remote", "add", "origin", "https://github.com/another-owner/another-repo.git")
|
||||
cmd.Dir = tmpDir
|
||||
require.NoError(t, cmd.Run())
|
||||
tmpDir := t.TempDir()
|
||||
runPublisherCommand(t, tmpDir, "git", "init")
|
||||
runPublisherCommand(t, tmpDir, "git", "remote", "add", "origin", "https://github.com/another-owner/another-repo.git")
|
||||
|
||||
repo, err := detectRepository(tmpDir)
|
||||
require.NoError(t, err)
|
||||
|
|
@ -448,13 +396,11 @@ func TestDetectRepository_Good(t *testing.T) {
|
|||
})
|
||||
}
|
||||
|
||||
func TestDetectRepository_Bad(t *testing.T) {
|
||||
func TestGitHub_DetectRepository_Bad(t *testing.T) {
|
||||
t.Run("fails when not a git repository", func(t *testing.T) {
|
||||
tmpDir, err := os.MkdirTemp("", "no-git-test")
|
||||
require.NoError(t, err)
|
||||
defer func() { _ = os.RemoveAll(tmpDir) }()
|
||||
tmpDir := t.TempDir()
|
||||
|
||||
_, err = detectRepository(tmpDir)
|
||||
_, err := detectRepository(tmpDir)
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "failed to get git remote")
|
||||
})
|
||||
|
|
@ -465,25 +411,17 @@ func TestDetectRepository_Bad(t *testing.T) {
|
|||
})
|
||||
|
||||
t.Run("fails when remote is not GitHub", func(t *testing.T) {
|
||||
tmpDir, err := os.MkdirTemp("", "git-test")
|
||||
require.NoError(t, err)
|
||||
defer func() { _ = os.RemoveAll(tmpDir) }()
|
||||
tmpDir := t.TempDir()
|
||||
runPublisherCommand(t, tmpDir, "git", "init")
|
||||
runPublisherCommand(t, tmpDir, "git", "remote", "add", "origin", "git@gitlab.com:owner/repo.git")
|
||||
|
||||
cmd := exec.Command("git", "init")
|
||||
cmd.Dir = tmpDir
|
||||
require.NoError(t, cmd.Run())
|
||||
|
||||
cmd = exec.Command("git", "remote", "add", "origin", "git@gitlab.com:owner/repo.git")
|
||||
cmd.Dir = tmpDir
|
||||
require.NoError(t, cmd.Run())
|
||||
|
||||
_, err = detectRepository(tmpDir)
|
||||
_, err := detectRepository(tmpDir)
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "not a GitHub URL")
|
||||
})
|
||||
}
|
||||
|
||||
func TestValidateGhCli_Bad(t *testing.T) {
|
||||
func TestGitHub_ValidateGhCli_Bad(t *testing.T) {
|
||||
// This test verifies the error messages from validateGhCli
|
||||
// We can't easily mock exec.Command, but we can at least
|
||||
// verify the function exists and returns expected error types
|
||||
|
|
@ -494,15 +432,15 @@ func TestValidateGhCli_Bad(t *testing.T) {
|
|||
if err != nil {
|
||||
// Either gh is not installed or not authenticated
|
||||
assert.True(t,
|
||||
strings.Contains(err.Error(), "gh CLI not found") ||
|
||||
strings.Contains(err.Error(), "not authenticated"),
|
||||
core.Contains(err.Error(), "gh CLI not found") ||
|
||||
core.Contains(err.Error(), "not authenticated"),
|
||||
"unexpected error: %s", err.Error())
|
||||
}
|
||||
// If err is nil, gh is installed and authenticated - that's OK too
|
||||
})
|
||||
}
|
||||
|
||||
func TestGitHubPublisher_ExecutePublish_Good(t *testing.T) {
|
||||
func TestGitHub_GitHubPublisherExecutePublish_Good(t *testing.T) {
|
||||
// These tests run only when gh CLI is available and authenticated
|
||||
if err := validateGhCli(); err != nil {
|
||||
t.Skip("skipping test: gh CLI not available or not authenticated")
|
||||
|
|
@ -535,7 +473,7 @@ func TestGitHubPublisher_ExecutePublish_Good(t *testing.T) {
|
|||
})
|
||||
}
|
||||
|
||||
func TestReleaseExists_Good(t *testing.T) {
|
||||
func TestGitHub_ReleaseExists_Good(t *testing.T) {
|
||||
// These tests run only when gh CLI is available
|
||||
if err := validateGhCli(); err != nil {
|
||||
t.Skip("skipping test: gh CLI not available or not authenticated")
|
||||
|
|
|
|||
|
|
@ -5,13 +5,10 @@ import (
|
|||
"bytes"
|
||||
"context"
|
||||
"embed"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"text/template"
|
||||
|
||||
"dappco.re/go/core"
|
||||
"dappco.re/go/core/build/internal/ax"
|
||||
"dappco.re/go/core/build/pkg/build"
|
||||
coreio "dappco.re/go/core/io"
|
||||
coreerr "dappco.re/go/core/log"
|
||||
|
|
@ -21,6 +18,7 @@ import (
|
|||
var homebrewTemplates embed.FS
|
||||
|
||||
// HomebrewConfig holds Homebrew-specific configuration.
|
||||
// Usage example: declare a value of type publishers.HomebrewConfig in integrating code.
|
||||
type HomebrewConfig struct {
|
||||
// Tap is the Homebrew tap repository (e.g., "host-uk/homebrew-tap").
|
||||
Tap string
|
||||
|
|
@ -31,6 +29,7 @@ type HomebrewConfig struct {
|
|||
}
|
||||
|
||||
// OfficialConfig holds configuration for generating files for official repo PRs.
|
||||
// Usage example: declare a value of type publishers.OfficialConfig in integrating code.
|
||||
type OfficialConfig struct {
|
||||
// Enabled determines whether to generate files for official repos.
|
||||
Enabled bool
|
||||
|
|
@ -39,19 +38,23 @@ type OfficialConfig struct {
|
|||
}
|
||||
|
||||
// HomebrewPublisher publishes releases to Homebrew.
|
||||
// Usage example: declare a value of type publishers.HomebrewPublisher in integrating code.
|
||||
type HomebrewPublisher struct{}
|
||||
|
||||
// NewHomebrewPublisher creates a new Homebrew publisher.
|
||||
// Usage example: call publishers.NewHomebrewPublisher(...) from integrating code.
|
||||
func NewHomebrewPublisher() *HomebrewPublisher {
|
||||
return &HomebrewPublisher{}
|
||||
}
|
||||
|
||||
// Name returns the publisher's identifier.
|
||||
// Usage example: call value.Name(...) from integrating code.
|
||||
func (p *HomebrewPublisher) Name() string {
|
||||
return "homebrew"
|
||||
}
|
||||
|
||||
// Publish publishes the release to Homebrew.
|
||||
// Usage example: call value.Publish(...) from integrating code.
|
||||
func (p *HomebrewPublisher) Publish(ctx context.Context, release *Release, pubCfg PublisherConfig, relCfg ReleaseConfig, dryRun bool) error {
|
||||
// Parse config
|
||||
cfg := p.parseConfig(pubCfg, relCfg)
|
||||
|
|
@ -79,7 +82,7 @@ func (p *HomebrewPublisher) Publish(ctx context.Context, release *Release, pubCf
|
|||
projectName = relCfg.GetProjectName()
|
||||
}
|
||||
if projectName == "" {
|
||||
parts := strings.Split(repo, "/")
|
||||
parts := core.Split(repo, "/")
|
||||
projectName = parts[len(parts)-1]
|
||||
}
|
||||
|
||||
|
|
@ -89,7 +92,7 @@ func (p *HomebrewPublisher) Publish(ctx context.Context, release *Release, pubCf
|
|||
}
|
||||
|
||||
// Strip leading 'v' from version
|
||||
version := strings.TrimPrefix(release.Version, "v")
|
||||
version := core.TrimPrefix(release.Version, "v")
|
||||
|
||||
// Build checksums map from artifacts
|
||||
checksums := buildChecksumMap(release.Artifacts)
|
||||
|
|
@ -97,7 +100,7 @@ func (p *HomebrewPublisher) Publish(ctx context.Context, release *Release, pubCf
|
|||
// Template data
|
||||
data := homebrewTemplateData{
|
||||
FormulaClass: toFormulaClass(formulaName),
|
||||
Description: fmt.Sprintf("%s CLI", projectName),
|
||||
Description: core.Sprintf("%s CLI", projectName),
|
||||
Repository: repo,
|
||||
Version: version,
|
||||
License: "MIT",
|
||||
|
|
@ -124,6 +127,7 @@ type homebrewTemplateData struct {
|
|||
}
|
||||
|
||||
// ChecksumMap holds checksums for different platform/arch combinations.
|
||||
// Usage example: declare a value of type publishers.ChecksumMap in integrating code.
|
||||
type ChecksumMap struct {
|
||||
DarwinAmd64 string
|
||||
DarwinArm64 string
|
||||
|
|
@ -163,38 +167,38 @@ func (p *HomebrewPublisher) parseConfig(pubCfg PublisherConfig, relCfg ReleaseCo
|
|||
|
||||
// dryRunPublish shows what would be done.
|
||||
func (p *HomebrewPublisher) dryRunPublish(m coreio.Medium, data homebrewTemplateData, cfg HomebrewConfig) error {
|
||||
fmt.Println()
|
||||
fmt.Println("=== DRY RUN: Homebrew Publish ===")
|
||||
fmt.Println()
|
||||
fmt.Printf("Formula: %s\n", data.FormulaClass)
|
||||
fmt.Printf("Version: %s\n", data.Version)
|
||||
fmt.Printf("Tap: %s\n", cfg.Tap)
|
||||
fmt.Printf("Repository: %s\n", data.Repository)
|
||||
fmt.Println()
|
||||
publisherPrintln()
|
||||
publisherPrintln("=== DRY RUN: Homebrew Publish ===")
|
||||
publisherPrintln()
|
||||
publisherPrint("Formula: %s", data.FormulaClass)
|
||||
publisherPrint("Version: %s", data.Version)
|
||||
publisherPrint("Tap: %s", cfg.Tap)
|
||||
publisherPrint("Repository: %s", data.Repository)
|
||||
publisherPrintln()
|
||||
|
||||
// Generate and show formula
|
||||
formula, err := p.renderTemplate(m, "templates/homebrew/formula.rb.tmpl", data)
|
||||
if err != nil {
|
||||
return coreerr.E("homebrew.dryRunPublish", "failed to render template", err)
|
||||
}
|
||||
fmt.Println("Generated formula.rb:")
|
||||
fmt.Println("---")
|
||||
fmt.Println(formula)
|
||||
fmt.Println("---")
|
||||
fmt.Println()
|
||||
publisherPrintln("Generated formula.rb:")
|
||||
publisherPrintln("---")
|
||||
publisherPrintln(formula)
|
||||
publisherPrintln("---")
|
||||
publisherPrintln()
|
||||
|
||||
if cfg.Tap != "" {
|
||||
fmt.Printf("Would commit to tap: %s\n", cfg.Tap)
|
||||
publisherPrint("Would commit to tap: %s", cfg.Tap)
|
||||
}
|
||||
if cfg.Official != nil && cfg.Official.Enabled {
|
||||
output := cfg.Official.Output
|
||||
if output == "" {
|
||||
output = "dist/homebrew"
|
||||
}
|
||||
fmt.Printf("Would write files for official PR to: %s\n", output)
|
||||
publisherPrint("Would write files for official PR to: %s", output)
|
||||
}
|
||||
fmt.Println()
|
||||
fmt.Println("=== END DRY RUN ===")
|
||||
publisherPrintln()
|
||||
publisherPrintln("=== END DRY RUN ===")
|
||||
|
||||
return nil
|
||||
}
|
||||
|
|
@ -211,20 +215,20 @@ func (p *HomebrewPublisher) executePublish(ctx context.Context, projectDir strin
|
|||
if cfg.Official != nil && cfg.Official.Enabled {
|
||||
output := cfg.Official.Output
|
||||
if output == "" {
|
||||
output = filepath.Join(projectDir, "dist", "homebrew")
|
||||
} else if !filepath.IsAbs(output) {
|
||||
output = filepath.Join(projectDir, output)
|
||||
output = ax.Join(projectDir, "dist", "homebrew")
|
||||
} else if !ax.IsAbs(output) {
|
||||
output = ax.Join(projectDir, output)
|
||||
}
|
||||
|
||||
if err := release.FS.EnsureDir(output); err != nil {
|
||||
return coreerr.E("homebrew.Publish", "failed to create output directory", err)
|
||||
}
|
||||
|
||||
formulaPath := filepath.Join(output, fmt.Sprintf("%s.rb", strings.ToLower(data.FormulaClass)))
|
||||
formulaPath := ax.Join(output, core.Sprintf("%s.rb", core.Lower(data.FormulaClass)))
|
||||
if err := release.FS.Write(formulaPath, formula); err != nil {
|
||||
return coreerr.E("homebrew.Publish", "failed to write formula", err)
|
||||
}
|
||||
fmt.Printf("Wrote Homebrew formula for official PR: %s\n", formulaPath)
|
||||
publisherPrint("Wrote Homebrew formula for official PR: %s", formulaPath)
|
||||
}
|
||||
|
||||
// If tap is configured, commit to it
|
||||
|
|
@ -240,59 +244,46 @@ func (p *HomebrewPublisher) executePublish(ctx context.Context, projectDir strin
|
|||
// commitToTap commits the formula to the tap repository.
|
||||
func (p *HomebrewPublisher) commitToTap(ctx context.Context, tap string, data homebrewTemplateData, formula string) error {
|
||||
// Clone tap repo to temp directory
|
||||
tmpDir, err := os.MkdirTemp("", "homebrew-tap-*")
|
||||
tmpDir, err := ax.TempDir("homebrew-tap-*")
|
||||
if err != nil {
|
||||
return coreerr.E("homebrew.commitToTap", "failed to create temp directory", err)
|
||||
}
|
||||
defer func() { _ = coreio.Local.DeleteAll(tmpDir) }()
|
||||
defer func() { _ = ax.RemoveAll(tmpDir) }()
|
||||
|
||||
// Clone the tap
|
||||
fmt.Printf("Cloning tap %s...\n", tap)
|
||||
cmd := exec.CommandContext(ctx, "gh", "repo", "clone", tap, tmpDir, "--", "--depth=1")
|
||||
cmd.Stdout = os.Stdout
|
||||
cmd.Stderr = os.Stderr
|
||||
if err := cmd.Run(); err != nil {
|
||||
publisherPrint("Cloning tap %s...", tap)
|
||||
if err := publisherRun(ctx, "", nil, "gh", "repo", "clone", tap, tmpDir, "--", "--depth=1"); err != nil {
|
||||
return coreerr.E("homebrew.commitToTap", "failed to clone tap", err)
|
||||
}
|
||||
|
||||
// Ensure Formula directory exists
|
||||
formulaDir := filepath.Join(tmpDir, "Formula")
|
||||
formulaDir := ax.Join(tmpDir, "Formula")
|
||||
if err := coreio.Local.EnsureDir(formulaDir); err != nil {
|
||||
return coreerr.E("homebrew.commitToTap", "failed to create Formula directory", err)
|
||||
}
|
||||
|
||||
// Write formula
|
||||
formulaPath := filepath.Join(formulaDir, fmt.Sprintf("%s.rb", strings.ToLower(data.FormulaClass)))
|
||||
formulaPath := ax.Join(formulaDir, core.Sprintf("%s.rb", core.Lower(data.FormulaClass)))
|
||||
if err := coreio.Local.Write(formulaPath, formula); err != nil {
|
||||
return coreerr.E("homebrew.commitToTap", "failed to write formula", err)
|
||||
}
|
||||
|
||||
// Git add, commit, push
|
||||
commitMsg := fmt.Sprintf("Update %s to %s", data.FormulaClass, data.Version)
|
||||
commitMsg := core.Sprintf("Update %s to %s", data.FormulaClass, data.Version)
|
||||
|
||||
cmd = exec.CommandContext(ctx, "git", "add", ".")
|
||||
cmd.Dir = tmpDir
|
||||
if err := cmd.Run(); err != nil {
|
||||
if err := ax.ExecDir(ctx, tmpDir, "git", "add", "."); err != nil {
|
||||
return coreerr.E("homebrew.commitToTap", "git add failed", err)
|
||||
}
|
||||
|
||||
cmd = exec.CommandContext(ctx, "git", "commit", "-m", commitMsg)
|
||||
cmd.Dir = tmpDir
|
||||
cmd.Stdout = os.Stdout
|
||||
cmd.Stderr = os.Stderr
|
||||
if err := cmd.Run(); err != nil {
|
||||
if err := publisherRun(ctx, tmpDir, nil, "git", "commit", "-m", commitMsg); err != nil {
|
||||
return coreerr.E("homebrew.commitToTap", "git commit failed", err)
|
||||
}
|
||||
|
||||
cmd = exec.CommandContext(ctx, "git", "push")
|
||||
cmd.Dir = tmpDir
|
||||
cmd.Stdout = os.Stdout
|
||||
cmd.Stderr = os.Stderr
|
||||
if err := cmd.Run(); err != nil {
|
||||
if err := publisherRun(ctx, tmpDir, nil, "git", "push"); err != nil {
|
||||
return coreerr.E("homebrew.commitToTap", "git push failed", err)
|
||||
}
|
||||
|
||||
fmt.Printf("Updated Homebrew tap: %s\n", tap)
|
||||
publisherPrint("Updated Homebrew tap: %s", tap)
|
||||
return nil
|
||||
}
|
||||
|
||||
|
|
@ -302,7 +293,7 @@ func (p *HomebrewPublisher) renderTemplate(m coreio.Medium, name string, data ho
|
|||
var err error
|
||||
|
||||
// Try custom template from medium
|
||||
customPath := filepath.Join(".core", name)
|
||||
customPath := ax.Join(".core", name)
|
||||
if m != nil && m.IsFile(customPath) {
|
||||
customContent, err := m.Read(customPath)
|
||||
if err == nil {
|
||||
|
|
@ -318,7 +309,7 @@ func (p *HomebrewPublisher) renderTemplate(m coreio.Medium, name string, data ho
|
|||
}
|
||||
}
|
||||
|
||||
tmpl, err := template.New(filepath.Base(name)).Parse(string(content))
|
||||
tmpl, err := template.New(ax.Base(name)).Parse(string(content))
|
||||
if err != nil {
|
||||
return "", coreerr.E("homebrew.renderTemplate", "failed to parse template "+name, err)
|
||||
}
|
||||
|
|
@ -334,13 +325,13 @@ func (p *HomebrewPublisher) renderTemplate(m coreio.Medium, name string, data ho
|
|||
// toFormulaClass converts a package name to a Ruby class name.
|
||||
func toFormulaClass(name string) string {
|
||||
// Convert kebab-case to PascalCase
|
||||
parts := strings.Split(name, "-")
|
||||
parts := core.Split(name, "-")
|
||||
for i, part := range parts {
|
||||
if len(part) > 0 {
|
||||
parts[i] = strings.ToUpper(part[:1]) + part[1:]
|
||||
parts[i] = core.Upper(part[:1]) + part[1:]
|
||||
}
|
||||
}
|
||||
return strings.Join(parts, "")
|
||||
return core.Join("", parts...)
|
||||
}
|
||||
|
||||
// buildChecksumMap extracts checksums from artifacts into a structured map.
|
||||
|
|
@ -349,21 +340,21 @@ func buildChecksumMap(artifacts []build.Artifact) ChecksumMap {
|
|||
|
||||
for _, a := range artifacts {
|
||||
// Parse artifact name to determine platform
|
||||
name := filepath.Base(a.Path)
|
||||
name := ax.Base(a.Path)
|
||||
checksum := a.Checksum
|
||||
|
||||
switch {
|
||||
case strings.Contains(name, "darwin-amd64"):
|
||||
case core.Contains(name, "darwin-amd64"):
|
||||
checksums.DarwinAmd64 = checksum
|
||||
case strings.Contains(name, "darwin-arm64"):
|
||||
case core.Contains(name, "darwin-arm64"):
|
||||
checksums.DarwinArm64 = checksum
|
||||
case strings.Contains(name, "linux-amd64"):
|
||||
case core.Contains(name, "linux-amd64"):
|
||||
checksums.LinuxAmd64 = checksum
|
||||
case strings.Contains(name, "linux-arm64"):
|
||||
case core.Contains(name, "linux-arm64"):
|
||||
checksums.LinuxArm64 = checksum
|
||||
case strings.Contains(name, "windows-amd64"):
|
||||
case core.Contains(name, "windows-amd64"):
|
||||
checksums.WindowsAmd64 = checksum
|
||||
case strings.Contains(name, "windows-arm64"):
|
||||
case core.Contains(name, "windows-arm64"):
|
||||
checksums.WindowsArm64 = checksum
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,9 +1,7 @@
|
|||
package publishers
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"dappco.re/go/core/build/pkg/build"
|
||||
|
|
@ -12,14 +10,14 @@ import (
|
|||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestHomebrewPublisher_Name_Good(t *testing.T) {
|
||||
func TestHomebrew_HomebrewPublisherName_Good(t *testing.T) {
|
||||
t.Run("returns homebrew", func(t *testing.T) {
|
||||
p := NewHomebrewPublisher()
|
||||
assert.Equal(t, "homebrew", p.Name())
|
||||
})
|
||||
}
|
||||
|
||||
func TestHomebrewPublisher_ParseConfig_Good(t *testing.T) {
|
||||
func TestHomebrew_HomebrewPublisherParseConfig_Good(t *testing.T) {
|
||||
p := NewHomebrewPublisher()
|
||||
|
||||
t.Run("uses defaults when no extended config", func(t *testing.T) {
|
||||
|
|
@ -81,7 +79,7 @@ func TestHomebrewPublisher_ParseConfig_Good(t *testing.T) {
|
|||
})
|
||||
}
|
||||
|
||||
func TestHomebrewPublisher_ToFormulaClass_Good(t *testing.T) {
|
||||
func TestHomebrew_HomebrewPublisherToFormulaClass_Good(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
input string
|
||||
|
|
@ -122,7 +120,7 @@ func TestHomebrewPublisher_ToFormulaClass_Good(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestHomebrewPublisher_BuildChecksumMap_Good(t *testing.T) {
|
||||
func TestHomebrew_HomebrewPublisherBuildChecksumMap_Good(t *testing.T) {
|
||||
t.Run("maps artifacts to checksums by platform", func(t *testing.T) {
|
||||
artifacts := []build.Artifact{
|
||||
{Path: "/dist/myapp-darwin-amd64.tar.gz", OS: "darwin", Arch: "amd64", Checksum: "abc123"},
|
||||
|
|
@ -167,7 +165,7 @@ func TestHomebrewPublisher_BuildChecksumMap_Good(t *testing.T) {
|
|||
})
|
||||
}
|
||||
|
||||
func TestHomebrewPublisher_RenderTemplate_Good(t *testing.T) {
|
||||
func TestHomebrew_HomebrewPublisherRenderTemplate_Good(t *testing.T) {
|
||||
p := NewHomebrewPublisher()
|
||||
|
||||
t.Run("renders formula template with data", func(t *testing.T) {
|
||||
|
|
@ -202,7 +200,7 @@ func TestHomebrewPublisher_RenderTemplate_Good(t *testing.T) {
|
|||
})
|
||||
}
|
||||
|
||||
func TestHomebrewPublisher_RenderTemplate_Bad(t *testing.T) {
|
||||
func TestHomebrew_HomebrewPublisherRenderTemplate_Bad(t *testing.T) {
|
||||
p := NewHomebrewPublisher()
|
||||
|
||||
t.Run("returns error for non-existent template", func(t *testing.T) {
|
||||
|
|
@ -213,15 +211,10 @@ func TestHomebrewPublisher_RenderTemplate_Bad(t *testing.T) {
|
|||
})
|
||||
}
|
||||
|
||||
func TestHomebrewPublisher_DryRunPublish_Good(t *testing.T) {
|
||||
func TestHomebrew_HomebrewPublisherDryRunPublish_Good(t *testing.T) {
|
||||
p := NewHomebrewPublisher()
|
||||
|
||||
t.Run("outputs expected dry run information", func(t *testing.T) {
|
||||
// Capture stdout
|
||||
oldStdout := os.Stdout
|
||||
r, w, _ := os.Pipe()
|
||||
os.Stdout = w
|
||||
|
||||
data := homebrewTemplateData{
|
||||
FormulaClass: "MyApp",
|
||||
Description: "My CLI",
|
||||
|
|
@ -235,15 +228,11 @@ func TestHomebrewPublisher_DryRunPublish_Good(t *testing.T) {
|
|||
Tap: "owner/homebrew-tap",
|
||||
}
|
||||
|
||||
err := p.dryRunPublish(io.Local, data, cfg)
|
||||
|
||||
_ = w.Close()
|
||||
var buf bytes.Buffer
|
||||
_, _ = buf.ReadFrom(r)
|
||||
os.Stdout = oldStdout
|
||||
|
||||
var err error
|
||||
output := capturePublisherOutput(t, func() {
|
||||
err = p.dryRunPublish(io.Local, data, cfg)
|
||||
})
|
||||
require.NoError(t, err)
|
||||
output := buf.String()
|
||||
|
||||
assert.Contains(t, output, "DRY RUN: Homebrew Publish")
|
||||
assert.Contains(t, output, "Formula: MyApp")
|
||||
|
|
@ -255,10 +244,6 @@ func TestHomebrewPublisher_DryRunPublish_Good(t *testing.T) {
|
|||
})
|
||||
|
||||
t.Run("shows official output path when enabled", func(t *testing.T) {
|
||||
oldStdout := os.Stdout
|
||||
r, w, _ := os.Pipe()
|
||||
os.Stdout = w
|
||||
|
||||
data := homebrewTemplateData{
|
||||
FormulaClass: "MyApp",
|
||||
Version: "1.0.0",
|
||||
|
|
@ -272,23 +257,15 @@ func TestHomebrewPublisher_DryRunPublish_Good(t *testing.T) {
|
|||
},
|
||||
}
|
||||
|
||||
err := p.dryRunPublish(io.Local, data, cfg)
|
||||
|
||||
_ = w.Close()
|
||||
var buf bytes.Buffer
|
||||
_, _ = buf.ReadFrom(r)
|
||||
os.Stdout = oldStdout
|
||||
|
||||
var err error
|
||||
output := capturePublisherOutput(t, func() {
|
||||
err = p.dryRunPublish(io.Local, data, cfg)
|
||||
})
|
||||
require.NoError(t, err)
|
||||
output := buf.String()
|
||||
assert.Contains(t, output, "Would write files for official PR to: custom/path")
|
||||
})
|
||||
|
||||
t.Run("uses default official output path when not specified", func(t *testing.T) {
|
||||
oldStdout := os.Stdout
|
||||
r, w, _ := os.Pipe()
|
||||
os.Stdout = w
|
||||
|
||||
data := homebrewTemplateData{
|
||||
FormulaClass: "MyApp",
|
||||
Version: "1.0.0",
|
||||
|
|
@ -301,20 +278,16 @@ func TestHomebrewPublisher_DryRunPublish_Good(t *testing.T) {
|
|||
},
|
||||
}
|
||||
|
||||
err := p.dryRunPublish(io.Local, data, cfg)
|
||||
|
||||
_ = w.Close()
|
||||
var buf bytes.Buffer
|
||||
_, _ = buf.ReadFrom(r)
|
||||
os.Stdout = oldStdout
|
||||
|
||||
var err error
|
||||
output := capturePublisherOutput(t, func() {
|
||||
err = p.dryRunPublish(io.Local, data, cfg)
|
||||
})
|
||||
require.NoError(t, err)
|
||||
output := buf.String()
|
||||
assert.Contains(t, output, "Would write files for official PR to: dist/homebrew")
|
||||
})
|
||||
}
|
||||
|
||||
func TestHomebrewPublisher_Publish_Bad(t *testing.T) {
|
||||
func TestHomebrew_HomebrewPublisherPublish_Bad(t *testing.T) {
|
||||
p := NewHomebrewPublisher()
|
||||
|
||||
t.Run("fails when tap not configured and not official mode", func(t *testing.T) {
|
||||
|
|
@ -332,7 +305,7 @@ func TestHomebrewPublisher_Publish_Bad(t *testing.T) {
|
|||
})
|
||||
}
|
||||
|
||||
func TestHomebrewConfig_Defaults_Good(t *testing.T) {
|
||||
func TestHomebrew_HomebrewConfigDefaults_Good(t *testing.T) {
|
||||
t.Run("has sensible defaults", func(t *testing.T) {
|
||||
p := NewHomebrewPublisher()
|
||||
pubCfg := PublisherConfig{Type: "homebrew"}
|
||||
|
|
|
|||
|
|
@ -1,13 +1,10 @@
|
|||
package publishers
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"dappco.re/go/core/build/internal/ax"
|
||||
"dappco.re/go/core/build/pkg/build"
|
||||
"dappco.re/go/core/io"
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
|
@ -16,7 +13,7 @@ import (
|
|||
|
||||
// --- GitHub Publisher Integration Tests ---
|
||||
|
||||
func TestGitHubPublisher_Integration_DryRunNoSideEffects_Good(t *testing.T) {
|
||||
func TestIntegration_GitHubPublisherIntegrationDryRunNoSideEffects_Good(t *testing.T) {
|
||||
p := NewGitHubPublisher()
|
||||
|
||||
t.Run("dry run creates no files on disk", func(t *testing.T) {
|
||||
|
|
@ -27,9 +24,9 @@ func TestGitHubPublisher_Integration_DryRunNoSideEffects_Good(t *testing.T) {
|
|||
ProjectDir: tmpDir,
|
||||
FS: io.Local,
|
||||
Artifacts: []build.Artifact{
|
||||
{Path: filepath.Join(tmpDir, "app-linux-amd64.tar.gz")},
|
||||
{Path: filepath.Join(tmpDir, "app-darwin-arm64.tar.gz")},
|
||||
{Path: filepath.Join(tmpDir, "CHECKSUMS.txt")},
|
||||
{Path: ax.Join(tmpDir, "app-linux-amd64.tar.gz")},
|
||||
{Path: ax.Join(tmpDir, "app-darwin-arm64.tar.gz")},
|
||||
{Path: ax.Join(tmpDir, "CHECKSUMS.txt")},
|
||||
},
|
||||
}
|
||||
pubCfg := PublisherConfig{
|
||||
|
|
@ -39,20 +36,11 @@ func TestGitHubPublisher_Integration_DryRunNoSideEffects_Good(t *testing.T) {
|
|||
}
|
||||
relCfg := &mockReleaseConfig{repository: "test-org/test-repo", projectName: "testapp"}
|
||||
|
||||
// Capture stdout
|
||||
oldStdout := os.Stdout
|
||||
r, w, _ := os.Pipe()
|
||||
os.Stdout = w
|
||||
|
||||
err := p.Publish(context.Background(), release, pubCfg, relCfg, true)
|
||||
|
||||
_ = w.Close()
|
||||
var buf bytes.Buffer
|
||||
_, _ = buf.ReadFrom(r)
|
||||
os.Stdout = oldStdout
|
||||
|
||||
var err error
|
||||
output := capturePublisherOutput(t, func() {
|
||||
err = p.Publish(context.Background(), release, pubCfg, relCfg, true)
|
||||
})
|
||||
require.NoError(t, err)
|
||||
output := buf.String()
|
||||
|
||||
// Verify dry run output contains expected information
|
||||
assert.Contains(t, output, "DRY RUN: GitHub Release")
|
||||
|
|
@ -69,7 +57,7 @@ func TestGitHubPublisher_Integration_DryRunNoSideEffects_Good(t *testing.T) {
|
|||
assert.Contains(t, output, "--prerelease")
|
||||
|
||||
// Verify no files were created in the temp directory
|
||||
entries, err := os.ReadDir(tmpDir)
|
||||
entries, err := ax.ReadDir(tmpDir)
|
||||
require.NoError(t, err)
|
||||
assert.Empty(t, entries, "dry run should not create any files")
|
||||
})
|
||||
|
|
@ -131,14 +119,10 @@ func TestGitHubPublisher_Integration_DryRunNoSideEffects_Good(t *testing.T) {
|
|||
})
|
||||
}
|
||||
|
||||
func TestGitHubPublisher_Integration_RepositoryDetection_Good(t *testing.T) {
|
||||
func TestIntegration_GitHubPublisherIntegrationRepositoryDetection_Good(t *testing.T) {
|
||||
p := NewGitHubPublisher()
|
||||
|
||||
t.Run("uses relCfg repository when provided", func(t *testing.T) {
|
||||
oldStdout := os.Stdout
|
||||
r, w, _ := os.Pipe()
|
||||
os.Stdout = w
|
||||
|
||||
release := &Release{
|
||||
Version: "v1.0.0",
|
||||
Changelog: "Changes",
|
||||
|
|
@ -148,31 +132,19 @@ func TestGitHubPublisher_Integration_RepositoryDetection_Good(t *testing.T) {
|
|||
pubCfg := PublisherConfig{Type: "github"}
|
||||
relCfg := &mockReleaseConfig{repository: "explicit/repo"}
|
||||
|
||||
err := p.Publish(context.Background(), release, pubCfg, relCfg, true)
|
||||
|
||||
_ = w.Close()
|
||||
var buf bytes.Buffer
|
||||
_, _ = buf.ReadFrom(r)
|
||||
os.Stdout = oldStdout
|
||||
|
||||
var err error
|
||||
output := capturePublisherOutput(t, func() {
|
||||
err = p.Publish(context.Background(), release, pubCfg, relCfg, true)
|
||||
})
|
||||
require.NoError(t, err)
|
||||
assert.Contains(t, buf.String(), "Repository: explicit/repo")
|
||||
assert.Contains(t, output, "Repository: explicit/repo")
|
||||
})
|
||||
|
||||
t.Run("detects repository from git remote when relCfg empty", func(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
|
||||
cmd := exec.Command("git", "init")
|
||||
cmd.Dir = tmpDir
|
||||
require.NoError(t, cmd.Run())
|
||||
|
||||
cmd = exec.Command("git", "remote", "add", "origin", "https://github.com/detected/from-git.git")
|
||||
cmd.Dir = tmpDir
|
||||
require.NoError(t, cmd.Run())
|
||||
|
||||
oldStdout := os.Stdout
|
||||
r, w, _ := os.Pipe()
|
||||
os.Stdout = w
|
||||
runPublisherCommand(t, tmpDir, "git", "init")
|
||||
runPublisherCommand(t, tmpDir, "git", "remote", "add", "origin", "https://github.com/detected/from-git.git")
|
||||
|
||||
release := &Release{
|
||||
Version: "v1.0.0",
|
||||
|
|
@ -183,15 +155,12 @@ func TestGitHubPublisher_Integration_RepositoryDetection_Good(t *testing.T) {
|
|||
pubCfg := PublisherConfig{Type: "github"}
|
||||
relCfg := &mockReleaseConfig{repository: ""}
|
||||
|
||||
err := p.Publish(context.Background(), release, pubCfg, relCfg, true)
|
||||
|
||||
_ = w.Close()
|
||||
var buf bytes.Buffer
|
||||
_, _ = buf.ReadFrom(r)
|
||||
os.Stdout = oldStdout
|
||||
|
||||
var err error
|
||||
output := capturePublisherOutput(t, func() {
|
||||
err = p.Publish(context.Background(), release, pubCfg, relCfg, true)
|
||||
})
|
||||
require.NoError(t, err)
|
||||
assert.Contains(t, buf.String(), "Repository: detected/from-git")
|
||||
assert.Contains(t, output, "Repository: detected/from-git")
|
||||
})
|
||||
|
||||
t.Run("fails when no repository available", func(t *testing.T) {
|
||||
|
|
@ -212,14 +181,10 @@ func TestGitHubPublisher_Integration_RepositoryDetection_Good(t *testing.T) {
|
|||
})
|
||||
}
|
||||
|
||||
func TestGitHubPublisher_Integration_ArtifactUpload_Good(t *testing.T) {
|
||||
func TestIntegration_GitHubPublisherIntegrationArtifactUpload_Good(t *testing.T) {
|
||||
p := NewGitHubPublisher()
|
||||
|
||||
t.Run("dry run lists all artifact types", func(t *testing.T) {
|
||||
oldStdout := os.Stdout
|
||||
r, w, _ := os.Pipe()
|
||||
os.Stdout = w
|
||||
|
||||
release := &Release{
|
||||
Version: "v1.0.0",
|
||||
Changelog: "Release notes",
|
||||
|
|
@ -235,15 +200,11 @@ func TestGitHubPublisher_Integration_ArtifactUpload_Good(t *testing.T) {
|
|||
}
|
||||
pubCfg := PublisherConfig{Type: "github"}
|
||||
|
||||
err := p.dryRunPublish(release, pubCfg, "owner/repo")
|
||||
|
||||
_ = w.Close()
|
||||
var buf bytes.Buffer
|
||||
_, _ = buf.ReadFrom(r)
|
||||
os.Stdout = oldStdout
|
||||
|
||||
var err error
|
||||
output := capturePublisherOutput(t, func() {
|
||||
err = p.dryRunPublish(release, pubCfg, "owner/repo")
|
||||
})
|
||||
require.NoError(t, err)
|
||||
output := buf.String()
|
||||
|
||||
assert.Contains(t, output, "Would upload artifacts:")
|
||||
assert.Contains(t, output, "app-linux-amd64.tar.gz")
|
||||
|
|
@ -281,7 +242,7 @@ func TestGitHubPublisher_Integration_ArtifactUpload_Good(t *testing.T) {
|
|||
|
||||
// --- Docker Publisher Integration Tests ---
|
||||
|
||||
func TestDockerPublisher_Integration_DryRunNoSideEffects_Good(t *testing.T) {
|
||||
func TestIntegration_DockerPublisherIntegrationDryRunNoSideEffects_Good(t *testing.T) {
|
||||
if err := validateDockerCli(); err != nil {
|
||||
t.Skip("skipping: docker CLI not available")
|
||||
}
|
||||
|
|
@ -292,13 +253,9 @@ func TestDockerPublisher_Integration_DryRunNoSideEffects_Good(t *testing.T) {
|
|||
tmpDir := t.TempDir()
|
||||
|
||||
// Create a Dockerfile
|
||||
err := os.WriteFile(filepath.Join(tmpDir, "Dockerfile"), []byte("FROM alpine:latest\n"), 0644)
|
||||
err := ax.WriteFile(ax.Join(tmpDir, "Dockerfile"), []byte("FROM alpine:latest\n"), 0o644)
|
||||
require.NoError(t, err)
|
||||
|
||||
oldStdout := os.Stdout
|
||||
r, w, _ := os.Pipe()
|
||||
os.Stdout = w
|
||||
|
||||
release := &Release{
|
||||
Version: "v1.2.3",
|
||||
ProjectDir: tmpDir,
|
||||
|
|
@ -319,15 +276,10 @@ func TestDockerPublisher_Integration_DryRunNoSideEffects_Good(t *testing.T) {
|
|||
}
|
||||
relCfg := &mockReleaseConfig{repository: "test-org/test-app"}
|
||||
|
||||
err = p.Publish(context.Background(), release, pubCfg, relCfg, true)
|
||||
|
||||
_ = w.Close()
|
||||
var buf bytes.Buffer
|
||||
_, _ = buf.ReadFrom(r)
|
||||
os.Stdout = oldStdout
|
||||
|
||||
output := capturePublisherOutput(t, func() {
|
||||
err = p.Publish(context.Background(), release, pubCfg, relCfg, true)
|
||||
})
|
||||
require.NoError(t, err)
|
||||
output := buf.String()
|
||||
|
||||
// Verify dry run output
|
||||
assert.Contains(t, output, "DRY RUN: Docker Build & Push")
|
||||
|
|
@ -399,7 +351,7 @@ func TestDockerPublisher_Integration_DryRunNoSideEffects_Good(t *testing.T) {
|
|||
})
|
||||
}
|
||||
|
||||
func TestDockerPublisher_Integration_ConfigParsing_Good(t *testing.T) {
|
||||
func TestIntegration_DockerPublisherIntegrationConfigParsing_Good(t *testing.T) {
|
||||
p := NewDockerPublisher()
|
||||
|
||||
t.Run("full config round-trip from PublisherConfig to DockerConfig", func(t *testing.T) {
|
||||
|
|
@ -437,16 +389,12 @@ func TestDockerPublisher_Integration_ConfigParsing_Good(t *testing.T) {
|
|||
|
||||
// --- Homebrew Publisher Integration Tests ---
|
||||
|
||||
func TestHomebrewPublisher_Integration_DryRunNoSideEffects_Good(t *testing.T) {
|
||||
func TestIntegration_HomebrewPublisherIntegrationDryRunNoSideEffects_Good(t *testing.T) {
|
||||
p := NewHomebrewPublisher()
|
||||
|
||||
t.Run("dry run generates formula without writing files", func(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
|
||||
oldStdout := os.Stdout
|
||||
r, w, _ := os.Pipe()
|
||||
os.Stdout = w
|
||||
|
||||
release := &Release{
|
||||
Version: "v2.1.0",
|
||||
ProjectDir: tmpDir,
|
||||
|
|
@ -467,15 +415,11 @@ func TestHomebrewPublisher_Integration_DryRunNoSideEffects_Good(t *testing.T) {
|
|||
}
|
||||
relCfg := &mockReleaseConfig{repository: "test-org/my-cli", projectName: "my-cli"}
|
||||
|
||||
err := p.Publish(context.Background(), release, pubCfg, relCfg, true)
|
||||
|
||||
_ = w.Close()
|
||||
var buf bytes.Buffer
|
||||
_, _ = buf.ReadFrom(r)
|
||||
os.Stdout = oldStdout
|
||||
|
||||
var err error
|
||||
output := capturePublisherOutput(t, func() {
|
||||
err = p.Publish(context.Background(), release, pubCfg, relCfg, true)
|
||||
})
|
||||
require.NoError(t, err)
|
||||
output := buf.String()
|
||||
|
||||
// Verify dry run output
|
||||
assert.Contains(t, output, "DRY RUN: Homebrew Publish")
|
||||
|
|
@ -495,16 +439,12 @@ func TestHomebrewPublisher_Integration_DryRunNoSideEffects_Good(t *testing.T) {
|
|||
assert.Contains(t, output, "Would commit to tap: test-org/homebrew-tap")
|
||||
|
||||
// Verify no files created
|
||||
entries, err := os.ReadDir(tmpDir)
|
||||
entries, err := ax.ReadDir(tmpDir)
|
||||
require.NoError(t, err)
|
||||
assert.Empty(t, entries, "dry run should not create any files")
|
||||
})
|
||||
|
||||
t.Run("dry run with official config shows output path", func(t *testing.T) {
|
||||
oldStdout := os.Stdout
|
||||
r, w, _ := os.Pipe()
|
||||
os.Stdout = w
|
||||
|
||||
release := &Release{
|
||||
Version: "v1.0.0",
|
||||
ProjectDir: "/project",
|
||||
|
|
@ -522,20 +462,16 @@ func TestHomebrewPublisher_Integration_DryRunNoSideEffects_Good(t *testing.T) {
|
|||
}
|
||||
relCfg := &mockReleaseConfig{repository: "owner/repo", projectName: "repo"}
|
||||
|
||||
err := p.Publish(context.Background(), release, pubCfg, relCfg, true)
|
||||
|
||||
_ = w.Close()
|
||||
var buf bytes.Buffer
|
||||
_, _ = buf.ReadFrom(r)
|
||||
os.Stdout = oldStdout
|
||||
|
||||
var err error
|
||||
output := capturePublisherOutput(t, func() {
|
||||
err = p.Publish(context.Background(), release, pubCfg, relCfg, true)
|
||||
})
|
||||
require.NoError(t, err)
|
||||
output := buf.String()
|
||||
assert.Contains(t, output, "Would write files for official PR to: dist/homebrew-official")
|
||||
})
|
||||
}
|
||||
|
||||
func TestHomebrewPublisher_Integration_FormulaGeneration_Good(t *testing.T) {
|
||||
func TestIntegration_HomebrewPublisherIntegrationFormulaGeneration_Good(t *testing.T) {
|
||||
p := NewHomebrewPublisher()
|
||||
|
||||
t.Run("generated formula contains correct Ruby class structure", func(t *testing.T) {
|
||||
|
|
@ -599,16 +535,12 @@ func TestHomebrewPublisher_Integration_FormulaGeneration_Good(t *testing.T) {
|
|||
|
||||
// --- Scoop Publisher Integration Tests ---
|
||||
|
||||
func TestScoopPublisher_Integration_DryRunNoSideEffects_Good(t *testing.T) {
|
||||
func TestIntegration_ScoopPublisherIntegrationDryRunNoSideEffects_Good(t *testing.T) {
|
||||
p := NewScoopPublisher()
|
||||
|
||||
t.Run("dry run generates manifest without writing files", func(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
|
||||
oldStdout := os.Stdout
|
||||
r, w, _ := os.Pipe()
|
||||
os.Stdout = w
|
||||
|
||||
release := &Release{
|
||||
Version: "v1.5.0",
|
||||
ProjectDir: tmpDir,
|
||||
|
|
@ -626,15 +558,11 @@ func TestScoopPublisher_Integration_DryRunNoSideEffects_Good(t *testing.T) {
|
|||
}
|
||||
relCfg := &mockReleaseConfig{repository: "test-org/myapp", projectName: "myapp"}
|
||||
|
||||
err := p.Publish(context.Background(), release, pubCfg, relCfg, true)
|
||||
|
||||
_ = w.Close()
|
||||
var buf bytes.Buffer
|
||||
_, _ = buf.ReadFrom(r)
|
||||
os.Stdout = oldStdout
|
||||
|
||||
var err error
|
||||
output := capturePublisherOutput(t, func() {
|
||||
err = p.Publish(context.Background(), release, pubCfg, relCfg, true)
|
||||
})
|
||||
require.NoError(t, err)
|
||||
output := buf.String()
|
||||
|
||||
assert.Contains(t, output, "DRY RUN: Scoop Publish")
|
||||
assert.Contains(t, output, "Package: myapp")
|
||||
|
|
@ -645,7 +573,7 @@ func TestScoopPublisher_Integration_DryRunNoSideEffects_Good(t *testing.T) {
|
|||
assert.Contains(t, output, "Would commit to bucket: test-org/scoop-bucket")
|
||||
|
||||
// Verify no files created
|
||||
entries, err := os.ReadDir(tmpDir)
|
||||
entries, err := ax.ReadDir(tmpDir)
|
||||
require.NoError(t, err)
|
||||
assert.Empty(t, entries)
|
||||
})
|
||||
|
|
@ -653,16 +581,12 @@ func TestScoopPublisher_Integration_DryRunNoSideEffects_Good(t *testing.T) {
|
|||
|
||||
// --- AUR Publisher Integration Tests ---
|
||||
|
||||
func TestAURPublisher_Integration_DryRunNoSideEffects_Good(t *testing.T) {
|
||||
func TestIntegration_AURPublisherIntegrationDryRunNoSideEffects_Good(t *testing.T) {
|
||||
p := NewAURPublisher()
|
||||
|
||||
t.Run("dry run generates PKGBUILD and SRCINFO without writing files", func(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
|
||||
oldStdout := os.Stdout
|
||||
r, w, _ := os.Pipe()
|
||||
os.Stdout = w
|
||||
|
||||
release := &Release{
|
||||
Version: "v2.0.0",
|
||||
ProjectDir: tmpDir,
|
||||
|
|
@ -680,15 +604,11 @@ func TestAURPublisher_Integration_DryRunNoSideEffects_Good(t *testing.T) {
|
|||
}
|
||||
relCfg := &mockReleaseConfig{repository: "test-org/myapp", projectName: "myapp"}
|
||||
|
||||
err := p.Publish(context.Background(), release, pubCfg, relCfg, true)
|
||||
|
||||
_ = w.Close()
|
||||
var buf bytes.Buffer
|
||||
_, _ = buf.ReadFrom(r)
|
||||
os.Stdout = oldStdout
|
||||
|
||||
var err error
|
||||
output := capturePublisherOutput(t, func() {
|
||||
err = p.Publish(context.Background(), release, pubCfg, relCfg, true)
|
||||
})
|
||||
require.NoError(t, err)
|
||||
output := buf.String()
|
||||
|
||||
assert.Contains(t, output, "DRY RUN: AUR Publish")
|
||||
assert.Contains(t, output, "Package: myapp-bin")
|
||||
|
|
@ -702,7 +622,7 @@ func TestAURPublisher_Integration_DryRunNoSideEffects_Good(t *testing.T) {
|
|||
assert.Contains(t, output, "Would push to AUR:")
|
||||
|
||||
// Verify no files created
|
||||
entries, err := os.ReadDir(tmpDir)
|
||||
entries, err := ax.ReadDir(tmpDir)
|
||||
require.NoError(t, err)
|
||||
assert.Empty(t, entries)
|
||||
})
|
||||
|
|
@ -710,16 +630,12 @@ func TestAURPublisher_Integration_DryRunNoSideEffects_Good(t *testing.T) {
|
|||
|
||||
// --- Chocolatey Publisher Integration Tests ---
|
||||
|
||||
func TestChocolateyPublisher_Integration_DryRunNoSideEffects_Good(t *testing.T) {
|
||||
func TestIntegration_ChocolateyPublisherIntegrationDryRunNoSideEffects_Good(t *testing.T) {
|
||||
p := NewChocolateyPublisher()
|
||||
|
||||
t.Run("dry run generates nuspec and install script without side effects", func(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
|
||||
oldStdout := os.Stdout
|
||||
r, w, _ := os.Pipe()
|
||||
os.Stdout = w
|
||||
|
||||
release := &Release{
|
||||
Version: "v1.0.0",
|
||||
ProjectDir: tmpDir,
|
||||
|
|
@ -737,15 +653,11 @@ func TestChocolateyPublisher_Integration_DryRunNoSideEffects_Good(t *testing.T)
|
|||
}
|
||||
relCfg := &mockReleaseConfig{repository: "owner/my-cli-tool", projectName: "my-cli-tool"}
|
||||
|
||||
err := p.Publish(context.Background(), release, pubCfg, relCfg, true)
|
||||
|
||||
_ = w.Close()
|
||||
var buf bytes.Buffer
|
||||
_, _ = buf.ReadFrom(r)
|
||||
os.Stdout = oldStdout
|
||||
|
||||
var err error
|
||||
output := capturePublisherOutput(t, func() {
|
||||
err = p.Publish(context.Background(), release, pubCfg, relCfg, true)
|
||||
})
|
||||
require.NoError(t, err)
|
||||
output := buf.String()
|
||||
|
||||
assert.Contains(t, output, "DRY RUN: Chocolatey Publish")
|
||||
assert.Contains(t, output, "Package: my-cli-tool")
|
||||
|
|
@ -757,7 +669,7 @@ func TestChocolateyPublisher_Integration_DryRunNoSideEffects_Good(t *testing.T)
|
|||
assert.Contains(t, output, "Would generate package files only")
|
||||
|
||||
// Verify no files created
|
||||
entries, err := os.ReadDir(tmpDir)
|
||||
entries, err := ax.ReadDir(tmpDir)
|
||||
require.NoError(t, err)
|
||||
assert.Empty(t, entries)
|
||||
})
|
||||
|
|
@ -765,16 +677,12 @@ func TestChocolateyPublisher_Integration_DryRunNoSideEffects_Good(t *testing.T)
|
|||
|
||||
// --- npm Publisher Integration Tests ---
|
||||
|
||||
func TestNpmPublisher_Integration_DryRunNoSideEffects_Good(t *testing.T) {
|
||||
func TestIntegration_NpmPublisherIntegrationDryRunNoSideEffects_Good(t *testing.T) {
|
||||
p := NewNpmPublisher()
|
||||
|
||||
t.Run("dry run generates package.json without writing files or publishing", func(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
|
||||
oldStdout := os.Stdout
|
||||
r, w, _ := os.Pipe()
|
||||
os.Stdout = w
|
||||
|
||||
release := &Release{
|
||||
Version: "v3.0.0",
|
||||
ProjectDir: tmpDir,
|
||||
|
|
@ -789,15 +697,11 @@ func TestNpmPublisher_Integration_DryRunNoSideEffects_Good(t *testing.T) {
|
|||
}
|
||||
relCfg := &mockReleaseConfig{repository: "test-org/my-cli", projectName: "my-cli"}
|
||||
|
||||
err := p.Publish(context.Background(), release, pubCfg, relCfg, true)
|
||||
|
||||
_ = w.Close()
|
||||
var buf bytes.Buffer
|
||||
_, _ = buf.ReadFrom(r)
|
||||
os.Stdout = oldStdout
|
||||
|
||||
var err error
|
||||
output := capturePublisherOutput(t, func() {
|
||||
err = p.Publish(context.Background(), release, pubCfg, relCfg, true)
|
||||
})
|
||||
require.NoError(t, err)
|
||||
output := buf.String()
|
||||
|
||||
assert.Contains(t, output, "DRY RUN: npm Publish")
|
||||
assert.Contains(t, output, "Package: @test-org/my-cli")
|
||||
|
|
@ -809,7 +713,7 @@ func TestNpmPublisher_Integration_DryRunNoSideEffects_Good(t *testing.T) {
|
|||
assert.Contains(t, output, "Would run: npm publish --access public")
|
||||
|
||||
// Verify no files created
|
||||
entries, err := os.ReadDir(tmpDir)
|
||||
entries, err := ax.ReadDir(tmpDir)
|
||||
require.NoError(t, err)
|
||||
assert.Empty(t, entries)
|
||||
})
|
||||
|
|
@ -817,7 +721,7 @@ func TestNpmPublisher_Integration_DryRunNoSideEffects_Good(t *testing.T) {
|
|||
|
||||
// --- LinuxKit Publisher Integration Tests ---
|
||||
|
||||
func TestLinuxKitPublisher_Integration_DryRunNoSideEffects_Good(t *testing.T) {
|
||||
func TestIntegration_LinuxKitPublisherIntegrationDryRunNoSideEffects_Good(t *testing.T) {
|
||||
if err := validateLinuxKitCli(); err != nil {
|
||||
t.Skip("skipping: linuxkit CLI not available")
|
||||
}
|
||||
|
|
@ -828,13 +732,9 @@ func TestLinuxKitPublisher_Integration_DryRunNoSideEffects_Good(t *testing.T) {
|
|||
tmpDir := t.TempDir()
|
||||
|
||||
// Create config file
|
||||
configDir := filepath.Join(tmpDir, ".core", "linuxkit")
|
||||
require.NoError(t, os.MkdirAll(configDir, 0755))
|
||||
require.NoError(t, os.WriteFile(filepath.Join(configDir, "server.yml"), []byte("kernel:\n image: test\n"), 0644))
|
||||
|
||||
oldStdout := os.Stdout
|
||||
r, w, _ := os.Pipe()
|
||||
os.Stdout = w
|
||||
configDir := ax.Join(tmpDir, ".core", "linuxkit")
|
||||
require.NoError(t, ax.MkdirAll(configDir, 0o755))
|
||||
require.NoError(t, ax.WriteFile(ax.Join(configDir, "server.yml"), []byte("kernel:\n image: test\n"), 0o644))
|
||||
|
||||
release := &Release{
|
||||
Version: "v1.0.0",
|
||||
|
|
@ -850,15 +750,11 @@ func TestLinuxKitPublisher_Integration_DryRunNoSideEffects_Good(t *testing.T) {
|
|||
}
|
||||
relCfg := &mockReleaseConfig{repository: "test-org/my-os"}
|
||||
|
||||
err := p.Publish(context.Background(), release, pubCfg, relCfg, true)
|
||||
|
||||
_ = w.Close()
|
||||
var buf bytes.Buffer
|
||||
_, _ = buf.ReadFrom(r)
|
||||
os.Stdout = oldStdout
|
||||
|
||||
var err error
|
||||
output := capturePublisherOutput(t, func() {
|
||||
err = p.Publish(context.Background(), release, pubCfg, relCfg, true)
|
||||
})
|
||||
require.NoError(t, err)
|
||||
output := buf.String()
|
||||
|
||||
assert.Contains(t, output, "DRY RUN: LinuxKit Build & Publish")
|
||||
assert.Contains(t, output, "Formats: iso, qcow2, docker")
|
||||
|
|
@ -876,15 +772,14 @@ func TestLinuxKitPublisher_Integration_DryRunNoSideEffects_Good(t *testing.T) {
|
|||
assert.Contains(t, output, "docker load")
|
||||
|
||||
// Verify no files created in dist
|
||||
distDir := filepath.Join(tmpDir, "dist")
|
||||
_, err = os.Stat(distDir)
|
||||
assert.True(t, os.IsNotExist(err), "dry run should not create dist directory")
|
||||
distDir := ax.Join(tmpDir, "dist")
|
||||
assert.False(t, ax.Exists(distDir), "dry run should not create dist directory")
|
||||
})
|
||||
}
|
||||
|
||||
// --- Cross-Publisher Integration Tests ---
|
||||
|
||||
func TestAllPublishers_Integration_NameUniqueness_Good(t *testing.T) {
|
||||
func TestIntegration_AllPublishersIntegrationNameUniqueness_Good(t *testing.T) {
|
||||
t.Run("all publishers have unique names", func(t *testing.T) {
|
||||
publishers := []Publisher{
|
||||
NewGitHubPublisher(),
|
||||
|
|
@ -909,21 +804,12 @@ func TestAllPublishers_Integration_NameUniqueness_Good(t *testing.T) {
|
|||
})
|
||||
}
|
||||
|
||||
func TestAllPublishers_Integration_NilRelCfg_Good(t *testing.T) {
|
||||
func TestIntegration_AllPublishersIntegrationNilRelCfg_Good(t *testing.T) {
|
||||
t.Run("github handles nil relCfg with git repo", func(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
|
||||
cmd := exec.Command("git", "init")
|
||||
cmd.Dir = tmpDir
|
||||
require.NoError(t, cmd.Run())
|
||||
|
||||
cmd = exec.Command("git", "remote", "add", "origin", "git@github.com:niltest/repo.git")
|
||||
cmd.Dir = tmpDir
|
||||
require.NoError(t, cmd.Run())
|
||||
|
||||
oldStdout := os.Stdout
|
||||
r, w, _ := os.Pipe()
|
||||
os.Stdout = w
|
||||
runPublisherCommand(t, tmpDir, "git", "init")
|
||||
runPublisherCommand(t, tmpDir, "git", "remote", "add", "origin", "git@github.com:niltest/repo.git")
|
||||
|
||||
release := &Release{
|
||||
Version: "v1.0.0",
|
||||
|
|
@ -933,19 +819,16 @@ func TestAllPublishers_Integration_NilRelCfg_Good(t *testing.T) {
|
|||
}
|
||||
pubCfg := PublisherConfig{Type: "github"}
|
||||
|
||||
err := NewGitHubPublisher().Publish(context.Background(), release, pubCfg, nil, true)
|
||||
|
||||
_ = w.Close()
|
||||
var buf bytes.Buffer
|
||||
_, _ = buf.ReadFrom(r)
|
||||
os.Stdout = oldStdout
|
||||
|
||||
var err error
|
||||
output := capturePublisherOutput(t, func() {
|
||||
err = NewGitHubPublisher().Publish(context.Background(), release, pubCfg, nil, true)
|
||||
})
|
||||
require.NoError(t, err)
|
||||
assert.Contains(t, buf.String(), "niltest/repo")
|
||||
assert.Contains(t, output, "niltest/repo")
|
||||
})
|
||||
}
|
||||
|
||||
func TestBuildChecksumMap_Integration_Good(t *testing.T) {
|
||||
func TestIntegration_BuildChecksumMapIntegration_Good(t *testing.T) {
|
||||
t.Run("maps all platforms correctly from realistic artifacts", func(t *testing.T) {
|
||||
artifacts := []build.Artifact{
|
||||
{Path: "/dist/core-v1.0.0-darwin-amd64.tar.gz", Checksum: "da64"},
|
||||
|
|
|
|||
|
|
@ -3,16 +3,14 @@ package publishers
|
|||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"dappco.re/go/core"
|
||||
"dappco.re/go/core/build/internal/ax"
|
||||
coreerr "dappco.re/go/core/log"
|
||||
)
|
||||
|
||||
// LinuxKitConfig holds configuration for the LinuxKit publisher.
|
||||
// Usage example: declare a value of type publishers.LinuxKitConfig in integrating code.
|
||||
type LinuxKitConfig struct {
|
||||
// Config is the path to the LinuxKit YAML configuration file.
|
||||
Config string `yaml:"config"`
|
||||
|
|
@ -26,19 +24,23 @@ type LinuxKitConfig struct {
|
|||
}
|
||||
|
||||
// LinuxKitPublisher builds and publishes LinuxKit images.
|
||||
// Usage example: declare a value of type publishers.LinuxKitPublisher in integrating code.
|
||||
type LinuxKitPublisher struct{}
|
||||
|
||||
// NewLinuxKitPublisher creates a new LinuxKit publisher.
|
||||
// Usage example: call publishers.NewLinuxKitPublisher(...) from integrating code.
|
||||
func NewLinuxKitPublisher() *LinuxKitPublisher {
|
||||
return &LinuxKitPublisher{}
|
||||
}
|
||||
|
||||
// Name returns the publisher's identifier.
|
||||
// Usage example: call value.Name(...) from integrating code.
|
||||
func (p *LinuxKitPublisher) Name() string {
|
||||
return "linuxkit"
|
||||
}
|
||||
|
||||
// Publish builds LinuxKit images and uploads them to the GitHub release.
|
||||
// Usage example: call value.Publish(...) from integrating code.
|
||||
func (p *LinuxKitPublisher) Publish(ctx context.Context, release *Release, pubCfg PublisherConfig, relCfg ReleaseConfig, dryRun bool) error {
|
||||
// Validate linuxkit CLI is available
|
||||
if err := validateLinuxKitCli(); err != nil {
|
||||
|
|
@ -79,7 +81,7 @@ func (p *LinuxKitPublisher) Publish(ctx context.Context, release *Release, pubCf
|
|||
// parseConfig extracts LinuxKit-specific configuration.
|
||||
func (p *LinuxKitPublisher) parseConfig(pubCfg PublisherConfig, projectDir string) LinuxKitConfig {
|
||||
cfg := LinuxKitConfig{
|
||||
Config: filepath.Join(projectDir, ".core", "linuxkit", "server.yml"),
|
||||
Config: ax.Join(projectDir, ".core", "linuxkit", "server.yml"),
|
||||
Formats: []string{"iso"},
|
||||
Platforms: []string{"linux/amd64"},
|
||||
}
|
||||
|
|
@ -87,10 +89,10 @@ func (p *LinuxKitPublisher) parseConfig(pubCfg PublisherConfig, projectDir strin
|
|||
// Override from extended config if present
|
||||
if ext, ok := pubCfg.Extended.(map[string]any); ok {
|
||||
if configPath, ok := ext["config"].(string); ok && configPath != "" {
|
||||
if filepath.IsAbs(configPath) {
|
||||
if ax.IsAbs(configPath) {
|
||||
cfg.Config = configPath
|
||||
} else {
|
||||
cfg.Config = filepath.Join(projectDir, configPath)
|
||||
cfg.Config = ax.Join(projectDir, configPath)
|
||||
}
|
||||
}
|
||||
if formats, ok := ext["formats"].([]any); ok && len(formats) > 0 {
|
||||
|
|
@ -116,62 +118,62 @@ func (p *LinuxKitPublisher) parseConfig(pubCfg PublisherConfig, projectDir strin
|
|||
|
||||
// dryRunPublish shows what would be done without actually building.
|
||||
func (p *LinuxKitPublisher) dryRunPublish(release *Release, cfg LinuxKitConfig, repo string) error {
|
||||
fmt.Println()
|
||||
fmt.Println("=== DRY RUN: LinuxKit Build & Publish ===")
|
||||
fmt.Println()
|
||||
fmt.Printf("Repository: %s\n", repo)
|
||||
fmt.Printf("Version: %s\n", release.Version)
|
||||
fmt.Printf("Config: %s\n", cfg.Config)
|
||||
fmt.Printf("Formats: %s\n", strings.Join(cfg.Formats, ", "))
|
||||
fmt.Printf("Platforms: %s\n", strings.Join(cfg.Platforms, ", "))
|
||||
fmt.Println()
|
||||
publisherPrintln()
|
||||
publisherPrintln("=== DRY RUN: LinuxKit Build & Publish ===")
|
||||
publisherPrintln()
|
||||
publisherPrint("Repository: %s", repo)
|
||||
publisherPrint("Version: %s", release.Version)
|
||||
publisherPrint("Config: %s", cfg.Config)
|
||||
publisherPrint("Formats: %s", core.Join(", ", cfg.Formats...))
|
||||
publisherPrint("Platforms: %s", core.Join(", ", cfg.Platforms...))
|
||||
publisherPrintln()
|
||||
|
||||
outputDir := filepath.Join(release.ProjectDir, "dist", "linuxkit")
|
||||
outputDir := ax.Join(release.ProjectDir, "dist", "linuxkit")
|
||||
baseName := p.buildBaseName(release.Version)
|
||||
|
||||
fmt.Println("Would execute commands:")
|
||||
publisherPrintln("Would execute commands:")
|
||||
for _, platform := range cfg.Platforms {
|
||||
parts := strings.Split(platform, "/")
|
||||
parts := core.Split(platform, "/")
|
||||
arch := "amd64"
|
||||
if len(parts) == 2 {
|
||||
arch = parts[1]
|
||||
}
|
||||
|
||||
for _, format := range cfg.Formats {
|
||||
outputName := fmt.Sprintf("%s-%s", baseName, arch)
|
||||
outputName := core.Sprintf("%s-%s", baseName, arch)
|
||||
args := p.buildLinuxKitArgs(cfg.Config, format, outputName, outputDir, arch)
|
||||
fmt.Printf(" linuxkit %s\n", strings.Join(args, " "))
|
||||
publisherPrint(" linuxkit %s", core.Join(" ", args...))
|
||||
}
|
||||
}
|
||||
fmt.Println()
|
||||
publisherPrintln()
|
||||
|
||||
fmt.Println("Would upload artifacts to release:")
|
||||
publisherPrintln("Would upload artifacts to release:")
|
||||
for _, platform := range cfg.Platforms {
|
||||
parts := strings.Split(platform, "/")
|
||||
parts := core.Split(platform, "/")
|
||||
arch := "amd64"
|
||||
if len(parts) == 2 {
|
||||
arch = parts[1]
|
||||
}
|
||||
|
||||
for _, format := range cfg.Formats {
|
||||
outputName := fmt.Sprintf("%s-%s", baseName, arch)
|
||||
outputName := core.Sprintf("%s-%s", baseName, arch)
|
||||
artifactPath := p.getArtifactPath(outputDir, outputName, format)
|
||||
fmt.Printf(" - %s\n", filepath.Base(artifactPath))
|
||||
publisherPrint(" - %s", ax.Base(artifactPath))
|
||||
if format == "docker" {
|
||||
fmt.Printf(" Usage: docker load < %s\n", filepath.Base(artifactPath))
|
||||
publisherPrint(" Usage: docker load < %s", ax.Base(artifactPath))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fmt.Println()
|
||||
fmt.Println("=== END DRY RUN ===")
|
||||
publisherPrintln()
|
||||
publisherPrintln("=== END DRY RUN ===")
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// executePublish builds LinuxKit images and uploads them.
|
||||
func (p *LinuxKitPublisher) executePublish(ctx context.Context, release *Release, cfg LinuxKitConfig, repo string) error {
|
||||
outputDir := filepath.Join(release.ProjectDir, "dist", "linuxkit")
|
||||
outputDir := ax.Join(release.ProjectDir, "dist", "linuxkit")
|
||||
|
||||
// Create output directory
|
||||
if err := release.FS.EnsureDir(outputDir); err != nil {
|
||||
|
|
@ -183,24 +185,19 @@ func (p *LinuxKitPublisher) executePublish(ctx context.Context, release *Release
|
|||
|
||||
// Build for each platform and format
|
||||
for _, platform := range cfg.Platforms {
|
||||
parts := strings.Split(platform, "/")
|
||||
parts := core.Split(platform, "/")
|
||||
arch := "amd64"
|
||||
if len(parts) == 2 {
|
||||
arch = parts[1]
|
||||
}
|
||||
|
||||
for _, format := range cfg.Formats {
|
||||
outputName := fmt.Sprintf("%s-%s", baseName, arch)
|
||||
outputName := core.Sprintf("%s-%s", baseName, arch)
|
||||
|
||||
// Build the image
|
||||
args := p.buildLinuxKitArgs(cfg.Config, format, outputName, outputDir, arch)
|
||||
cmd := exec.CommandContext(ctx, "linuxkit", args...)
|
||||
cmd.Dir = release.ProjectDir
|
||||
cmd.Stdout = os.Stdout
|
||||
cmd.Stderr = os.Stderr
|
||||
|
||||
fmt.Printf("Building LinuxKit image: %s (%s)\n", outputName, format)
|
||||
if err := cmd.Run(); err != nil {
|
||||
publisherPrint("Building LinuxKit image: %s (%s)", outputName, format)
|
||||
if err := publisherRun(ctx, release.ProjectDir, nil, "linuxkit", args...); err != nil {
|
||||
return coreerr.E("linuxkit.Publish", "build failed for "+platform+"/"+format, err)
|
||||
}
|
||||
|
||||
|
|
@ -217,12 +214,12 @@ func (p *LinuxKitPublisher) executePublish(ctx context.Context, release *Release
|
|||
}
|
||||
|
||||
if err := UploadArtifact(ctx, repo, release.Version, artifactPath); err != nil {
|
||||
return coreerr.E("linuxkit.Publish", "failed to upload "+filepath.Base(artifactPath), err)
|
||||
return coreerr.E("linuxkit.Publish", "failed to upload "+ax.Base(artifactPath), err)
|
||||
}
|
||||
|
||||
// Print helpful usage info for docker format
|
||||
if strings.HasSuffix(artifactPath, ".docker.tar") {
|
||||
fmt.Printf(" Load with: docker load < %s\n", filepath.Base(artifactPath))
|
||||
if core.HasSuffix(artifactPath, ".docker.tar") {
|
||||
publisherPrint(" Load with: docker load < %s", ax.Base(artifactPath))
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -232,8 +229,8 @@ func (p *LinuxKitPublisher) executePublish(ctx context.Context, release *Release
|
|||
// buildBaseName creates the base name for output files.
|
||||
func (p *LinuxKitPublisher) buildBaseName(version string) string {
|
||||
// Strip leading 'v' if present for cleaner filenames
|
||||
name := strings.TrimPrefix(version, "v")
|
||||
return fmt.Sprintf("linuxkit-%s", name)
|
||||
name := core.TrimPrefix(version, "v")
|
||||
return core.Sprintf("linuxkit-%s", name)
|
||||
}
|
||||
|
||||
// buildLinuxKitArgs builds the arguments for linuxkit build command.
|
||||
|
|
@ -263,7 +260,7 @@ func (p *LinuxKitPublisher) buildLinuxKitArgs(configPath, format, outputName, ou
|
|||
// getArtifactPath returns the expected path of the built artifact.
|
||||
func (p *LinuxKitPublisher) getArtifactPath(outputDir, outputName, format string) string {
|
||||
ext := p.getFormatExtension(format)
|
||||
return filepath.Join(outputDir, outputName+ext)
|
||||
return ax.Join(outputDir, outputName+ext)
|
||||
}
|
||||
|
||||
// getFormatExtension returns the file extension for a LinuxKit output format.
|
||||
|
|
@ -297,9 +294,8 @@ func (p *LinuxKitPublisher) getFormatExtension(format string) string {
|
|||
|
||||
// validateLinuxKitCli checks if the linuxkit CLI is available.
|
||||
func validateLinuxKitCli() error {
|
||||
cmd := exec.Command("linuxkit", "version")
|
||||
if err := cmd.Run(); err != nil {
|
||||
return coreerr.E("linuxkit.validateLinuxKitCli", "linuxkit CLI not found. Install it from https://github.com/linuxkit/linuxkit", nil)
|
||||
if _, err := ax.LookPath("linuxkit"); err != nil {
|
||||
return coreerr.E("linuxkit.validateLinuxKitCli", "linuxkit CLI not found. Install it from https://github.com/linuxkit/linuxkit", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,26 +1,23 @@
|
|||
package publishers
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"dappco.re/go/core/build/internal/ax"
|
||||
"dappco.re/go/core/io"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestLinuxKitPublisher_Name_Good(t *testing.T) {
|
||||
func TestLinuxKit_LinuxKitPublisherName_Good(t *testing.T) {
|
||||
t.Run("returns linuxkit", func(t *testing.T) {
|
||||
p := NewLinuxKitPublisher()
|
||||
assert.Equal(t, "linuxkit", p.Name())
|
||||
})
|
||||
}
|
||||
|
||||
func TestLinuxKitPublisher_ParseConfig_Good(t *testing.T) {
|
||||
func TestLinuxKit_LinuxKitPublisherParseConfig_Good(t *testing.T) {
|
||||
p := NewLinuxKitPublisher()
|
||||
|
||||
t.Run("uses defaults when no extended config", func(t *testing.T) {
|
||||
|
|
@ -61,7 +58,7 @@ func TestLinuxKitPublisher_ParseConfig_Good(t *testing.T) {
|
|||
})
|
||||
}
|
||||
|
||||
func TestLinuxKitPublisher_BuildLinuxKitArgs_Good(t *testing.T) {
|
||||
func TestLinuxKit_LinuxKitPublisherBuildLinuxKitArgs_Good(t *testing.T) {
|
||||
p := NewLinuxKitPublisher()
|
||||
|
||||
t.Run("builds basic args for amd64", func(t *testing.T) {
|
||||
|
|
@ -88,7 +85,7 @@ func TestLinuxKitPublisher_BuildLinuxKitArgs_Good(t *testing.T) {
|
|||
})
|
||||
}
|
||||
|
||||
func TestLinuxKitPublisher_BuildBaseName_Good(t *testing.T) {
|
||||
func TestLinuxKit_LinuxKitPublisherBuildBaseName_Good(t *testing.T) {
|
||||
p := NewLinuxKitPublisher()
|
||||
|
||||
t.Run("strips v prefix", func(t *testing.T) {
|
||||
|
|
@ -102,7 +99,7 @@ func TestLinuxKitPublisher_BuildBaseName_Good(t *testing.T) {
|
|||
})
|
||||
}
|
||||
|
||||
func TestLinuxKitPublisher_GetArtifactPath_Good(t *testing.T) {
|
||||
func TestLinuxKit_LinuxKitPublisherGetArtifactPath_Good(t *testing.T) {
|
||||
p := NewLinuxKitPublisher()
|
||||
|
||||
tests := []struct {
|
||||
|
|
@ -157,7 +154,7 @@ func TestLinuxKitPublisher_GetArtifactPath_Good(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestLinuxKitPublisher_GetFormatExtension_Good(t *testing.T) {
|
||||
func TestLinuxKit_LinuxKitPublisherGetFormatExtension_Good(t *testing.T) {
|
||||
p := NewLinuxKitPublisher()
|
||||
|
||||
tests := []struct {
|
||||
|
|
@ -182,7 +179,7 @@ func TestLinuxKitPublisher_GetFormatExtension_Good(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestLinuxKitPublisher_Publish_Bad(t *testing.T) {
|
||||
func TestLinuxKit_LinuxKitPublisherPublish_Bad(t *testing.T) {
|
||||
p := NewLinuxKitPublisher()
|
||||
|
||||
t.Run("fails when config file not found with linuxkit installed", func(t *testing.T) {
|
||||
|
|
@ -232,13 +229,11 @@ func TestLinuxKitPublisher_Publish_Bad(t *testing.T) {
|
|||
}
|
||||
|
||||
// Create temp directory that is NOT a git repo
|
||||
tmpDir, err := os.MkdirTemp("", "linuxkit-test")
|
||||
require.NoError(t, err)
|
||||
defer func() { _ = os.RemoveAll(tmpDir) }()
|
||||
tmpDir := t.TempDir()
|
||||
|
||||
// Create a config file
|
||||
configPath := filepath.Join(tmpDir, "config.yml")
|
||||
err = os.WriteFile(configPath, []byte("kernel:\n image: test\n"), 0644)
|
||||
configPath := ax.Join(tmpDir, "config.yml")
|
||||
err := ax.WriteFile(configPath, []byte("kernel:\n image: test\n"), 0o644)
|
||||
require.NoError(t, err)
|
||||
|
||||
release := &Release{
|
||||
|
|
@ -260,7 +255,7 @@ func TestLinuxKitPublisher_Publish_Bad(t *testing.T) {
|
|||
})
|
||||
}
|
||||
|
||||
func TestValidateLinuxKitCli_Good(t *testing.T) {
|
||||
func TestLinuxKit_ValidateLinuxKitCli_Good(t *testing.T) {
|
||||
t.Run("returns expected error when linuxkit not installed", func(t *testing.T) {
|
||||
err := validateLinuxKitCli()
|
||||
if err != nil {
|
||||
|
|
@ -271,7 +266,7 @@ func TestValidateLinuxKitCli_Good(t *testing.T) {
|
|||
})
|
||||
}
|
||||
|
||||
func TestLinuxKitPublisher_Publish_WithCLI_Good(t *testing.T) {
|
||||
func TestLinuxKit_LinuxKitPublisherPublishWithCLI_Good(t *testing.T) {
|
||||
// These tests run only when linuxkit CLI is available
|
||||
if err := validateLinuxKitCli(); err != nil {
|
||||
t.Skip("skipping test: linuxkit CLI not available")
|
||||
|
|
@ -280,23 +275,17 @@ func TestLinuxKitPublisher_Publish_WithCLI_Good(t *testing.T) {
|
|||
p := NewLinuxKitPublisher()
|
||||
|
||||
t.Run("succeeds with dry run and valid config", func(t *testing.T) {
|
||||
tmpDir, err := os.MkdirTemp("", "linuxkit-test")
|
||||
require.NoError(t, err)
|
||||
defer func() { _ = os.RemoveAll(tmpDir) }()
|
||||
tmpDir := t.TempDir()
|
||||
|
||||
// Create config directory and file
|
||||
configDir := filepath.Join(tmpDir, ".core", "linuxkit")
|
||||
err = os.MkdirAll(configDir, 0755)
|
||||
configDir := ax.Join(tmpDir, ".core", "linuxkit")
|
||||
err := ax.MkdirAll(configDir, 0o755)
|
||||
require.NoError(t, err)
|
||||
|
||||
configPath := filepath.Join(configDir, "server.yml")
|
||||
err = os.WriteFile(configPath, []byte("kernel:\n image: linuxkit/kernel:5.10\n"), 0644)
|
||||
configPath := ax.Join(configDir, "server.yml")
|
||||
err = ax.WriteFile(configPath, []byte("kernel:\n image: linuxkit/kernel:5.10\n"), 0o644)
|
||||
require.NoError(t, err)
|
||||
|
||||
oldStdout := os.Stdout
|
||||
r, w, _ := os.Pipe()
|
||||
os.Stdout = w
|
||||
|
||||
release := &Release{
|
||||
Version: "v1.0.0",
|
||||
ProjectDir: tmpDir,
|
||||
|
|
@ -305,22 +294,15 @@ func TestLinuxKitPublisher_Publish_WithCLI_Good(t *testing.T) {
|
|||
pubCfg := PublisherConfig{Type: "linuxkit"}
|
||||
relCfg := &mockReleaseConfig{repository: "owner/repo"}
|
||||
|
||||
err = p.Publish(context.TODO(), release, pubCfg, relCfg, true)
|
||||
|
||||
_ = w.Close()
|
||||
var buf bytes.Buffer
|
||||
_, _ = buf.ReadFrom(r)
|
||||
os.Stdout = oldStdout
|
||||
|
||||
output := capturePublisherOutput(t, func() {
|
||||
err = p.Publish(context.TODO(), release, pubCfg, relCfg, true)
|
||||
})
|
||||
require.NoError(t, err)
|
||||
output := buf.String()
|
||||
assert.Contains(t, output, "DRY RUN: LinuxKit Build & Publish")
|
||||
})
|
||||
|
||||
t.Run("fails with missing config file", func(t *testing.T) {
|
||||
tmpDir, err := os.MkdirTemp("", "linuxkit-test")
|
||||
require.NoError(t, err)
|
||||
defer func() { _ = os.RemoveAll(tmpDir) }()
|
||||
tmpDir := t.TempDir()
|
||||
|
||||
release := &Release{
|
||||
Version: "v1.0.0",
|
||||
|
|
@ -330,28 +312,22 @@ func TestLinuxKitPublisher_Publish_WithCLI_Good(t *testing.T) {
|
|||
pubCfg := PublisherConfig{Type: "linuxkit"}
|
||||
relCfg := &mockReleaseConfig{repository: "owner/repo"}
|
||||
|
||||
err = p.Publish(context.TODO(), release, pubCfg, relCfg, false)
|
||||
err := p.Publish(context.TODO(), release, pubCfg, relCfg, false)
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "config file not found")
|
||||
})
|
||||
|
||||
t.Run("uses relCfg repository", func(t *testing.T) {
|
||||
tmpDir, err := os.MkdirTemp("", "linuxkit-test")
|
||||
require.NoError(t, err)
|
||||
defer func() { _ = os.RemoveAll(tmpDir) }()
|
||||
tmpDir := t.TempDir()
|
||||
|
||||
configDir := filepath.Join(tmpDir, ".core", "linuxkit")
|
||||
err = os.MkdirAll(configDir, 0755)
|
||||
configDir := ax.Join(tmpDir, ".core", "linuxkit")
|
||||
err := ax.MkdirAll(configDir, 0o755)
|
||||
require.NoError(t, err)
|
||||
|
||||
configPath := filepath.Join(configDir, "server.yml")
|
||||
err = os.WriteFile(configPath, []byte("kernel:\n image: test\n"), 0644)
|
||||
configPath := ax.Join(configDir, "server.yml")
|
||||
err = ax.WriteFile(configPath, []byte("kernel:\n image: test\n"), 0o644)
|
||||
require.NoError(t, err)
|
||||
|
||||
oldStdout := os.Stdout
|
||||
r, w, _ := os.Pipe()
|
||||
os.Stdout = w
|
||||
|
||||
release := &Release{
|
||||
Version: "v1.0.0",
|
||||
ProjectDir: tmpDir,
|
||||
|
|
@ -360,44 +336,28 @@ func TestLinuxKitPublisher_Publish_WithCLI_Good(t *testing.T) {
|
|||
pubCfg := PublisherConfig{Type: "linuxkit"}
|
||||
relCfg := &mockReleaseConfig{repository: "custom-owner/custom-repo"}
|
||||
|
||||
err = p.Publish(context.TODO(), release, pubCfg, relCfg, true)
|
||||
|
||||
_ = w.Close()
|
||||
var buf bytes.Buffer
|
||||
_, _ = buf.ReadFrom(r)
|
||||
os.Stdout = oldStdout
|
||||
|
||||
output := capturePublisherOutput(t, func() {
|
||||
err = p.Publish(context.TODO(), release, pubCfg, relCfg, true)
|
||||
})
|
||||
require.NoError(t, err)
|
||||
output := buf.String()
|
||||
assert.Contains(t, output, "custom-owner/custom-repo")
|
||||
})
|
||||
|
||||
t.Run("detects repository when not provided", func(t *testing.T) {
|
||||
tmpDir, err := os.MkdirTemp("", "linuxkit-test")
|
||||
require.NoError(t, err)
|
||||
defer func() { _ = os.RemoveAll(tmpDir) }()
|
||||
tmpDir := t.TempDir()
|
||||
|
||||
// Create config file
|
||||
configDir := filepath.Join(tmpDir, ".core", "linuxkit")
|
||||
err = os.MkdirAll(configDir, 0755)
|
||||
configDir := ax.Join(tmpDir, ".core", "linuxkit")
|
||||
err := ax.MkdirAll(configDir, 0o755)
|
||||
require.NoError(t, err)
|
||||
|
||||
configPath := filepath.Join(configDir, "server.yml")
|
||||
err = os.WriteFile(configPath, []byte("kernel:\n image: test\n"), 0644)
|
||||
configPath := ax.Join(configDir, "server.yml")
|
||||
err = ax.WriteFile(configPath, []byte("kernel:\n image: test\n"), 0o644)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Initialize git repo
|
||||
cmd := exec.Command("git", "init")
|
||||
cmd.Dir = tmpDir
|
||||
require.NoError(t, cmd.Run())
|
||||
|
||||
cmd = exec.Command("git", "remote", "add", "origin", "git@github.com:detected-owner/detected-repo.git")
|
||||
cmd.Dir = tmpDir
|
||||
require.NoError(t, cmd.Run())
|
||||
|
||||
oldStdout := os.Stdout
|
||||
r, w, _ := os.Pipe()
|
||||
os.Stdout = w
|
||||
runPublisherCommand(t, tmpDir, "git", "init")
|
||||
runPublisherCommand(t, tmpDir, "git", "remote", "add", "origin", "git@github.com:detected-owner/detected-repo.git")
|
||||
|
||||
release := &Release{
|
||||
Version: "v1.0.0",
|
||||
|
|
@ -407,20 +367,15 @@ func TestLinuxKitPublisher_Publish_WithCLI_Good(t *testing.T) {
|
|||
pubCfg := PublisherConfig{Type: "linuxkit"}
|
||||
relCfg := &mockReleaseConfig{repository: ""} // Empty to trigger detection
|
||||
|
||||
err = p.Publish(context.TODO(), release, pubCfg, relCfg, true)
|
||||
|
||||
_ = w.Close()
|
||||
var buf bytes.Buffer
|
||||
_, _ = buf.ReadFrom(r)
|
||||
os.Stdout = oldStdout
|
||||
|
||||
output := capturePublisherOutput(t, func() {
|
||||
err = p.Publish(context.TODO(), release, pubCfg, relCfg, true)
|
||||
})
|
||||
require.NoError(t, err)
|
||||
output := buf.String()
|
||||
assert.Contains(t, output, "detected-owner/detected-repo")
|
||||
})
|
||||
}
|
||||
|
||||
func TestLinuxKitPublisher_Publish_NilRelCfg_Good(t *testing.T) {
|
||||
func TestLinuxKit_LinuxKitPublisherPublishNilRelCfg_Good(t *testing.T) {
|
||||
if err := validateLinuxKitCli(); err != nil {
|
||||
t.Skip("skipping test: linuxkit CLI not available")
|
||||
}
|
||||
|
|
@ -428,31 +383,20 @@ func TestLinuxKitPublisher_Publish_NilRelCfg_Good(t *testing.T) {
|
|||
p := NewLinuxKitPublisher()
|
||||
|
||||
t.Run("handles nil relCfg by detecting repo", func(t *testing.T) {
|
||||
tmpDir, err := os.MkdirTemp("", "linuxkit-test")
|
||||
require.NoError(t, err)
|
||||
defer func() { _ = os.RemoveAll(tmpDir) }()
|
||||
tmpDir := t.TempDir()
|
||||
|
||||
// Create config file
|
||||
configDir := filepath.Join(tmpDir, ".core", "linuxkit")
|
||||
err = os.MkdirAll(configDir, 0755)
|
||||
configDir := ax.Join(tmpDir, ".core", "linuxkit")
|
||||
err := ax.MkdirAll(configDir, 0o755)
|
||||
require.NoError(t, err)
|
||||
|
||||
configPath := filepath.Join(configDir, "server.yml")
|
||||
err = os.WriteFile(configPath, []byte("kernel:\n image: test\n"), 0644)
|
||||
configPath := ax.Join(configDir, "server.yml")
|
||||
err = ax.WriteFile(configPath, []byte("kernel:\n image: test\n"), 0o644)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Initialize git repo
|
||||
cmd := exec.Command("git", "init")
|
||||
cmd.Dir = tmpDir
|
||||
require.NoError(t, cmd.Run())
|
||||
|
||||
cmd = exec.Command("git", "remote", "add", "origin", "git@github.com:nil-owner/nil-repo.git")
|
||||
cmd.Dir = tmpDir
|
||||
require.NoError(t, cmd.Run())
|
||||
|
||||
oldStdout := os.Stdout
|
||||
r, w, _ := os.Pipe()
|
||||
os.Stdout = w
|
||||
runPublisherCommand(t, tmpDir, "git", "init")
|
||||
runPublisherCommand(t, tmpDir, "git", "remote", "add", "origin", "git@github.com:nil-owner/nil-repo.git")
|
||||
|
||||
release := &Release{
|
||||
Version: "v1.0.0",
|
||||
|
|
@ -461,15 +405,10 @@ func TestLinuxKitPublisher_Publish_NilRelCfg_Good(t *testing.T) {
|
|||
}
|
||||
pubCfg := PublisherConfig{Type: "linuxkit"}
|
||||
|
||||
err = p.Publish(context.TODO(), release, pubCfg, nil, true) // nil relCfg
|
||||
|
||||
_ = w.Close()
|
||||
var buf bytes.Buffer
|
||||
_, _ = buf.ReadFrom(r)
|
||||
os.Stdout = oldStdout
|
||||
|
||||
output := capturePublisherOutput(t, func() {
|
||||
err = p.Publish(context.TODO(), release, pubCfg, nil, true)
|
||||
})
|
||||
require.NoError(t, err)
|
||||
output := buf.String()
|
||||
assert.Contains(t, output, "nil-owner/nil-repo")
|
||||
})
|
||||
}
|
||||
|
|
@ -488,14 +427,10 @@ func (m *mockReleaseConfig) GetProjectName() string {
|
|||
return m.projectName
|
||||
}
|
||||
|
||||
func TestLinuxKitPublisher_DryRunPublish_Good(t *testing.T) {
|
||||
func TestLinuxKit_LinuxKitPublisherDryRunPublish_Good(t *testing.T) {
|
||||
p := NewLinuxKitPublisher()
|
||||
|
||||
t.Run("outputs expected dry run information", func(t *testing.T) {
|
||||
oldStdout := os.Stdout
|
||||
r, w, _ := os.Pipe()
|
||||
os.Stdout = w
|
||||
|
||||
release := &Release{
|
||||
Version: "v1.0.0",
|
||||
ProjectDir: "/project",
|
||||
|
|
@ -507,15 +442,11 @@ func TestLinuxKitPublisher_DryRunPublish_Good(t *testing.T) {
|
|||
Platforms: []string{"linux/amd64", "linux/arm64"},
|
||||
}
|
||||
|
||||
err := p.dryRunPublish(release, cfg, "owner/repo")
|
||||
|
||||
_ = w.Close()
|
||||
var buf bytes.Buffer
|
||||
_, _ = buf.ReadFrom(r)
|
||||
os.Stdout = oldStdout
|
||||
|
||||
var err error
|
||||
output := capturePublisherOutput(t, func() {
|
||||
err = p.dryRunPublish(release, cfg, "owner/repo")
|
||||
})
|
||||
require.NoError(t, err)
|
||||
output := buf.String()
|
||||
|
||||
assert.Contains(t, output, "DRY RUN: LinuxKit Build & Publish")
|
||||
assert.Contains(t, output, "Repository: owner/repo")
|
||||
|
|
@ -534,10 +465,6 @@ func TestLinuxKitPublisher_DryRunPublish_Good(t *testing.T) {
|
|||
})
|
||||
|
||||
t.Run("shows docker format usage hint", func(t *testing.T) {
|
||||
oldStdout := os.Stdout
|
||||
r, w, _ := os.Pipe()
|
||||
os.Stdout = w
|
||||
|
||||
release := &Release{
|
||||
Version: "v1.0.0",
|
||||
ProjectDir: "/project",
|
||||
|
|
@ -549,25 +476,17 @@ func TestLinuxKitPublisher_DryRunPublish_Good(t *testing.T) {
|
|||
Platforms: []string{"linux/amd64"},
|
||||
}
|
||||
|
||||
err := p.dryRunPublish(release, cfg, "owner/repo")
|
||||
|
||||
_ = w.Close()
|
||||
var buf bytes.Buffer
|
||||
_, _ = buf.ReadFrom(r)
|
||||
os.Stdout = oldStdout
|
||||
|
||||
var err error
|
||||
output := capturePublisherOutput(t, func() {
|
||||
err = p.dryRunPublish(release, cfg, "owner/repo")
|
||||
})
|
||||
require.NoError(t, err)
|
||||
output := buf.String()
|
||||
|
||||
assert.Contains(t, output, "linuxkit-1.0.0-amd64.docker.tar")
|
||||
assert.Contains(t, output, "Usage: docker load <")
|
||||
})
|
||||
|
||||
t.Run("handles single platform and format", func(t *testing.T) {
|
||||
oldStdout := os.Stdout
|
||||
r, w, _ := os.Pipe()
|
||||
os.Stdout = w
|
||||
|
||||
release := &Release{
|
||||
Version: "v2.0.0",
|
||||
ProjectDir: "/project",
|
||||
|
|
@ -579,22 +498,18 @@ func TestLinuxKitPublisher_DryRunPublish_Good(t *testing.T) {
|
|||
Platforms: []string{"linux/amd64"},
|
||||
}
|
||||
|
||||
err := p.dryRunPublish(release, cfg, "owner/repo")
|
||||
|
||||
_ = w.Close()
|
||||
var buf bytes.Buffer
|
||||
_, _ = buf.ReadFrom(r)
|
||||
os.Stdout = oldStdout
|
||||
|
||||
var err error
|
||||
output := capturePublisherOutput(t, func() {
|
||||
err = p.dryRunPublish(release, cfg, "owner/repo")
|
||||
})
|
||||
require.NoError(t, err)
|
||||
output := buf.String()
|
||||
|
||||
assert.Contains(t, output, "linuxkit-2.0.0-amd64.iso")
|
||||
assert.NotContains(t, output, "arm64")
|
||||
})
|
||||
}
|
||||
|
||||
func TestLinuxKitPublisher_GetFormatExtension_AllFormats_Good(t *testing.T) {
|
||||
func TestLinuxKit_LinuxKitPublisherGetFormatExtensionAllFormats_Good(t *testing.T) {
|
||||
p := NewLinuxKitPublisher()
|
||||
|
||||
tests := []struct {
|
||||
|
|
@ -628,7 +543,7 @@ func TestLinuxKitPublisher_GetFormatExtension_AllFormats_Good(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestLinuxKitPublisher_BuildLinuxKitArgs_AllArchitectures_Good(t *testing.T) {
|
||||
func TestLinuxKit_LinuxKitPublisherBuildLinuxKitArgsAllArchitectures_Good(t *testing.T) {
|
||||
p := NewLinuxKitPublisher()
|
||||
|
||||
t.Run("amd64 does not include arch flag", func(t *testing.T) {
|
||||
|
|
@ -660,7 +575,7 @@ func TestLinuxKitPublisher_BuildLinuxKitArgs_AllArchitectures_Good(t *testing.T)
|
|||
})
|
||||
}
|
||||
|
||||
func TestLinuxKitPublisher_ParseConfig_EdgeCases_Good(t *testing.T) {
|
||||
func TestLinuxKit_LinuxKitPublisherParseConfigEdgeCases_Good(t *testing.T) {
|
||||
p := NewLinuxKitPublisher()
|
||||
|
||||
t.Run("handles nil extended config", func(t *testing.T) {
|
||||
|
|
@ -716,7 +631,7 @@ func TestLinuxKitPublisher_ParseConfig_EdgeCases_Good(t *testing.T) {
|
|||
})
|
||||
}
|
||||
|
||||
func TestLinuxKitPublisher_BuildBaseName_EdgeCases_Good(t *testing.T) {
|
||||
func TestLinuxKit_LinuxKitPublisherBuildBaseNameEdgeCases_Good(t *testing.T) {
|
||||
p := NewLinuxKitPublisher()
|
||||
|
||||
tests := []struct {
|
||||
|
|
@ -740,7 +655,7 @@ func TestLinuxKitPublisher_BuildBaseName_EdgeCases_Good(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestLinuxKitPublisher_GetArtifactPath_AllFormats_Good(t *testing.T) {
|
||||
func TestLinuxKit_LinuxKitPublisherGetArtifactPathAllFormats_Good(t *testing.T) {
|
||||
p := NewLinuxKitPublisher()
|
||||
|
||||
tests := []struct {
|
||||
|
|
@ -802,7 +717,7 @@ func TestLinuxKitPublisher_GetArtifactPath_AllFormats_Good(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestLinuxKitPublisher_Publish_NilFS_Bad(t *testing.T) {
|
||||
func TestLinuxKit_LinuxKitPublisherPublishNilFS_Bad(t *testing.T) {
|
||||
if err := validateLinuxKitCli(); err != nil {
|
||||
t.Skip("skipping test: linuxkit CLI not available")
|
||||
}
|
||||
|
|
@ -824,7 +739,7 @@ func TestLinuxKitPublisher_Publish_NilFS_Bad(t *testing.T) {
|
|||
})
|
||||
}
|
||||
|
||||
func TestLinuxKitPublisher_Publish_DryRun_Good(t *testing.T) {
|
||||
func TestLinuxKit_LinuxKitPublisherPublishDryRun_Good(t *testing.T) {
|
||||
if testing.Short() {
|
||||
t.Skip("skipping integration test in short mode")
|
||||
}
|
||||
|
|
@ -838,22 +753,15 @@ func TestLinuxKitPublisher_Publish_DryRun_Good(t *testing.T) {
|
|||
|
||||
t.Run("dry run succeeds with valid config file", func(t *testing.T) {
|
||||
// Create temp directory with config file
|
||||
tmpDir, err := os.MkdirTemp("", "linuxkit-test")
|
||||
require.NoError(t, err)
|
||||
defer func() { _ = os.RemoveAll(tmpDir) }()
|
||||
|
||||
configDir := filepath.Join(tmpDir, ".core", "linuxkit")
|
||||
err = os.MkdirAll(configDir, 0755)
|
||||
tmpDir := t.TempDir()
|
||||
configDir := ax.Join(tmpDir, ".core", "linuxkit")
|
||||
err := ax.MkdirAll(configDir, 0o755)
|
||||
require.NoError(t, err)
|
||||
|
||||
configPath := filepath.Join(configDir, "server.yml")
|
||||
err = os.WriteFile(configPath, []byte("kernel:\n image: linuxkit/kernel:5.10\n"), 0644)
|
||||
configPath := ax.Join(configDir, "server.yml")
|
||||
err = ax.WriteFile(configPath, []byte("kernel:\n image: linuxkit/kernel:5.10\n"), 0o644)
|
||||
require.NoError(t, err)
|
||||
|
||||
oldStdout := os.Stdout
|
||||
r, w, _ := os.Pipe()
|
||||
os.Stdout = w
|
||||
|
||||
release := &Release{
|
||||
Version: "v1.0.0",
|
||||
ProjectDir: tmpDir,
|
||||
|
|
@ -862,31 +770,20 @@ func TestLinuxKitPublisher_Publish_DryRun_Good(t *testing.T) {
|
|||
pubCfg := PublisherConfig{Type: "linuxkit"}
|
||||
relCfg := &mockReleaseConfig{repository: "owner/repo"}
|
||||
|
||||
err = p.Publish(context.TODO(), release, pubCfg, relCfg, true)
|
||||
|
||||
_ = w.Close()
|
||||
var buf bytes.Buffer
|
||||
_, _ = buf.ReadFrom(r)
|
||||
os.Stdout = oldStdout
|
||||
|
||||
output := capturePublisherOutput(t, func() {
|
||||
err = p.Publish(context.TODO(), release, pubCfg, relCfg, true)
|
||||
})
|
||||
require.NoError(t, err)
|
||||
output := buf.String()
|
||||
assert.Contains(t, output, "DRY RUN: LinuxKit Build & Publish")
|
||||
})
|
||||
|
||||
t.Run("dry run uses custom config path", func(t *testing.T) {
|
||||
tmpDir, err := os.MkdirTemp("", "linuxkit-test")
|
||||
require.NoError(t, err)
|
||||
defer func() { _ = os.RemoveAll(tmpDir) }()
|
||||
tmpDir := t.TempDir()
|
||||
|
||||
customConfigPath := filepath.Join(tmpDir, "custom-config.yml")
|
||||
err = os.WriteFile(customConfigPath, []byte("kernel:\n image: custom\n"), 0644)
|
||||
customConfigPath := ax.Join(tmpDir, "custom-config.yml")
|
||||
err := ax.WriteFile(customConfigPath, []byte("kernel:\n image: custom\n"), 0o644)
|
||||
require.NoError(t, err)
|
||||
|
||||
oldStdout := os.Stdout
|
||||
r, w, _ := os.Pipe()
|
||||
os.Stdout = w
|
||||
|
||||
release := &Release{
|
||||
Version: "v1.0.0",
|
||||
ProjectDir: tmpDir,
|
||||
|
|
@ -900,31 +797,20 @@ func TestLinuxKitPublisher_Publish_DryRun_Good(t *testing.T) {
|
|||
}
|
||||
relCfg := &mockReleaseConfig{repository: "owner/repo"}
|
||||
|
||||
err = p.Publish(context.TODO(), release, pubCfg, relCfg, true)
|
||||
|
||||
_ = w.Close()
|
||||
var buf bytes.Buffer
|
||||
_, _ = buf.ReadFrom(r)
|
||||
os.Stdout = oldStdout
|
||||
|
||||
output := capturePublisherOutput(t, func() {
|
||||
err = p.Publish(context.TODO(), release, pubCfg, relCfg, true)
|
||||
})
|
||||
require.NoError(t, err)
|
||||
output := buf.String()
|
||||
assert.Contains(t, output, "custom-config.yml")
|
||||
})
|
||||
|
||||
t.Run("dry run with multiple formats and platforms", func(t *testing.T) {
|
||||
tmpDir, err := os.MkdirTemp("", "linuxkit-test")
|
||||
require.NoError(t, err)
|
||||
defer func() { _ = os.RemoveAll(tmpDir) }()
|
||||
tmpDir := t.TempDir()
|
||||
|
||||
configPath := filepath.Join(tmpDir, "config.yml")
|
||||
err = os.WriteFile(configPath, []byte("kernel:\n image: test\n"), 0644)
|
||||
configPath := ax.Join(tmpDir, "config.yml")
|
||||
err := ax.WriteFile(configPath, []byte("kernel:\n image: test\n"), 0o644)
|
||||
require.NoError(t, err)
|
||||
|
||||
oldStdout := os.Stdout
|
||||
r, w, _ := os.Pipe()
|
||||
os.Stdout = w
|
||||
|
||||
release := &Release{
|
||||
Version: "v2.0.0",
|
||||
ProjectDir: tmpDir,
|
||||
|
|
@ -940,15 +826,10 @@ func TestLinuxKitPublisher_Publish_DryRun_Good(t *testing.T) {
|
|||
}
|
||||
relCfg := &mockReleaseConfig{repository: "owner/repo"}
|
||||
|
||||
err = p.Publish(context.TODO(), release, pubCfg, relCfg, true)
|
||||
|
||||
_ = w.Close()
|
||||
var buf bytes.Buffer
|
||||
_, _ = buf.ReadFrom(r)
|
||||
os.Stdout = oldStdout
|
||||
|
||||
output := capturePublisherOutput(t, func() {
|
||||
err = p.Publish(context.TODO(), release, pubCfg, relCfg, true)
|
||||
})
|
||||
require.NoError(t, err)
|
||||
output := buf.String()
|
||||
|
||||
// Check all format/platform combinations are listed
|
||||
assert.Contains(t, output, "linuxkit-2.0.0-amd64.iso")
|
||||
|
|
|
|||
|
|
@ -5,13 +5,10 @@ import (
|
|||
"bytes"
|
||||
"context"
|
||||
"embed"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"text/template"
|
||||
|
||||
"dappco.re/go/core"
|
||||
"dappco.re/go/core/build/internal/ax"
|
||||
coreio "dappco.re/go/core/io"
|
||||
coreerr "dappco.re/go/core/log"
|
||||
)
|
||||
|
|
@ -20,6 +17,7 @@ import (
|
|||
var npmTemplates embed.FS
|
||||
|
||||
// NpmConfig holds npm-specific configuration.
|
||||
// Usage example: declare a value of type publishers.NpmConfig in integrating code.
|
||||
type NpmConfig struct {
|
||||
// Package is the npm package name (e.g., "@host-uk/core").
|
||||
Package string
|
||||
|
|
@ -28,20 +26,24 @@ type NpmConfig struct {
|
|||
}
|
||||
|
||||
// NpmPublisher publishes releases to npm using the binary wrapper pattern.
|
||||
// Usage example: declare a value of type publishers.NpmPublisher in integrating code.
|
||||
type NpmPublisher struct{}
|
||||
|
||||
// NewNpmPublisher creates a new npm publisher.
|
||||
// Usage example: call publishers.NewNpmPublisher(...) from integrating code.
|
||||
func NewNpmPublisher() *NpmPublisher {
|
||||
return &NpmPublisher{}
|
||||
}
|
||||
|
||||
// Name returns the publisher's identifier.
|
||||
// Usage example: call value.Name(...) from integrating code.
|
||||
func (p *NpmPublisher) Name() string {
|
||||
return "npm"
|
||||
}
|
||||
|
||||
// Publish publishes the release to npm.
|
||||
// It generates a binary wrapper package that downloads the correct platform binary on postinstall.
|
||||
// Usage example: call value.Publish(...) from integrating code.
|
||||
func (p *NpmPublisher) Publish(ctx context.Context, release *Release, pubCfg PublisherConfig, relCfg ReleaseConfig, dryRun bool) error {
|
||||
// Parse npm config
|
||||
npmCfg := p.parseConfig(pubCfg, relCfg)
|
||||
|
|
@ -71,18 +73,18 @@ func (p *NpmPublisher) Publish(ctx context.Context, release *Release, pubCfg Pub
|
|||
}
|
||||
if projectName == "" {
|
||||
// Try to infer from package name
|
||||
parts := strings.Split(npmCfg.Package, "/")
|
||||
parts := core.Split(npmCfg.Package, "/")
|
||||
projectName = parts[len(parts)-1]
|
||||
}
|
||||
|
||||
// Strip leading 'v' from version for npm
|
||||
version := strings.TrimPrefix(release.Version, "v")
|
||||
version := core.TrimPrefix(release.Version, "v")
|
||||
|
||||
// Template data
|
||||
data := npmTemplateData{
|
||||
Package: npmCfg.Package,
|
||||
Version: version,
|
||||
Description: fmt.Sprintf("%s CLI", projectName),
|
||||
Description: core.Sprintf("%s CLI", projectName),
|
||||
License: "MIT",
|
||||
Repository: repo,
|
||||
BinaryName: projectName,
|
||||
|
|
@ -131,30 +133,30 @@ type npmTemplateData struct {
|
|||
|
||||
// dryRunPublish shows what would be done without actually publishing.
|
||||
func (p *NpmPublisher) dryRunPublish(m coreio.Medium, data npmTemplateData) error {
|
||||
fmt.Println()
|
||||
fmt.Println("=== DRY RUN: npm Publish ===")
|
||||
fmt.Println()
|
||||
fmt.Printf("Package: %s\n", data.Package)
|
||||
fmt.Printf("Version: %s\n", data.Version)
|
||||
fmt.Printf("Access: %s\n", data.Access)
|
||||
fmt.Printf("Repository: %s\n", data.Repository)
|
||||
fmt.Printf("Binary: %s\n", data.BinaryName)
|
||||
fmt.Println()
|
||||
publisherPrintln()
|
||||
publisherPrintln("=== DRY RUN: npm Publish ===")
|
||||
publisherPrintln()
|
||||
publisherPrint("Package: %s", data.Package)
|
||||
publisherPrint("Version: %s", data.Version)
|
||||
publisherPrint("Access: %s", data.Access)
|
||||
publisherPrint("Repository: %s", data.Repository)
|
||||
publisherPrint("Binary: %s", data.BinaryName)
|
||||
publisherPrintln()
|
||||
|
||||
// Generate and show package.json
|
||||
pkgJSON, err := p.renderTemplate(m, "templates/npm/package.json.tmpl", data)
|
||||
if err != nil {
|
||||
return coreerr.E("npm.dryRunPublish", "failed to render template", err)
|
||||
}
|
||||
fmt.Println("Generated package.json:")
|
||||
fmt.Println("---")
|
||||
fmt.Println(pkgJSON)
|
||||
fmt.Println("---")
|
||||
fmt.Println()
|
||||
publisherPrintln("Generated package.json:")
|
||||
publisherPrintln("---")
|
||||
publisherPrintln(pkgJSON)
|
||||
publisherPrintln("---")
|
||||
publisherPrintln()
|
||||
|
||||
fmt.Println("Would run: npm publish --access", data.Access)
|
||||
fmt.Println()
|
||||
fmt.Println("=== END DRY RUN ===")
|
||||
publisherPrintln("Would run: npm publish --access", data.Access)
|
||||
publisherPrintln()
|
||||
publisherPrintln("=== END DRY RUN ===")
|
||||
|
||||
return nil
|
||||
}
|
||||
|
|
@ -162,19 +164,20 @@ func (p *NpmPublisher) dryRunPublish(m coreio.Medium, data npmTemplateData) erro
|
|||
// executePublish actually creates and publishes the npm package.
|
||||
func (p *NpmPublisher) executePublish(ctx context.Context, m coreio.Medium, data npmTemplateData, cfg *NpmConfig) error {
|
||||
// Check for NPM_TOKEN
|
||||
if os.Getenv("NPM_TOKEN") == "" {
|
||||
npmToken := core.Env("NPM_TOKEN")
|
||||
if npmToken == "" {
|
||||
return coreerr.E("npm.Publish", "NPM_TOKEN environment variable is required", nil)
|
||||
}
|
||||
|
||||
// Create temp directory for package
|
||||
tmpDir, err := os.MkdirTemp("", "npm-publish-*")
|
||||
tmpDir, err := ax.TempDir("npm-publish-*")
|
||||
if err != nil {
|
||||
return coreerr.E("npm.Publish", "failed to create temp directory", err)
|
||||
}
|
||||
defer func() { _ = coreio.Local.DeleteAll(tmpDir) }()
|
||||
defer func() { _ = ax.RemoveAll(tmpDir) }()
|
||||
|
||||
// Create bin directory
|
||||
binDir := filepath.Join(tmpDir, "bin")
|
||||
binDir := ax.Join(tmpDir, "bin")
|
||||
if err := coreio.Local.EnsureDir(binDir); err != nil {
|
||||
return coreerr.E("npm.Publish", "failed to create bin directory", err)
|
||||
}
|
||||
|
|
@ -184,7 +187,7 @@ func (p *NpmPublisher) executePublish(ctx context.Context, m coreio.Medium, data
|
|||
if err != nil {
|
||||
return coreerr.E("npm.Publish", "failed to render package.json", err)
|
||||
}
|
||||
if err := coreio.Local.Write(filepath.Join(tmpDir, "package.json"), pkgJSON); err != nil {
|
||||
if err := coreio.Local.Write(ax.Join(tmpDir, "package.json"), pkgJSON); err != nil {
|
||||
return coreerr.E("npm.Publish", "failed to write package.json", err)
|
||||
}
|
||||
|
||||
|
|
@ -193,7 +196,7 @@ func (p *NpmPublisher) executePublish(ctx context.Context, m coreio.Medium, data
|
|||
if err != nil {
|
||||
return coreerr.E("npm.Publish", "failed to render install.js", err)
|
||||
}
|
||||
if err := coreio.Local.Write(filepath.Join(tmpDir, "install.js"), installJS); err != nil {
|
||||
if err := coreio.Local.Write(ax.Join(tmpDir, "install.js"), installJS); err != nil {
|
||||
return coreerr.E("npm.Publish", "failed to write install.js", err)
|
||||
}
|
||||
|
||||
|
|
@ -202,30 +205,24 @@ func (p *NpmPublisher) executePublish(ctx context.Context, m coreio.Medium, data
|
|||
if err != nil {
|
||||
return coreerr.E("npm.Publish", "failed to render run.js", err)
|
||||
}
|
||||
if err := coreio.Local.Write(filepath.Join(binDir, "run.js"), runJS); err != nil {
|
||||
if err := coreio.Local.Write(ax.Join(binDir, "run.js"), runJS); err != nil {
|
||||
return coreerr.E("npm.Publish", "failed to write run.js", err)
|
||||
}
|
||||
|
||||
// Create .npmrc with token
|
||||
npmrc := "//registry.npmjs.org/:_authToken=${NPM_TOKEN}\n"
|
||||
if err := coreio.Local.Write(filepath.Join(tmpDir, ".npmrc"), npmrc); err != nil {
|
||||
if err := coreio.Local.Write(ax.Join(tmpDir, ".npmrc"), npmrc); err != nil {
|
||||
return coreerr.E("npm.Publish", "failed to write .npmrc", err)
|
||||
}
|
||||
|
||||
// Run npm publish
|
||||
cmd := exec.CommandContext(ctx, "npm", "publish", "--access", data.Access)
|
||||
cmd.Dir = tmpDir
|
||||
cmd.Stdout = os.Stdout
|
||||
cmd.Stderr = os.Stderr
|
||||
cmd.Env = append(os.Environ(), "NPM_TOKEN="+os.Getenv("NPM_TOKEN"))
|
||||
|
||||
fmt.Printf("Publishing %s@%s to npm...\n", data.Package, data.Version)
|
||||
if err := cmd.Run(); err != nil {
|
||||
publisherPrint("Publishing %s@%s to npm...", data.Package, data.Version)
|
||||
if err := publisherRun(ctx, tmpDir, []string{"NPM_TOKEN=" + npmToken}, "npm", "publish", "--access", data.Access); err != nil {
|
||||
return coreerr.E("npm.Publish", "npm publish failed", err)
|
||||
}
|
||||
|
||||
fmt.Printf("Published %s@%s to npm\n", data.Package, data.Version)
|
||||
fmt.Printf(" https://www.npmjs.com/package/%s\n", data.Package)
|
||||
publisherPrint("Published %s@%s to npm", data.Package, data.Version)
|
||||
publisherPrint(" https://www.npmjs.com/package/%s", data.Package)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
|
@ -236,7 +233,7 @@ func (p *NpmPublisher) renderTemplate(m coreio.Medium, name string, data npmTemp
|
|||
var err error
|
||||
|
||||
// Try custom template from medium
|
||||
customPath := filepath.Join(".core", name)
|
||||
customPath := ax.Join(".core", name)
|
||||
if m != nil && m.IsFile(customPath) {
|
||||
customContent, err := m.Read(customPath)
|
||||
if err == nil {
|
||||
|
|
@ -252,7 +249,7 @@ func (p *NpmPublisher) renderTemplate(m coreio.Medium, name string, data npmTemp
|
|||
}
|
||||
}
|
||||
|
||||
tmpl, err := template.New(filepath.Base(name)).Parse(string(content))
|
||||
tmpl, err := template.New(ax.Base(name)).Parse(string(content))
|
||||
if err != nil {
|
||||
return "", coreerr.E("npm.renderTemplate", "failed to parse template "+name, err)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,9 +1,7 @@
|
|||
package publishers
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"dappco.re/go/core/io"
|
||||
|
|
@ -12,14 +10,14 @@ import (
|
|||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestNpmPublisher_Name_Good(t *testing.T) {
|
||||
func TestNpm_NpmPublisherName_Good(t *testing.T) {
|
||||
t.Run("returns npm", func(t *testing.T) {
|
||||
p := NewNpmPublisher()
|
||||
assert.Equal(t, "npm", p.Name())
|
||||
})
|
||||
}
|
||||
|
||||
func TestNpmPublisher_ParseConfig_Good(t *testing.T) {
|
||||
func TestNpm_NpmPublisherParseConfig_Good(t *testing.T) {
|
||||
p := NewNpmPublisher()
|
||||
|
||||
t.Run("uses defaults when no extended config", func(t *testing.T) {
|
||||
|
|
@ -88,7 +86,7 @@ func TestNpmPublisher_ParseConfig_Good(t *testing.T) {
|
|||
})
|
||||
}
|
||||
|
||||
func TestNpmPublisher_RenderTemplate_Good(t *testing.T) {
|
||||
func TestNpm_NpmPublisherRenderTemplate_Good(t *testing.T) {
|
||||
p := NewNpmPublisher()
|
||||
|
||||
t.Run("renders package.json template with data", func(t *testing.T) {
|
||||
|
|
@ -134,7 +132,7 @@ func TestNpmPublisher_RenderTemplate_Good(t *testing.T) {
|
|||
})
|
||||
}
|
||||
|
||||
func TestNpmPublisher_RenderTemplate_Bad(t *testing.T) {
|
||||
func TestNpm_NpmPublisherRenderTemplate_Bad(t *testing.T) {
|
||||
p := NewNpmPublisher()
|
||||
|
||||
t.Run("returns error for non-existent template", func(t *testing.T) {
|
||||
|
|
@ -145,14 +143,10 @@ func TestNpmPublisher_RenderTemplate_Bad(t *testing.T) {
|
|||
})
|
||||
}
|
||||
|
||||
func TestNpmPublisher_DryRunPublish_Good(t *testing.T) {
|
||||
func TestNpm_NpmPublisherDryRunPublish_Good(t *testing.T) {
|
||||
p := NewNpmPublisher()
|
||||
|
||||
t.Run("outputs expected dry run information", func(t *testing.T) {
|
||||
oldStdout := os.Stdout
|
||||
r, w, _ := os.Pipe()
|
||||
os.Stdout = w
|
||||
|
||||
data := npmTemplateData{
|
||||
Package: "@myorg/mycli",
|
||||
Version: "1.0.0",
|
||||
|
|
@ -161,15 +155,11 @@ func TestNpmPublisher_DryRunPublish_Good(t *testing.T) {
|
|||
BinaryName: "mycli",
|
||||
Description: "My CLI",
|
||||
}
|
||||
err := p.dryRunPublish(io.Local, data)
|
||||
|
||||
_ = w.Close()
|
||||
var buf bytes.Buffer
|
||||
_, _ = buf.ReadFrom(r)
|
||||
os.Stdout = oldStdout
|
||||
|
||||
var err error
|
||||
output := capturePublisherOutput(t, func() {
|
||||
err = p.dryRunPublish(io.Local, data)
|
||||
})
|
||||
require.NoError(t, err)
|
||||
output := buf.String()
|
||||
|
||||
assert.Contains(t, output, "DRY RUN: npm Publish")
|
||||
assert.Contains(t, output, "Package: @myorg/mycli")
|
||||
|
|
@ -183,10 +173,6 @@ func TestNpmPublisher_DryRunPublish_Good(t *testing.T) {
|
|||
})
|
||||
|
||||
t.Run("shows restricted access correctly", func(t *testing.T) {
|
||||
oldStdout := os.Stdout
|
||||
r, w, _ := os.Pipe()
|
||||
os.Stdout = w
|
||||
|
||||
data := npmTemplateData{
|
||||
Package: "@private/cli",
|
||||
Version: "2.0.0",
|
||||
|
|
@ -195,22 +181,18 @@ func TestNpmPublisher_DryRunPublish_Good(t *testing.T) {
|
|||
BinaryName: "cli",
|
||||
}
|
||||
|
||||
err := p.dryRunPublish(io.Local, data)
|
||||
|
||||
_ = w.Close()
|
||||
var buf bytes.Buffer
|
||||
_, _ = buf.ReadFrom(r)
|
||||
os.Stdout = oldStdout
|
||||
|
||||
var err error
|
||||
output := capturePublisherOutput(t, func() {
|
||||
err = p.dryRunPublish(io.Local, data)
|
||||
})
|
||||
require.NoError(t, err)
|
||||
output := buf.String()
|
||||
|
||||
assert.Contains(t, output, "Access: restricted")
|
||||
assert.Contains(t, output, "Would run: npm publish --access restricted")
|
||||
})
|
||||
}
|
||||
|
||||
func TestNpmPublisher_Publish_Bad(t *testing.T) {
|
||||
func TestNpm_NpmPublisherPublish_Bad(t *testing.T) {
|
||||
p := NewNpmPublisher()
|
||||
|
||||
t.Run("fails when package name not configured", func(t *testing.T) {
|
||||
|
|
@ -228,14 +210,7 @@ func TestNpmPublisher_Publish_Bad(t *testing.T) {
|
|||
})
|
||||
|
||||
t.Run("fails when NPM_TOKEN not set in non-dry-run", func(t *testing.T) {
|
||||
// Ensure NPM_TOKEN is not set
|
||||
oldToken := os.Getenv("NPM_TOKEN")
|
||||
_ = os.Unsetenv("NPM_TOKEN")
|
||||
defer func() {
|
||||
if oldToken != "" {
|
||||
_ = os.Setenv("NPM_TOKEN", oldToken)
|
||||
}
|
||||
}()
|
||||
t.Setenv("NPM_TOKEN", "")
|
||||
|
||||
release := &Release{
|
||||
Version: "v1.0.0",
|
||||
|
|
@ -256,7 +231,7 @@ func TestNpmPublisher_Publish_Bad(t *testing.T) {
|
|||
})
|
||||
}
|
||||
|
||||
func TestNpmConfig_Defaults_Good(t *testing.T) {
|
||||
func TestNpm_NpmConfigDefaults_Good(t *testing.T) {
|
||||
t.Run("has sensible defaults", func(t *testing.T) {
|
||||
p := NewNpmPublisher()
|
||||
pubCfg := PublisherConfig{Type: "npm"}
|
||||
|
|
@ -269,7 +244,7 @@ func TestNpmConfig_Defaults_Good(t *testing.T) {
|
|||
})
|
||||
}
|
||||
|
||||
func TestNpmTemplateData_Good(t *testing.T) {
|
||||
func TestNpm_NpmTemplateData_Good(t *testing.T) {
|
||||
t.Run("struct has all expected fields", func(t *testing.T) {
|
||||
data := npmTemplateData{
|
||||
Package: "@myorg/package",
|
||||
|
|
|
|||
38
pkg/release/publishers/output.go
Normal file
38
pkg/release/publishers/output.go
Normal file
|
|
@ -0,0 +1,38 @@
|
|||
package publishers
|
||||
|
||||
import (
|
||||
"context"
|
||||
"io"
|
||||
|
||||
"dappco.re/go/core"
|
||||
"dappco.re/go/core/build/internal/ax"
|
||||
)
|
||||
|
||||
var publisherStdout io.Writer
|
||||
var publisherStderr io.Writer
|
||||
|
||||
func publisherPrint(format string, args ...any) {
|
||||
core.Print(publisherStdout, format, args...)
|
||||
}
|
||||
|
||||
func publisherPrintln(args ...any) {
|
||||
if len(args) == 0 {
|
||||
publisherPrint("")
|
||||
return
|
||||
}
|
||||
|
||||
parts := make([]string, 0, len(args))
|
||||
for _, arg := range args {
|
||||
parts = append(parts, core.Sprintf("%v", arg))
|
||||
}
|
||||
|
||||
publisherPrint("%s", core.Join(" ", parts...))
|
||||
}
|
||||
|
||||
func publisherRun(ctx context.Context, dir string, env []string, command string, args ...string) error {
|
||||
output, err := ax.CombinedOutput(ctx, dir, env, command, args...)
|
||||
if output != "" {
|
||||
publisherPrint("%s", output)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
|
@ -9,6 +9,7 @@ import (
|
|||
)
|
||||
|
||||
// Release represents a release to be published.
|
||||
// Usage example: declare a value of type publishers.Release in integrating code.
|
||||
type Release struct {
|
||||
// Version is the semantic version string (e.g., "v1.2.3").
|
||||
Version string
|
||||
|
|
@ -23,6 +24,7 @@ type Release struct {
|
|||
}
|
||||
|
||||
// PublisherConfig holds configuration for a publisher.
|
||||
// Usage example: declare a value of type publishers.PublisherConfig in integrating code.
|
||||
type PublisherConfig struct {
|
||||
// Type is the publisher type (e.g., "github", "linuxkit", "docker").
|
||||
Type string
|
||||
|
|
@ -35,12 +37,14 @@ type PublisherConfig struct {
|
|||
}
|
||||
|
||||
// ReleaseConfig holds release configuration needed by publishers.
|
||||
// Usage example: declare a value of type publishers.ReleaseConfig in integrating code.
|
||||
type ReleaseConfig interface {
|
||||
GetRepository() string
|
||||
GetProjectName() string
|
||||
}
|
||||
|
||||
// Publisher defines the interface for release publishers.
|
||||
// Usage example: declare a value of type publishers.Publisher in integrating code.
|
||||
type Publisher interface {
|
||||
// Name returns the publisher's identifier.
|
||||
Name() string
|
||||
|
|
@ -51,6 +55,7 @@ type Publisher interface {
|
|||
|
||||
// NewRelease creates a Release from the release package's Release type.
|
||||
// This is a helper to convert between packages.
|
||||
// Usage example: call publishers.NewRelease(...) from integrating code.
|
||||
func NewRelease(version string, artifacts []build.Artifact, changelog, projectDir string, fs io.Medium) *Release {
|
||||
return &Release{
|
||||
Version: version,
|
||||
|
|
@ -62,6 +67,7 @@ func NewRelease(version string, artifacts []build.Artifact, changelog, projectDi
|
|||
}
|
||||
|
||||
// NewPublisherConfig creates a PublisherConfig.
|
||||
// Usage example: call publishers.NewPublisherConfig(...) from integrating code.
|
||||
func NewPublisherConfig(pubType string, prerelease, draft bool, extended any) PublisherConfig {
|
||||
return PublisherConfig{
|
||||
Type: pubType,
|
||||
|
|
|
|||
|
|
@ -5,13 +5,10 @@ import (
|
|||
"bytes"
|
||||
"context"
|
||||
"embed"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"text/template"
|
||||
|
||||
"dappco.re/go/core"
|
||||
"dappco.re/go/core/build/internal/ax"
|
||||
"dappco.re/go/core/build/pkg/build"
|
||||
coreio "dappco.re/go/core/io"
|
||||
coreerr "dappco.re/go/core/log"
|
||||
|
|
@ -21,6 +18,7 @@ import (
|
|||
var scoopTemplates embed.FS
|
||||
|
||||
// ScoopConfig holds Scoop-specific configuration.
|
||||
// Usage example: declare a value of type publishers.ScoopConfig in integrating code.
|
||||
type ScoopConfig struct {
|
||||
// Bucket is the Scoop bucket repository (e.g., "host-uk/scoop-bucket").
|
||||
Bucket string
|
||||
|
|
@ -29,19 +27,23 @@ type ScoopConfig struct {
|
|||
}
|
||||
|
||||
// ScoopPublisher publishes releases to Scoop.
|
||||
// Usage example: declare a value of type publishers.ScoopPublisher in integrating code.
|
||||
type ScoopPublisher struct{}
|
||||
|
||||
// NewScoopPublisher creates a new Scoop publisher.
|
||||
// Usage example: call publishers.NewScoopPublisher(...) from integrating code.
|
||||
func NewScoopPublisher() *ScoopPublisher {
|
||||
return &ScoopPublisher{}
|
||||
}
|
||||
|
||||
// Name returns the publisher's identifier.
|
||||
// Usage example: call value.Name(...) from integrating code.
|
||||
func (p *ScoopPublisher) Name() string {
|
||||
return "scoop"
|
||||
}
|
||||
|
||||
// Publish publishes the release to Scoop.
|
||||
// Usage example: call value.Publish(...) from integrating code.
|
||||
func (p *ScoopPublisher) Publish(ctx context.Context, release *Release, pubCfg PublisherConfig, relCfg ReleaseConfig, dryRun bool) error {
|
||||
cfg := p.parseConfig(pubCfg, relCfg)
|
||||
|
||||
|
|
@ -66,16 +68,16 @@ func (p *ScoopPublisher) Publish(ctx context.Context, release *Release, pubCfg P
|
|||
projectName = relCfg.GetProjectName()
|
||||
}
|
||||
if projectName == "" {
|
||||
parts := strings.Split(repo, "/")
|
||||
parts := core.Split(repo, "/")
|
||||
projectName = parts[len(parts)-1]
|
||||
}
|
||||
|
||||
version := strings.TrimPrefix(release.Version, "v")
|
||||
version := core.TrimPrefix(release.Version, "v")
|
||||
checksums := buildChecksumMap(release.Artifacts)
|
||||
|
||||
data := scoopTemplateData{
|
||||
PackageName: projectName,
|
||||
Description: fmt.Sprintf("%s CLI", projectName),
|
||||
Description: core.Sprintf("%s CLI", projectName),
|
||||
Repository: repo,
|
||||
Version: version,
|
||||
License: "MIT",
|
||||
|
|
@ -122,37 +124,37 @@ func (p *ScoopPublisher) parseConfig(pubCfg PublisherConfig, relCfg ReleaseConfi
|
|||
}
|
||||
|
||||
func (p *ScoopPublisher) dryRunPublish(m coreio.Medium, data scoopTemplateData, cfg ScoopConfig) error {
|
||||
fmt.Println()
|
||||
fmt.Println("=== DRY RUN: Scoop Publish ===")
|
||||
fmt.Println()
|
||||
fmt.Printf("Package: %s\n", data.PackageName)
|
||||
fmt.Printf("Version: %s\n", data.Version)
|
||||
fmt.Printf("Bucket: %s\n", cfg.Bucket)
|
||||
fmt.Printf("Repository: %s\n", data.Repository)
|
||||
fmt.Println()
|
||||
publisherPrintln()
|
||||
publisherPrintln("=== DRY RUN: Scoop Publish ===")
|
||||
publisherPrintln()
|
||||
publisherPrint("Package: %s", data.PackageName)
|
||||
publisherPrint("Version: %s", data.Version)
|
||||
publisherPrint("Bucket: %s", cfg.Bucket)
|
||||
publisherPrint("Repository: %s", data.Repository)
|
||||
publisherPrintln()
|
||||
|
||||
manifest, err := p.renderTemplate(m, "templates/scoop/manifest.json.tmpl", data)
|
||||
if err != nil {
|
||||
return coreerr.E("scoop.dryRunPublish", "failed to render template", err)
|
||||
}
|
||||
fmt.Println("Generated manifest.json:")
|
||||
fmt.Println("---")
|
||||
fmt.Println(manifest)
|
||||
fmt.Println("---")
|
||||
fmt.Println()
|
||||
publisherPrintln("Generated manifest.json:")
|
||||
publisherPrintln("---")
|
||||
publisherPrintln(manifest)
|
||||
publisherPrintln("---")
|
||||
publisherPrintln()
|
||||
|
||||
if cfg.Bucket != "" {
|
||||
fmt.Printf("Would commit to bucket: %s\n", cfg.Bucket)
|
||||
publisherPrint("Would commit to bucket: %s", cfg.Bucket)
|
||||
}
|
||||
if cfg.Official != nil && cfg.Official.Enabled {
|
||||
output := cfg.Official.Output
|
||||
if output == "" {
|
||||
output = "dist/scoop"
|
||||
}
|
||||
fmt.Printf("Would write files for official PR to: %s\n", output)
|
||||
publisherPrint("Would write files for official PR to: %s", output)
|
||||
}
|
||||
fmt.Println()
|
||||
fmt.Println("=== END DRY RUN ===")
|
||||
publisherPrintln()
|
||||
publisherPrintln("=== END DRY RUN ===")
|
||||
|
||||
return nil
|
||||
}
|
||||
|
|
@ -167,20 +169,20 @@ func (p *ScoopPublisher) executePublish(ctx context.Context, projectDir string,
|
|||
if cfg.Official != nil && cfg.Official.Enabled {
|
||||
output := cfg.Official.Output
|
||||
if output == "" {
|
||||
output = filepath.Join(projectDir, "dist", "scoop")
|
||||
} else if !filepath.IsAbs(output) {
|
||||
output = filepath.Join(projectDir, output)
|
||||
output = ax.Join(projectDir, "dist", "scoop")
|
||||
} else if !ax.IsAbs(output) {
|
||||
output = ax.Join(projectDir, output)
|
||||
}
|
||||
|
||||
if err := release.FS.EnsureDir(output); err != nil {
|
||||
return coreerr.E("scoop.Publish", "failed to create output directory", err)
|
||||
}
|
||||
|
||||
manifestPath := filepath.Join(output, fmt.Sprintf("%s.json", data.PackageName))
|
||||
manifestPath := ax.Join(output, core.Sprintf("%s.json", data.PackageName))
|
||||
if err := release.FS.Write(manifestPath, manifest); err != nil {
|
||||
return coreerr.E("scoop.Publish", "failed to write manifest", err)
|
||||
}
|
||||
fmt.Printf("Wrote Scoop manifest for official PR: %s\n", manifestPath)
|
||||
publisherPrint("Wrote Scoop manifest for official PR: %s", manifestPath)
|
||||
}
|
||||
|
||||
// If bucket is configured, commit to it
|
||||
|
|
@ -194,56 +196,43 @@ func (p *ScoopPublisher) executePublish(ctx context.Context, projectDir string,
|
|||
}
|
||||
|
||||
func (p *ScoopPublisher) commitToBucket(ctx context.Context, bucket string, data scoopTemplateData, manifest string) error {
|
||||
tmpDir, err := os.MkdirTemp("", "scoop-bucket-*")
|
||||
tmpDir, err := ax.TempDir("scoop-bucket-*")
|
||||
if err != nil {
|
||||
return coreerr.E("scoop.commitToBucket", "failed to create temp directory", err)
|
||||
}
|
||||
defer func() { _ = coreio.Local.DeleteAll(tmpDir) }()
|
||||
defer func() { _ = ax.RemoveAll(tmpDir) }()
|
||||
|
||||
fmt.Printf("Cloning bucket %s...\n", bucket)
|
||||
cmd := exec.CommandContext(ctx, "gh", "repo", "clone", bucket, tmpDir, "--", "--depth=1")
|
||||
cmd.Stdout = os.Stdout
|
||||
cmd.Stderr = os.Stderr
|
||||
if err := cmd.Run(); err != nil {
|
||||
publisherPrint("Cloning bucket %s...", bucket)
|
||||
if err := publisherRun(ctx, "", nil, "gh", "repo", "clone", bucket, tmpDir, "--", "--depth=1"); err != nil {
|
||||
return coreerr.E("scoop.commitToBucket", "failed to clone bucket", err)
|
||||
}
|
||||
|
||||
// Ensure bucket directory exists
|
||||
bucketDir := filepath.Join(tmpDir, "bucket")
|
||||
bucketDir := ax.Join(tmpDir, "bucket")
|
||||
if !coreio.Local.IsDir(bucketDir) {
|
||||
bucketDir = tmpDir // Some repos put manifests in root
|
||||
}
|
||||
|
||||
manifestPath := filepath.Join(bucketDir, fmt.Sprintf("%s.json", data.PackageName))
|
||||
manifestPath := ax.Join(bucketDir, core.Sprintf("%s.json", data.PackageName))
|
||||
if err := coreio.Local.Write(manifestPath, manifest); err != nil {
|
||||
return coreerr.E("scoop.commitToBucket", "failed to write manifest", err)
|
||||
}
|
||||
|
||||
commitMsg := fmt.Sprintf("Update %s to %s", data.PackageName, data.Version)
|
||||
commitMsg := core.Sprintf("Update %s to %s", data.PackageName, data.Version)
|
||||
|
||||
cmd = exec.CommandContext(ctx, "git", "add", ".")
|
||||
cmd.Dir = tmpDir
|
||||
if err := cmd.Run(); err != nil {
|
||||
if err := ax.ExecDir(ctx, tmpDir, "git", "add", "."); err != nil {
|
||||
return coreerr.E("scoop.commitToBucket", "git add failed", err)
|
||||
}
|
||||
|
||||
cmd = exec.CommandContext(ctx, "git", "commit", "-m", commitMsg)
|
||||
cmd.Dir = tmpDir
|
||||
cmd.Stdout = os.Stdout
|
||||
cmd.Stderr = os.Stderr
|
||||
if err := cmd.Run(); err != nil {
|
||||
if err := publisherRun(ctx, tmpDir, nil, "git", "commit", "-m", commitMsg); err != nil {
|
||||
return coreerr.E("scoop.commitToBucket", "git commit failed", err)
|
||||
}
|
||||
|
||||
cmd = exec.CommandContext(ctx, "git", "push")
|
||||
cmd.Dir = tmpDir
|
||||
cmd.Stdout = os.Stdout
|
||||
cmd.Stderr = os.Stderr
|
||||
if err := cmd.Run(); err != nil {
|
||||
if err := publisherRun(ctx, tmpDir, nil, "git", "push"); err != nil {
|
||||
return coreerr.E("scoop.commitToBucket", "git push failed", err)
|
||||
}
|
||||
|
||||
fmt.Printf("Updated Scoop bucket: %s\n", bucket)
|
||||
publisherPrint("Updated Scoop bucket: %s", bucket)
|
||||
return nil
|
||||
}
|
||||
|
||||
|
|
@ -252,7 +241,7 @@ func (p *ScoopPublisher) renderTemplate(m coreio.Medium, name string, data scoop
|
|||
var err error
|
||||
|
||||
// Try custom template from medium
|
||||
customPath := filepath.Join(".core", name)
|
||||
customPath := ax.Join(".core", name)
|
||||
if m != nil && m.IsFile(customPath) {
|
||||
customContent, err := m.Read(customPath)
|
||||
if err != nil {
|
||||
|
|
@ -269,7 +258,7 @@ func (p *ScoopPublisher) renderTemplate(m coreio.Medium, name string, data scoop
|
|||
}
|
||||
}
|
||||
|
||||
tmpl, err := template.New(filepath.Base(name)).Parse(string(content))
|
||||
tmpl, err := template.New(ax.Base(name)).Parse(string(content))
|
||||
if err != nil {
|
||||
return "", coreerr.E("scoop.renderTemplate", "failed to parse template "+name, err)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,9 +1,7 @@
|
|||
package publishers
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"dappco.re/go/core/io"
|
||||
|
|
@ -12,14 +10,14 @@ import (
|
|||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestScoopPublisher_Name_Good(t *testing.T) {
|
||||
func TestScoop_ScoopPublisherName_Good(t *testing.T) {
|
||||
t.Run("returns scoop", func(t *testing.T) {
|
||||
p := NewScoopPublisher()
|
||||
assert.Equal(t, "scoop", p.Name())
|
||||
})
|
||||
}
|
||||
|
||||
func TestScoopPublisher_ParseConfig_Good(t *testing.T) {
|
||||
func TestScoop_ScoopPublisherParseConfig_Good(t *testing.T) {
|
||||
p := NewScoopPublisher()
|
||||
|
||||
t.Run("uses defaults when no extended config", func(t *testing.T) {
|
||||
|
|
@ -90,7 +88,7 @@ func TestScoopPublisher_ParseConfig_Good(t *testing.T) {
|
|||
})
|
||||
}
|
||||
|
||||
func TestScoopPublisher_RenderTemplate_Good(t *testing.T) {
|
||||
func TestScoop_ScoopPublisherRenderTemplate_Good(t *testing.T) {
|
||||
p := NewScoopPublisher()
|
||||
|
||||
t.Run("renders manifest template with data", func(t *testing.T) {
|
||||
|
|
@ -143,7 +141,7 @@ func TestScoopPublisher_RenderTemplate_Good(t *testing.T) {
|
|||
})
|
||||
}
|
||||
|
||||
func TestScoopPublisher_RenderTemplate_Bad(t *testing.T) {
|
||||
func TestScoop_ScoopPublisherRenderTemplate_Bad(t *testing.T) {
|
||||
p := NewScoopPublisher()
|
||||
|
||||
t.Run("returns error for non-existent template", func(t *testing.T) {
|
||||
|
|
@ -154,14 +152,10 @@ func TestScoopPublisher_RenderTemplate_Bad(t *testing.T) {
|
|||
})
|
||||
}
|
||||
|
||||
func TestScoopPublisher_DryRunPublish_Good(t *testing.T) {
|
||||
func TestScoop_ScoopPublisherDryRunPublish_Good(t *testing.T) {
|
||||
p := NewScoopPublisher()
|
||||
|
||||
t.Run("outputs expected dry run information", func(t *testing.T) {
|
||||
oldStdout := os.Stdout
|
||||
r, w, _ := os.Pipe()
|
||||
os.Stdout = w
|
||||
|
||||
data := scoopTemplateData{
|
||||
PackageName: "myapp",
|
||||
Version: "1.0.0",
|
||||
|
|
@ -173,15 +167,11 @@ func TestScoopPublisher_DryRunPublish_Good(t *testing.T) {
|
|||
Bucket: "owner/scoop-bucket",
|
||||
}
|
||||
|
||||
err := p.dryRunPublish(io.Local, data, cfg)
|
||||
|
||||
_ = w.Close()
|
||||
var buf bytes.Buffer
|
||||
_, _ = buf.ReadFrom(r)
|
||||
os.Stdout = oldStdout
|
||||
|
||||
var err error
|
||||
output := capturePublisherOutput(t, func() {
|
||||
err = p.dryRunPublish(io.Local, data, cfg)
|
||||
})
|
||||
require.NoError(t, err)
|
||||
output := buf.String()
|
||||
|
||||
assert.Contains(t, output, "DRY RUN: Scoop Publish")
|
||||
assert.Contains(t, output, "Package: myapp")
|
||||
|
|
@ -194,10 +184,6 @@ func TestScoopPublisher_DryRunPublish_Good(t *testing.T) {
|
|||
})
|
||||
|
||||
t.Run("shows official output path when enabled", func(t *testing.T) {
|
||||
oldStdout := os.Stdout
|
||||
r, w, _ := os.Pipe()
|
||||
os.Stdout = w
|
||||
|
||||
data := scoopTemplateData{
|
||||
PackageName: "myapp",
|
||||
Version: "1.0.0",
|
||||
|
|
@ -211,23 +197,15 @@ func TestScoopPublisher_DryRunPublish_Good(t *testing.T) {
|
|||
},
|
||||
}
|
||||
|
||||
err := p.dryRunPublish(io.Local, data, cfg)
|
||||
|
||||
_ = w.Close()
|
||||
var buf bytes.Buffer
|
||||
_, _ = buf.ReadFrom(r)
|
||||
os.Stdout = oldStdout
|
||||
|
||||
var err error
|
||||
output := capturePublisherOutput(t, func() {
|
||||
err = p.dryRunPublish(io.Local, data, cfg)
|
||||
})
|
||||
require.NoError(t, err)
|
||||
output := buf.String()
|
||||
assert.Contains(t, output, "Would write files for official PR to: custom/scoop/path")
|
||||
})
|
||||
|
||||
t.Run("uses default official output path when not specified", func(t *testing.T) {
|
||||
oldStdout := os.Stdout
|
||||
r, w, _ := os.Pipe()
|
||||
os.Stdout = w
|
||||
|
||||
data := scoopTemplateData{
|
||||
PackageName: "myapp",
|
||||
Version: "1.0.0",
|
||||
|
|
@ -240,20 +218,16 @@ func TestScoopPublisher_DryRunPublish_Good(t *testing.T) {
|
|||
},
|
||||
}
|
||||
|
||||
err := p.dryRunPublish(io.Local, data, cfg)
|
||||
|
||||
_ = w.Close()
|
||||
var buf bytes.Buffer
|
||||
_, _ = buf.ReadFrom(r)
|
||||
os.Stdout = oldStdout
|
||||
|
||||
var err error
|
||||
output := capturePublisherOutput(t, func() {
|
||||
err = p.dryRunPublish(io.Local, data, cfg)
|
||||
})
|
||||
require.NoError(t, err)
|
||||
output := buf.String()
|
||||
assert.Contains(t, output, "Would write files for official PR to: dist/scoop")
|
||||
})
|
||||
}
|
||||
|
||||
func TestScoopPublisher_Publish_Bad(t *testing.T) {
|
||||
func TestScoop_ScoopPublisherPublish_Bad(t *testing.T) {
|
||||
p := NewScoopPublisher()
|
||||
|
||||
t.Run("fails when bucket not configured and not official mode", func(t *testing.T) {
|
||||
|
|
@ -271,7 +245,7 @@ func TestScoopPublisher_Publish_Bad(t *testing.T) {
|
|||
})
|
||||
}
|
||||
|
||||
func TestScoopConfig_Defaults_Good(t *testing.T) {
|
||||
func TestScoop_ScoopConfigDefaults_Good(t *testing.T) {
|
||||
t.Run("has sensible defaults", func(t *testing.T) {
|
||||
p := NewScoopPublisher()
|
||||
pubCfg := PublisherConfig{Type: "scoop"}
|
||||
|
|
@ -284,7 +258,7 @@ func TestScoopConfig_Defaults_Good(t *testing.T) {
|
|||
})
|
||||
}
|
||||
|
||||
func TestScoopTemplateData_Good(t *testing.T) {
|
||||
func TestScoop_ScoopTemplateData_Good(t *testing.T) {
|
||||
t.Run("struct has all expected fields", func(t *testing.T) {
|
||||
data := scoopTemplateData{
|
||||
PackageName: "myapp",
|
||||
|
|
|
|||
32
pkg/release/publishers/test_helpers_test.go
Normal file
32
pkg/release/publishers/test_helpers_test.go
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
package publishers
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"dappco.re/go/core/build/internal/ax"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func capturePublisherOutput(t *testing.T, fn func()) string {
|
||||
t.Helper()
|
||||
|
||||
var buf bytes.Buffer
|
||||
oldStdout := publisherStdout
|
||||
oldStderr := publisherStderr
|
||||
publisherStdout = &buf
|
||||
publisherStderr = &buf
|
||||
defer func() {
|
||||
publisherStdout = oldStdout
|
||||
publisherStderr = oldStderr
|
||||
}()
|
||||
|
||||
fn()
|
||||
return buf.String()
|
||||
}
|
||||
|
||||
func runPublisherCommand(t *testing.T, dir, command string, args ...string) {
|
||||
t.Helper()
|
||||
require.NoError(t, ax.ExecDir(context.Background(), dir, command, args...))
|
||||
}
|
||||
|
|
@ -5,10 +5,9 @@ package release
|
|||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"dappco.re/go/core"
|
||||
"dappco.re/go/core/build/internal/ax"
|
||||
"dappco.re/go/core/build/pkg/build"
|
||||
"dappco.re/go/core/build/pkg/build/builders"
|
||||
"dappco.re/go/core/build/pkg/release/publishers"
|
||||
|
|
@ -17,6 +16,7 @@ import (
|
|||
)
|
||||
|
||||
// Release represents a release with its version, artifacts, and changelog.
|
||||
// Usage example: declare a value of type release.Release in integrating code.
|
||||
type Release struct {
|
||||
// Version is the semantic version string (e.g., "v1.2.3").
|
||||
Version string
|
||||
|
|
@ -33,6 +33,7 @@ type Release struct {
|
|||
// Publish publishes pre-built artifacts from dist/ to configured targets.
|
||||
// Use this after `core build` to separate build and publish concerns.
|
||||
// If dryRun is true, it will show what would be done without actually publishing.
|
||||
// Usage example: call release.Publish(...) from integrating code.
|
||||
func Publish(ctx context.Context, cfg *Config, dryRun bool) (*Release, error) {
|
||||
if cfg == nil {
|
||||
return nil, coreerr.E("release.Publish", "config is nil", nil)
|
||||
|
|
@ -46,7 +47,7 @@ func Publish(ctx context.Context, cfg *Config, dryRun bool) (*Release, error) {
|
|||
}
|
||||
|
||||
// Resolve to absolute path
|
||||
absProjectDir, err := filepath.Abs(projectDir)
|
||||
absProjectDir, err := ax.Abs(projectDir)
|
||||
if err != nil {
|
||||
return nil, coreerr.E("release.Publish", "failed to resolve project directory", err)
|
||||
}
|
||||
|
|
@ -61,7 +62,7 @@ func Publish(ctx context.Context, cfg *Config, dryRun bool) (*Release, error) {
|
|||
}
|
||||
|
||||
// Step 2: Find pre-built artifacts in dist/
|
||||
distDir := filepath.Join(absProjectDir, "dist")
|
||||
distDir := ax.Join(absProjectDir, "dist")
|
||||
artifacts, err := findArtifacts(m, distDir)
|
||||
if err != nil {
|
||||
return nil, coreerr.E("release.Publish", "failed to find artifacts", err)
|
||||
|
|
@ -75,7 +76,7 @@ func Publish(ctx context.Context, cfg *Config, dryRun bool) (*Release, error) {
|
|||
changelog, err := Generate(absProjectDir, "", version)
|
||||
if err != nil {
|
||||
// Non-fatal: continue with empty changelog
|
||||
changelog = fmt.Sprintf("Release %s", version)
|
||||
changelog = core.Sprintf("Release %s", version)
|
||||
}
|
||||
|
||||
release := &Release{
|
||||
|
|
@ -126,13 +127,13 @@ func findArtifacts(m io.Medium, distDir string) ([]build.Artifact, error) {
|
|||
}
|
||||
|
||||
name := entry.Name()
|
||||
path := filepath.Join(distDir, name)
|
||||
path := ax.Join(distDir, name)
|
||||
|
||||
// Include archives and checksums
|
||||
if strings.HasSuffix(name, ".tar.gz") ||
|
||||
strings.HasSuffix(name, ".zip") ||
|
||||
strings.HasSuffix(name, ".txt") ||
|
||||
strings.HasSuffix(name, ".sig") {
|
||||
if core.HasSuffix(name, ".tar.gz") ||
|
||||
core.HasSuffix(name, ".zip") ||
|
||||
core.HasSuffix(name, ".txt") ||
|
||||
core.HasSuffix(name, ".sig") {
|
||||
artifacts = append(artifacts, build.Artifact{Path: path})
|
||||
}
|
||||
}
|
||||
|
|
@ -144,6 +145,7 @@ func findArtifacts(m io.Medium, distDir string) ([]build.Artifact, error) {
|
|||
// generate changelog, and publish to configured targets.
|
||||
// For separated concerns, prefer using `core build` then `core ci` (Publish).
|
||||
// If dryRun is true, it will show what would be done without actually publishing.
|
||||
// Usage example: call release.Run(...) from integrating code.
|
||||
func Run(ctx context.Context, cfg *Config, dryRun bool) (*Release, error) {
|
||||
if cfg == nil {
|
||||
return nil, coreerr.E("release.Run", "config is nil", nil)
|
||||
|
|
@ -157,7 +159,7 @@ func Run(ctx context.Context, cfg *Config, dryRun bool) (*Release, error) {
|
|||
}
|
||||
|
||||
// Resolve to absolute path
|
||||
absProjectDir, err := filepath.Abs(projectDir)
|
||||
absProjectDir, err := ax.Abs(projectDir)
|
||||
if err != nil {
|
||||
return nil, coreerr.E("release.Run", "failed to resolve project directory", err)
|
||||
}
|
||||
|
|
@ -175,7 +177,7 @@ func Run(ctx context.Context, cfg *Config, dryRun bool) (*Release, error) {
|
|||
changelog, err := Generate(absProjectDir, "", version)
|
||||
if err != nil {
|
||||
// Non-fatal: continue with empty changelog
|
||||
changelog = fmt.Sprintf("Release %s", version)
|
||||
changelog = core.Sprintf("Release %s", version)
|
||||
}
|
||||
|
||||
// Step 3: Build artifacts
|
||||
|
|
@ -250,11 +252,11 @@ func buildArtifacts(ctx context.Context, fs io.Medium, cfg *Config, projectDir,
|
|||
binaryName = buildCfg.Project.Name
|
||||
}
|
||||
if binaryName == "" {
|
||||
binaryName = filepath.Base(projectDir)
|
||||
binaryName = ax.Base(projectDir)
|
||||
}
|
||||
|
||||
// Determine output directory
|
||||
outputDir := filepath.Join(projectDir, "dist")
|
||||
outputDir := ax.Join(projectDir, "dist")
|
||||
|
||||
// Get builder (detect project type)
|
||||
projectType, err := build.PrimaryType(fs, projectDir)
|
||||
|
|
@ -296,7 +298,7 @@ func buildArtifacts(ctx context.Context, fs io.Medium, cfg *Config, projectDir,
|
|||
}
|
||||
|
||||
// Write CHECKSUMS.txt
|
||||
checksumPath := filepath.Join(outputDir, "CHECKSUMS.txt")
|
||||
checksumPath := ax.Join(outputDir, "CHECKSUMS.txt")
|
||||
if err := build.WriteChecksumFile(fs, checksummedArtifacts, checksumPath); err != nil {
|
||||
return nil, coreerr.E("release.buildArtifacts", "failed to write checksums file", err)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,26 +2,24 @@ package release
|
|||
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"dappco.re/go/core/build/internal/ax"
|
||||
"dappco.re/go/core/build/pkg/build"
|
||||
"dappco.re/go/core/io"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestFindArtifacts_Good(t *testing.T) {
|
||||
func TestRelease_FindArtifacts_Good(t *testing.T) {
|
||||
t.Run("finds tar.gz artifacts", func(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
distDir := filepath.Join(dir, "dist")
|
||||
require.NoError(t, os.MkdirAll(distDir, 0755))
|
||||
distDir := ax.Join(dir, "dist")
|
||||
require.NoError(t, ax.MkdirAll(distDir, 0755))
|
||||
|
||||
// Create test artifact files
|
||||
require.NoError(t, os.WriteFile(filepath.Join(distDir, "app-linux-amd64.tar.gz"), []byte("test"), 0644))
|
||||
require.NoError(t, os.WriteFile(filepath.Join(distDir, "app-darwin-arm64.tar.gz"), []byte("test"), 0644))
|
||||
require.NoError(t, ax.WriteFile(ax.Join(distDir, "app-linux-amd64.tar.gz"), []byte("test"), 0644))
|
||||
require.NoError(t, ax.WriteFile(ax.Join(distDir, "app-darwin-arm64.tar.gz"), []byte("test"), 0644))
|
||||
|
||||
artifacts, err := findArtifacts(io.Local, distDir)
|
||||
require.NoError(t, err)
|
||||
|
|
@ -31,10 +29,10 @@ func TestFindArtifacts_Good(t *testing.T) {
|
|||
|
||||
t.Run("finds zip artifacts", func(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
distDir := filepath.Join(dir, "dist")
|
||||
require.NoError(t, os.MkdirAll(distDir, 0755))
|
||||
distDir := ax.Join(dir, "dist")
|
||||
require.NoError(t, ax.MkdirAll(distDir, 0755))
|
||||
|
||||
require.NoError(t, os.WriteFile(filepath.Join(distDir, "app-windows-amd64.zip"), []byte("test"), 0644))
|
||||
require.NoError(t, ax.WriteFile(ax.Join(distDir, "app-windows-amd64.zip"), []byte("test"), 0644))
|
||||
|
||||
artifacts, err := findArtifacts(io.Local, distDir)
|
||||
require.NoError(t, err)
|
||||
|
|
@ -45,10 +43,10 @@ func TestFindArtifacts_Good(t *testing.T) {
|
|||
|
||||
t.Run("finds checksum files", func(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
distDir := filepath.Join(dir, "dist")
|
||||
require.NoError(t, os.MkdirAll(distDir, 0755))
|
||||
distDir := ax.Join(dir, "dist")
|
||||
require.NoError(t, ax.MkdirAll(distDir, 0755))
|
||||
|
||||
require.NoError(t, os.WriteFile(filepath.Join(distDir, "CHECKSUMS.txt"), []byte("checksums"), 0644))
|
||||
require.NoError(t, ax.WriteFile(ax.Join(distDir, "CHECKSUMS.txt"), []byte("checksums"), 0644))
|
||||
|
||||
artifacts, err := findArtifacts(io.Local, distDir)
|
||||
require.NoError(t, err)
|
||||
|
|
@ -59,10 +57,10 @@ func TestFindArtifacts_Good(t *testing.T) {
|
|||
|
||||
t.Run("finds signature files", func(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
distDir := filepath.Join(dir, "dist")
|
||||
require.NoError(t, os.MkdirAll(distDir, 0755))
|
||||
distDir := ax.Join(dir, "dist")
|
||||
require.NoError(t, ax.MkdirAll(distDir, 0755))
|
||||
|
||||
require.NoError(t, os.WriteFile(filepath.Join(distDir, "app.tar.gz.sig"), []byte("signature"), 0644))
|
||||
require.NoError(t, ax.WriteFile(ax.Join(distDir, "app.tar.gz.sig"), []byte("signature"), 0644))
|
||||
|
||||
artifacts, err := findArtifacts(io.Local, distDir)
|
||||
require.NoError(t, err)
|
||||
|
|
@ -72,13 +70,13 @@ func TestFindArtifacts_Good(t *testing.T) {
|
|||
|
||||
t.Run("finds mixed artifact types", func(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
distDir := filepath.Join(dir, "dist")
|
||||
require.NoError(t, os.MkdirAll(distDir, 0755))
|
||||
distDir := ax.Join(dir, "dist")
|
||||
require.NoError(t, ax.MkdirAll(distDir, 0755))
|
||||
|
||||
require.NoError(t, os.WriteFile(filepath.Join(distDir, "app-linux.tar.gz"), []byte("test"), 0644))
|
||||
require.NoError(t, os.WriteFile(filepath.Join(distDir, "app-windows.zip"), []byte("test"), 0644))
|
||||
require.NoError(t, os.WriteFile(filepath.Join(distDir, "CHECKSUMS.txt"), []byte("checksums"), 0644))
|
||||
require.NoError(t, os.WriteFile(filepath.Join(distDir, "app.sig"), []byte("sig"), 0644))
|
||||
require.NoError(t, ax.WriteFile(ax.Join(distDir, "app-linux.tar.gz"), []byte("test"), 0644))
|
||||
require.NoError(t, ax.WriteFile(ax.Join(distDir, "app-windows.zip"), []byte("test"), 0644))
|
||||
require.NoError(t, ax.WriteFile(ax.Join(distDir, "CHECKSUMS.txt"), []byte("checksums"), 0644))
|
||||
require.NoError(t, ax.WriteFile(ax.Join(distDir, "app.sig"), []byte("sig"), 0644))
|
||||
|
||||
artifacts, err := findArtifacts(io.Local, distDir)
|
||||
require.NoError(t, err)
|
||||
|
|
@ -88,12 +86,12 @@ func TestFindArtifacts_Good(t *testing.T) {
|
|||
|
||||
t.Run("ignores non-artifact files", func(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
distDir := filepath.Join(dir, "dist")
|
||||
require.NoError(t, os.MkdirAll(distDir, 0755))
|
||||
distDir := ax.Join(dir, "dist")
|
||||
require.NoError(t, ax.MkdirAll(distDir, 0755))
|
||||
|
||||
require.NoError(t, os.WriteFile(filepath.Join(distDir, "README.md"), []byte("readme"), 0644))
|
||||
require.NoError(t, os.WriteFile(filepath.Join(distDir, "app.exe"), []byte("binary"), 0644))
|
||||
require.NoError(t, os.WriteFile(filepath.Join(distDir, "app.tar.gz"), []byte("artifact"), 0644))
|
||||
require.NoError(t, ax.WriteFile(ax.Join(distDir, "README.md"), []byte("readme"), 0644))
|
||||
require.NoError(t, ax.WriteFile(ax.Join(distDir, "app.exe"), []byte("binary"), 0644))
|
||||
require.NoError(t, ax.WriteFile(ax.Join(distDir, "app.tar.gz"), []byte("artifact"), 0644))
|
||||
|
||||
artifacts, err := findArtifacts(io.Local, distDir)
|
||||
require.NoError(t, err)
|
||||
|
|
@ -104,12 +102,12 @@ func TestFindArtifacts_Good(t *testing.T) {
|
|||
|
||||
t.Run("ignores subdirectories", func(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
distDir := filepath.Join(dir, "dist")
|
||||
require.NoError(t, os.MkdirAll(distDir, 0755))
|
||||
require.NoError(t, os.MkdirAll(filepath.Join(distDir, "subdir"), 0755))
|
||||
distDir := ax.Join(dir, "dist")
|
||||
require.NoError(t, ax.MkdirAll(distDir, 0755))
|
||||
require.NoError(t, ax.MkdirAll(ax.Join(distDir, "subdir"), 0755))
|
||||
|
||||
require.NoError(t, os.WriteFile(filepath.Join(distDir, "app.tar.gz"), []byte("artifact"), 0644))
|
||||
require.NoError(t, os.WriteFile(filepath.Join(distDir, "subdir", "nested.tar.gz"), []byte("nested"), 0644))
|
||||
require.NoError(t, ax.WriteFile(ax.Join(distDir, "app.tar.gz"), []byte("artifact"), 0644))
|
||||
require.NoError(t, ax.WriteFile(ax.Join(distDir, "subdir", "nested.tar.gz"), []byte("nested"), 0644))
|
||||
|
||||
artifacts, err := findArtifacts(io.Local, distDir)
|
||||
require.NoError(t, err)
|
||||
|
|
@ -120,8 +118,8 @@ func TestFindArtifacts_Good(t *testing.T) {
|
|||
|
||||
t.Run("returns empty slice for empty dist directory", func(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
distDir := filepath.Join(dir, "dist")
|
||||
require.NoError(t, os.MkdirAll(distDir, 0755))
|
||||
distDir := ax.Join(dir, "dist")
|
||||
require.NoError(t, ax.MkdirAll(distDir, 0755))
|
||||
|
||||
artifacts, err := findArtifacts(io.Local, distDir)
|
||||
require.NoError(t, err)
|
||||
|
|
@ -130,10 +128,10 @@ func TestFindArtifacts_Good(t *testing.T) {
|
|||
})
|
||||
}
|
||||
|
||||
func TestFindArtifacts_Bad(t *testing.T) {
|
||||
func TestRelease_FindArtifacts_Bad(t *testing.T) {
|
||||
t.Run("returns error when dist directory does not exist", func(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
distDir := filepath.Join(dir, "dist")
|
||||
distDir := ax.Join(dir, "dist")
|
||||
|
||||
_, err := findArtifacts(io.Local, distDir)
|
||||
assert.Error(t, err)
|
||||
|
|
@ -141,17 +139,17 @@ func TestFindArtifacts_Bad(t *testing.T) {
|
|||
})
|
||||
|
||||
t.Run("returns error when dist directory is unreadable", func(t *testing.T) {
|
||||
if os.Geteuid() == 0 {
|
||||
if ax.Geteuid() == 0 {
|
||||
t.Skip("root can read any directory")
|
||||
}
|
||||
dir := t.TempDir()
|
||||
distDir := filepath.Join(dir, "dist")
|
||||
require.NoError(t, os.MkdirAll(distDir, 0755))
|
||||
distDir := ax.Join(dir, "dist")
|
||||
require.NoError(t, ax.MkdirAll(distDir, 0755))
|
||||
|
||||
// Create a file that looks like dist but will cause ReadDir to fail
|
||||
// by making the directory unreadable
|
||||
require.NoError(t, os.Chmod(distDir, 0000))
|
||||
defer func() { _ = os.Chmod(distDir, 0755) }()
|
||||
require.NoError(t, ax.Chmod(distDir, 0000))
|
||||
defer func() { _ = ax.Chmod(distDir, 0755) }()
|
||||
|
||||
_, err := findArtifacts(io.Local, distDir)
|
||||
assert.Error(t, err)
|
||||
|
|
@ -159,7 +157,7 @@ func TestFindArtifacts_Bad(t *testing.T) {
|
|||
})
|
||||
}
|
||||
|
||||
func TestGetBuilder_Good(t *testing.T) {
|
||||
func TestRelease_GetBuilder_Good(t *testing.T) {
|
||||
t.Run("returns Go builder for go project type", func(t *testing.T) {
|
||||
builder, err := getBuilder(build.ProjectTypeGo)
|
||||
require.NoError(t, err)
|
||||
|
|
@ -175,7 +173,7 @@ func TestGetBuilder_Good(t *testing.T) {
|
|||
})
|
||||
}
|
||||
|
||||
func TestGetBuilder_Bad(t *testing.T) {
|
||||
func TestRelease_GetBuilder_Bad(t *testing.T) {
|
||||
t.Run("returns error for Node project type", func(t *testing.T) {
|
||||
_, err := getBuilder(build.ProjectTypeNode)
|
||||
assert.Error(t, err)
|
||||
|
|
@ -195,7 +193,7 @@ func TestGetBuilder_Bad(t *testing.T) {
|
|||
})
|
||||
}
|
||||
|
||||
func TestGetPublisher_Good(t *testing.T) {
|
||||
func TestRelease_GetPublisher_Good(t *testing.T) {
|
||||
tests := []struct {
|
||||
pubType string
|
||||
expectedName string
|
||||
|
|
@ -220,7 +218,7 @@ func TestGetPublisher_Good(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestGetPublisher_Bad(t *testing.T) {
|
||||
func TestRelease_GetPublisher_Bad(t *testing.T) {
|
||||
t.Run("returns error for unsupported publisher type", func(t *testing.T) {
|
||||
_, err := getPublisher("unsupported")
|
||||
assert.Error(t, err)
|
||||
|
|
@ -234,7 +232,7 @@ func TestGetPublisher_Bad(t *testing.T) {
|
|||
})
|
||||
}
|
||||
|
||||
func TestBuildExtendedConfig_Good(t *testing.T) {
|
||||
func TestRelease_BuildExtendedConfig_Good(t *testing.T) {
|
||||
t.Run("returns empty map for minimal config", func(t *testing.T) {
|
||||
cfg := PublisherConfig{
|
||||
Type: "github",
|
||||
|
|
@ -371,7 +369,7 @@ func TestBuildExtendedConfig_Good(t *testing.T) {
|
|||
})
|
||||
}
|
||||
|
||||
func TestToAnySlice_Good(t *testing.T) {
|
||||
func TestRelease_ToAnySlice_Good(t *testing.T) {
|
||||
t.Run("converts string slice to any slice", func(t *testing.T) {
|
||||
input := []string{"a", "b", "c"}
|
||||
|
||||
|
|
@ -401,12 +399,12 @@ func TestToAnySlice_Good(t *testing.T) {
|
|||
})
|
||||
}
|
||||
|
||||
func TestPublish_Good(t *testing.T) {
|
||||
func TestRelease_Publish_Good(t *testing.T) {
|
||||
t.Run("returns release with version from config", func(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
distDir := filepath.Join(dir, "dist")
|
||||
require.NoError(t, os.MkdirAll(distDir, 0755))
|
||||
require.NoError(t, os.WriteFile(filepath.Join(distDir, "app.tar.gz"), []byte("test"), 0644))
|
||||
distDir := ax.Join(dir, "dist")
|
||||
require.NoError(t, ax.MkdirAll(distDir, 0755))
|
||||
require.NoError(t, ax.WriteFile(ax.Join(distDir, "app.tar.gz"), []byte("test"), 0644))
|
||||
|
||||
cfg := DefaultConfig()
|
||||
cfg.SetProjectDir(dir)
|
||||
|
|
@ -422,11 +420,11 @@ func TestPublish_Good(t *testing.T) {
|
|||
|
||||
t.Run("finds artifacts in dist directory", func(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
distDir := filepath.Join(dir, "dist")
|
||||
require.NoError(t, os.MkdirAll(distDir, 0755))
|
||||
require.NoError(t, os.WriteFile(filepath.Join(distDir, "app-linux.tar.gz"), []byte("test"), 0644))
|
||||
require.NoError(t, os.WriteFile(filepath.Join(distDir, "app-darwin.tar.gz"), []byte("test"), 0644))
|
||||
require.NoError(t, os.WriteFile(filepath.Join(distDir, "CHECKSUMS.txt"), []byte("checksums"), 0644))
|
||||
distDir := ax.Join(dir, "dist")
|
||||
require.NoError(t, ax.MkdirAll(distDir, 0755))
|
||||
require.NoError(t, ax.WriteFile(ax.Join(distDir, "app-linux.tar.gz"), []byte("test"), 0644))
|
||||
require.NoError(t, ax.WriteFile(ax.Join(distDir, "app-darwin.tar.gz"), []byte("test"), 0644))
|
||||
require.NoError(t, ax.WriteFile(ax.Join(distDir, "CHECKSUMS.txt"), []byte("checksums"), 0644))
|
||||
|
||||
cfg := DefaultConfig()
|
||||
cfg.SetProjectDir(dir)
|
||||
|
|
@ -440,7 +438,7 @@ func TestPublish_Good(t *testing.T) {
|
|||
})
|
||||
}
|
||||
|
||||
func TestPublish_Bad(t *testing.T) {
|
||||
func TestRelease_Publish_Bad(t *testing.T) {
|
||||
t.Run("returns error when config is nil", func(t *testing.T) {
|
||||
_, err := Publish(context.Background(), nil, true)
|
||||
assert.Error(t, err)
|
||||
|
|
@ -461,8 +459,8 @@ func TestPublish_Bad(t *testing.T) {
|
|||
|
||||
t.Run("returns error when no artifacts found", func(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
distDir := filepath.Join(dir, "dist")
|
||||
require.NoError(t, os.MkdirAll(distDir, 0755))
|
||||
distDir := ax.Join(dir, "dist")
|
||||
require.NoError(t, ax.MkdirAll(distDir, 0755))
|
||||
|
||||
cfg := DefaultConfig()
|
||||
cfg.SetProjectDir(dir)
|
||||
|
|
@ -475,9 +473,9 @@ func TestPublish_Bad(t *testing.T) {
|
|||
|
||||
t.Run("returns error for unsupported publisher", func(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
distDir := filepath.Join(dir, "dist")
|
||||
require.NoError(t, os.MkdirAll(distDir, 0755))
|
||||
require.NoError(t, os.WriteFile(filepath.Join(distDir, "app.tar.gz"), []byte("test"), 0644))
|
||||
distDir := ax.Join(dir, "dist")
|
||||
require.NoError(t, ax.MkdirAll(distDir, 0755))
|
||||
require.NoError(t, ax.WriteFile(ax.Join(distDir, "app.tar.gz"), []byte("test"), 0644))
|
||||
|
||||
cfg := DefaultConfig()
|
||||
cfg.SetProjectDir(dir)
|
||||
|
|
@ -493,9 +491,9 @@ func TestPublish_Bad(t *testing.T) {
|
|||
|
||||
t.Run("returns error when version determination fails in non-git dir", func(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
distDir := filepath.Join(dir, "dist")
|
||||
require.NoError(t, os.MkdirAll(distDir, 0755))
|
||||
require.NoError(t, os.WriteFile(filepath.Join(distDir, "app.tar.gz"), []byte("test"), 0644))
|
||||
distDir := ax.Join(dir, "dist")
|
||||
require.NoError(t, ax.MkdirAll(distDir, 0755))
|
||||
require.NoError(t, ax.WriteFile(ax.Join(distDir, "app.tar.gz"), []byte("test"), 0644))
|
||||
|
||||
cfg := DefaultConfig()
|
||||
cfg.SetProjectDir(dir)
|
||||
|
|
@ -510,7 +508,7 @@ func TestPublish_Bad(t *testing.T) {
|
|||
})
|
||||
}
|
||||
|
||||
func TestRun_Good(t *testing.T) {
|
||||
func TestRelease_Run_Good(t *testing.T) {
|
||||
t.Run("returns release with version from config", func(t *testing.T) {
|
||||
// Create a minimal Go project for testing
|
||||
dir := t.TempDir()
|
||||
|
|
@ -520,14 +518,14 @@ func TestRun_Good(t *testing.T) {
|
|||
|
||||
go 1.21
|
||||
`
|
||||
require.NoError(t, os.WriteFile(filepath.Join(dir, "go.mod"), []byte(goMod), 0644))
|
||||
require.NoError(t, ax.WriteFile(ax.Join(dir, "go.mod"), []byte(goMod), 0644))
|
||||
|
||||
// Create main.go
|
||||
mainGo := `package main
|
||||
|
||||
func main() {}
|
||||
`
|
||||
require.NoError(t, os.WriteFile(filepath.Join(dir, "main.go"), []byte(mainGo), 0644))
|
||||
require.NoError(t, ax.WriteFile(ax.Join(dir, "main.go"), []byte(mainGo), 0644))
|
||||
|
||||
cfg := DefaultConfig()
|
||||
cfg.SetProjectDir(dir)
|
||||
|
|
@ -548,7 +546,7 @@ func main() {}
|
|||
})
|
||||
}
|
||||
|
||||
func TestRun_Bad(t *testing.T) {
|
||||
func TestRelease_Run_Bad(t *testing.T) {
|
||||
t.Run("returns error when config is nil", func(t *testing.T) {
|
||||
_, err := Run(context.Background(), nil, true)
|
||||
assert.Error(t, err)
|
||||
|
|
@ -556,7 +554,7 @@ func TestRun_Bad(t *testing.T) {
|
|||
})
|
||||
}
|
||||
|
||||
func TestRelease_Structure(t *testing.T) {
|
||||
func TestRelease_Structure_Good(t *testing.T) {
|
||||
t.Run("Release struct holds expected fields", func(t *testing.T) {
|
||||
release := &Release{
|
||||
Version: "v1.0.0",
|
||||
|
|
@ -572,16 +570,16 @@ func TestRelease_Structure(t *testing.T) {
|
|||
})
|
||||
}
|
||||
|
||||
func TestPublish_VersionFromGit(t *testing.T) {
|
||||
func TestRelease_PublishVersionFromGit_Good(t *testing.T) {
|
||||
t.Run("determines version from git when not set", func(t *testing.T) {
|
||||
dir := setupPublishGitRepo(t)
|
||||
createPublishCommit(t, dir, "feat: initial commit")
|
||||
createPublishTag(t, dir, "v1.2.3")
|
||||
|
||||
// Create dist directory with artifact
|
||||
distDir := filepath.Join(dir, "dist")
|
||||
require.NoError(t, os.MkdirAll(distDir, 0755))
|
||||
require.NoError(t, os.WriteFile(filepath.Join(distDir, "app.tar.gz"), []byte("test"), 0644))
|
||||
distDir := ax.Join(dir, "dist")
|
||||
require.NoError(t, ax.MkdirAll(distDir, 0755))
|
||||
require.NoError(t, ax.WriteFile(ax.Join(distDir, "app.tar.gz"), []byte("test"), 0644))
|
||||
|
||||
cfg := DefaultConfig()
|
||||
cfg.SetProjectDir(dir)
|
||||
|
|
@ -595,7 +593,7 @@ func TestPublish_VersionFromGit(t *testing.T) {
|
|||
})
|
||||
}
|
||||
|
||||
func TestPublish_ChangelogGeneration(t *testing.T) {
|
||||
func TestRelease_PublishChangelogGeneration_Good(t *testing.T) {
|
||||
t.Run("generates changelog from git commits when available", func(t *testing.T) {
|
||||
dir := setupPublishGitRepo(t)
|
||||
createPublishCommit(t, dir, "feat: add feature")
|
||||
|
|
@ -604,9 +602,9 @@ func TestPublish_ChangelogGeneration(t *testing.T) {
|
|||
createPublishTag(t, dir, "v1.0.1")
|
||||
|
||||
// Create dist directory with artifact
|
||||
distDir := filepath.Join(dir, "dist")
|
||||
require.NoError(t, os.MkdirAll(distDir, 0755))
|
||||
require.NoError(t, os.WriteFile(filepath.Join(distDir, "app.tar.gz"), []byte("test"), 0644))
|
||||
distDir := ax.Join(dir, "dist")
|
||||
require.NoError(t, ax.MkdirAll(distDir, 0755))
|
||||
require.NoError(t, ax.WriteFile(ax.Join(distDir, "app.tar.gz"), []byte("test"), 0644))
|
||||
|
||||
cfg := DefaultConfig()
|
||||
cfg.SetProjectDir(dir)
|
||||
|
|
@ -622,9 +620,9 @@ func TestPublish_ChangelogGeneration(t *testing.T) {
|
|||
|
||||
t.Run("uses fallback changelog on error", func(t *testing.T) {
|
||||
dir := t.TempDir() // Not a git repo
|
||||
distDir := filepath.Join(dir, "dist")
|
||||
require.NoError(t, os.MkdirAll(distDir, 0755))
|
||||
require.NoError(t, os.WriteFile(filepath.Join(distDir, "app.tar.gz"), []byte("test"), 0644))
|
||||
distDir := ax.Join(dir, "dist")
|
||||
require.NoError(t, ax.MkdirAll(distDir, 0755))
|
||||
require.NoError(t, ax.WriteFile(ax.Join(distDir, "app.tar.gz"), []byte("test"), 0644))
|
||||
|
||||
cfg := DefaultConfig()
|
||||
cfg.SetProjectDir(dir)
|
||||
|
|
@ -639,13 +637,13 @@ func TestPublish_ChangelogGeneration(t *testing.T) {
|
|||
})
|
||||
}
|
||||
|
||||
func TestPublish_DefaultProjectDir(t *testing.T) {
|
||||
func TestRelease_PublishDefaultProjectDir_Good(t *testing.T) {
|
||||
t.Run("uses current directory when projectDir is empty", func(t *testing.T) {
|
||||
// Create artifacts in current directory's dist folder
|
||||
dir := t.TempDir()
|
||||
distDir := filepath.Join(dir, "dist")
|
||||
require.NoError(t, os.MkdirAll(distDir, 0755))
|
||||
require.NoError(t, os.WriteFile(filepath.Join(distDir, "app.tar.gz"), []byte("test"), 0644))
|
||||
distDir := ax.Join(dir, "dist")
|
||||
require.NoError(t, ax.MkdirAll(distDir, 0755))
|
||||
require.NoError(t, ax.WriteFile(ax.Join(distDir, "app.tar.gz"), []byte("test"), 0644))
|
||||
|
||||
cfg := DefaultConfig()
|
||||
cfg.SetProjectDir(dir)
|
||||
|
|
@ -664,17 +662,9 @@ func setupPublishGitRepo(t *testing.T) string {
|
|||
t.Helper()
|
||||
dir := t.TempDir()
|
||||
|
||||
cmd := exec.Command("git", "init")
|
||||
cmd.Dir = dir
|
||||
require.NoError(t, cmd.Run())
|
||||
|
||||
cmd = exec.Command("git", "config", "user.email", "test@example.com")
|
||||
cmd.Dir = dir
|
||||
require.NoError(t, cmd.Run())
|
||||
|
||||
cmd = exec.Command("git", "config", "user.name", "Test User")
|
||||
cmd.Dir = dir
|
||||
require.NoError(t, cmd.Run())
|
||||
runGit(t, dir, "init")
|
||||
runGit(t, dir, "config", "user.email", "test@example.com")
|
||||
runGit(t, dir, "config", "user.name", "Test User")
|
||||
|
||||
return dir
|
||||
}
|
||||
|
|
@ -682,23 +672,16 @@ func setupPublishGitRepo(t *testing.T) string {
|
|||
func createPublishCommit(t *testing.T, dir, message string) {
|
||||
t.Helper()
|
||||
|
||||
filePath := filepath.Join(dir, "publish_test.txt")
|
||||
content, _ := os.ReadFile(filePath)
|
||||
filePath := ax.Join(dir, "publish_test.txt")
|
||||
content, _ := ax.ReadFile(filePath)
|
||||
content = append(content, []byte(message+"\n")...)
|
||||
require.NoError(t, os.WriteFile(filePath, content, 0644))
|
||||
require.NoError(t, ax.WriteFile(filePath, content, 0644))
|
||||
|
||||
cmd := exec.Command("git", "add", ".")
|
||||
cmd.Dir = dir
|
||||
require.NoError(t, cmd.Run())
|
||||
|
||||
cmd = exec.Command("git", "commit", "-m", message)
|
||||
cmd.Dir = dir
|
||||
require.NoError(t, cmd.Run())
|
||||
runGit(t, dir, "add", ".")
|
||||
runGit(t, dir, "commit", "-m", message)
|
||||
}
|
||||
|
||||
func createPublishTag(t *testing.T, dir, tag string) {
|
||||
t.Helper()
|
||||
cmd := exec.Command("git", "tag", tag)
|
||||
cmd.Dir = dir
|
||||
require.NoError(t, cmd.Run())
|
||||
runGit(t, dir, "tag", tag)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,13 +3,14 @@ package release
|
|||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"dappco.re/go/core"
|
||||
"dappco.re/go/core/build/pkg/sdk"
|
||||
coreerr "dappco.re/go/core/log"
|
||||
)
|
||||
|
||||
// SDKRelease holds the result of an SDK release.
|
||||
// Usage example: declare a value of type release.SDKRelease in integrating code.
|
||||
type SDKRelease struct {
|
||||
// Version is the SDK version.
|
||||
Version string
|
||||
|
|
@ -21,6 +22,7 @@ type SDKRelease struct {
|
|||
|
||||
// RunSDK executes SDK-only release: diff check + generate.
|
||||
// If dryRun is true, it shows what would be done without generating.
|
||||
// Usage example: call release.RunSDK(...) from integrating code.
|
||||
func RunSDK(ctx context.Context, cfg *Config, dryRun bool) (*SDKRelease, error) {
|
||||
if cfg == nil {
|
||||
return nil, coreerr.E("release.RunSDK", "config is nil", nil)
|
||||
|
|
@ -49,12 +51,12 @@ func RunSDK(ctx context.Context, cfg *Config, dryRun bool) (*SDKRelease, error)
|
|||
breaking, err := checkBreakingChanges(projectDir, cfg.SDK)
|
||||
if err != nil {
|
||||
// Non-fatal: warn and continue
|
||||
fmt.Printf("Warning: diff check failed: %v\n", err)
|
||||
core.Print(nil, "Warning: diff check failed: %v", err)
|
||||
} else if breaking {
|
||||
if cfg.SDK.Diff.FailOnBreaking {
|
||||
return nil, coreerr.E("release.RunSDK", "breaking API changes detected", nil)
|
||||
}
|
||||
fmt.Printf("Warning: breaking API changes detected\n")
|
||||
core.Print(nil, "Warning: breaking API changes detected")
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -8,13 +8,13 @@ import (
|
|||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestRunSDK_Bad_NilConfig(t *testing.T) {
|
||||
func TestSDK_RunSDKNilConfig_Bad(t *testing.T) {
|
||||
_, err := RunSDK(context.Background(), nil, true)
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "config is nil")
|
||||
}
|
||||
|
||||
func TestRunSDK_Bad_NoSDKConfig(t *testing.T) {
|
||||
func TestSDK_RunSDKNoSDKConfig_Bad(t *testing.T) {
|
||||
cfg := &Config{
|
||||
SDK: nil,
|
||||
}
|
||||
|
|
@ -25,7 +25,7 @@ func TestRunSDK_Bad_NoSDKConfig(t *testing.T) {
|
|||
assert.Contains(t, err.Error(), "sdk not configured")
|
||||
}
|
||||
|
||||
func TestRunSDK_Good_DryRun(t *testing.T) {
|
||||
func TestSDK_RunSDKDryRun_Good(t *testing.T) {
|
||||
cfg := &Config{
|
||||
SDK: &SDKConfig{
|
||||
Languages: []string{"typescript", "python"},
|
||||
|
|
@ -45,7 +45,7 @@ func TestRunSDK_Good_DryRun(t *testing.T) {
|
|||
assert.Equal(t, "sdk", result.Output)
|
||||
}
|
||||
|
||||
func TestRunSDK_Good_DryRunDefaultOutput(t *testing.T) {
|
||||
func TestSDK_RunSDKDryRunDefaultOutput_Good(t *testing.T) {
|
||||
cfg := &Config{
|
||||
SDK: &SDKConfig{
|
||||
Languages: []string{"go"},
|
||||
|
|
@ -61,7 +61,7 @@ func TestRunSDK_Good_DryRunDefaultOutput(t *testing.T) {
|
|||
assert.Equal(t, "sdk", result.Output)
|
||||
}
|
||||
|
||||
func TestRunSDK_Good_DryRunDefaultProjectDir(t *testing.T) {
|
||||
func TestSDK_RunSDKDryRunDefaultProjectDir_Good(t *testing.T) {
|
||||
cfg := &Config{
|
||||
SDK: &SDKConfig{
|
||||
Languages: []string{"typescript"},
|
||||
|
|
@ -77,7 +77,7 @@ func TestRunSDK_Good_DryRunDefaultProjectDir(t *testing.T) {
|
|||
assert.Equal(t, "v1.0.0", result.Version)
|
||||
}
|
||||
|
||||
func TestRunSDK_Bad_BreakingChangesFailOnBreaking(t *testing.T) {
|
||||
func TestSDK_RunSDKBreakingChangesFailOnBreaking_Bad(t *testing.T) {
|
||||
// This test verifies that when diff.FailOnBreaking is true and breaking changes
|
||||
// are detected, RunSDK returns an error. However, since we can't easily mock
|
||||
// the diff check, this test verifies the config is correctly processed.
|
||||
|
|
@ -102,7 +102,7 @@ func TestRunSDK_Bad_BreakingChangesFailOnBreaking(t *testing.T) {
|
|||
assert.Equal(t, "v1.0.0", result.Version)
|
||||
}
|
||||
|
||||
func TestToSDKConfig_Good(t *testing.T) {
|
||||
func TestSDK_ToSDKConfig_Good(t *testing.T) {
|
||||
sdkCfg := &SDKConfig{
|
||||
Spec: "api/openapi.yaml",
|
||||
Languages: []string{"typescript", "go"},
|
||||
|
|
@ -128,12 +128,12 @@ func TestToSDKConfig_Good(t *testing.T) {
|
|||
assert.True(t, result.Diff.FailOnBreaking)
|
||||
}
|
||||
|
||||
func TestToSDKConfig_Good_NilInput(t *testing.T) {
|
||||
func TestSDK_ToSDKConfigNilInput_Good(t *testing.T) {
|
||||
result := toSDKConfig(nil)
|
||||
assert.Nil(t, result)
|
||||
}
|
||||
|
||||
func TestRunSDK_Good_WithDiffEnabledNoFailOnBreaking(t *testing.T) {
|
||||
func TestSDK_RunSDKWithDiffEnabledNoFailOnBreaking_Good(t *testing.T) {
|
||||
// Tests diff enabled but FailOnBreaking=false (should warn but not fail)
|
||||
cfg := &Config{
|
||||
SDK: &SDKConfig{
|
||||
|
|
@ -155,7 +155,7 @@ func TestRunSDK_Good_WithDiffEnabledNoFailOnBreaking(t *testing.T) {
|
|||
assert.Contains(t, result.Languages, "typescript")
|
||||
}
|
||||
|
||||
func TestRunSDK_Good_MultipleLanguages(t *testing.T) {
|
||||
func TestSDK_RunSDKMultipleLanguages_Good(t *testing.T) {
|
||||
// Tests multiple language support
|
||||
cfg := &Config{
|
||||
SDK: &SDKConfig{
|
||||
|
|
@ -174,7 +174,7 @@ func TestRunSDK_Good_MultipleLanguages(t *testing.T) {
|
|||
assert.Equal(t, "multi-sdk", result.Output)
|
||||
}
|
||||
|
||||
func TestRunSDK_Good_WithPackageConfig(t *testing.T) {
|
||||
func TestSDK_RunSDKWithPackageConfig_Good(t *testing.T) {
|
||||
// Tests that package config is properly handled
|
||||
cfg := &Config{
|
||||
SDK: &SDKConfig{
|
||||
|
|
@ -195,7 +195,7 @@ func TestRunSDK_Good_WithPackageConfig(t *testing.T) {
|
|||
assert.Equal(t, "v1.0.0", result.Version)
|
||||
}
|
||||
|
||||
func TestToSDKConfig_Good_EmptyPackageConfig(t *testing.T) {
|
||||
func TestSDK_ToSDKConfigEmptyPackageConfig_Good(t *testing.T) {
|
||||
// Tests conversion with empty package config
|
||||
sdkCfg := &SDKConfig{
|
||||
Languages: []string{"go"},
|
||||
|
|
@ -211,7 +211,7 @@ func TestToSDKConfig_Good_EmptyPackageConfig(t *testing.T) {
|
|||
assert.Empty(t, result.Package.Version)
|
||||
}
|
||||
|
||||
func TestToSDKConfig_Good_DiffDisabled(t *testing.T) {
|
||||
func TestSDK_ToSDKConfigDiffDisabled_Good(t *testing.T) {
|
||||
// Tests conversion with diff disabled
|
||||
sdkCfg := &SDKConfig{
|
||||
Languages: []string{"typescript"},
|
||||
|
|
|
|||
14
pkg/release/test_helpers_test.go
Normal file
14
pkg/release/test_helpers_test.go
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
package release
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"dappco.re/go/core/build/internal/ax"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func runGit(t *testing.T, dir string, args ...string) {
|
||||
t.Helper()
|
||||
require.NoError(t, ax.ExecDir(context.Background(), dir, "git", args...))
|
||||
}
|
||||
|
|
@ -2,12 +2,12 @@
|
|||
package release
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os/exec"
|
||||
"context"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"dappco.re/go/core"
|
||||
"dappco.re/go/core/build/internal/ax"
|
||||
coreerr "dappco.re/go/core/log"
|
||||
)
|
||||
|
||||
|
|
@ -19,6 +19,8 @@ var semverRegex = regexp.MustCompile(`^v?(\d+)\.(\d+)\.(\d+)(?:-([a-zA-Z0-9.-]+)
|
|||
// 1. Git tag on HEAD
|
||||
// 2. Most recent tag + increment patch
|
||||
// 3. Default to v0.0.1 if no tags exist
|
||||
//
|
||||
// Usage example: call release.DetermineVersion(...) from integrating code.
|
||||
func DetermineVersion(dir string) (string, error) {
|
||||
// Check if HEAD has a tag
|
||||
headTag, err := getTagOnHead(dir)
|
||||
|
|
@ -42,6 +44,8 @@ func DetermineVersion(dir string) (string, error) {
|
|||
// - "v1.2.3" -> "v1.2.4"
|
||||
// - "1.2.3" -> "v1.2.4"
|
||||
// - "v1.2.3-alpha" -> "v1.2.4" (strips prerelease)
|
||||
//
|
||||
// Usage example: call release.IncrementVersion(...) from integrating code.
|
||||
func IncrementVersion(current string) string {
|
||||
matches := semverRegex.FindStringSubmatch(current)
|
||||
if matches == nil {
|
||||
|
|
@ -56,13 +60,15 @@ func IncrementVersion(current string) string {
|
|||
// Increment patch
|
||||
patch++
|
||||
|
||||
return fmt.Sprintf("v%d.%d.%d", major, minor, patch)
|
||||
return core.Sprintf("v%d.%d.%d", major, minor, patch)
|
||||
}
|
||||
|
||||
// IncrementMinor increments the minor version of a semver string.
|
||||
// Examples:
|
||||
// - "v1.2.3" -> "v1.3.0"
|
||||
// - "1.2.3" -> "v1.3.0"
|
||||
//
|
||||
// Usage example: call release.IncrementMinor(...) from integrating code.
|
||||
func IncrementMinor(current string) string {
|
||||
matches := semverRegex.FindStringSubmatch(current)
|
||||
if matches == nil {
|
||||
|
|
@ -75,13 +81,15 @@ func IncrementMinor(current string) string {
|
|||
// Increment minor, reset patch
|
||||
minor++
|
||||
|
||||
return fmt.Sprintf("v%d.%d.0", major, minor)
|
||||
return core.Sprintf("v%d.%d.0", major, minor)
|
||||
}
|
||||
|
||||
// IncrementMajor increments the major version of a semver string.
|
||||
// Examples:
|
||||
// - "v1.2.3" -> "v2.0.0"
|
||||
// - "1.2.3" -> "v2.0.0"
|
||||
//
|
||||
// Usage example: call release.IncrementMajor(...) from integrating code.
|
||||
func IncrementMajor(current string) string {
|
||||
matches := semverRegex.FindStringSubmatch(current)
|
||||
if matches == nil {
|
||||
|
|
@ -93,11 +101,12 @@ func IncrementMajor(current string) string {
|
|||
// Increment major, reset minor and patch
|
||||
major++
|
||||
|
||||
return fmt.Sprintf("v%d.0.0", major)
|
||||
return core.Sprintf("v%d.0.0", major)
|
||||
}
|
||||
|
||||
// ParseVersion parses a semver string into its components.
|
||||
// Returns (major, minor, patch, prerelease, build, error).
|
||||
// Usage example: call release.ParseVersion(...) from integrating code.
|
||||
func ParseVersion(version string) (int, int, int, string, string, error) {
|
||||
matches := semverRegex.FindStringSubmatch(version)
|
||||
if matches == nil {
|
||||
|
|
@ -114,13 +123,14 @@ func ParseVersion(version string) (int, int, int, string, string, error) {
|
|||
}
|
||||
|
||||
// ValidateVersion checks if a string is a valid semver.
|
||||
// Usage example: call release.ValidateVersion(...) from integrating code.
|
||||
func ValidateVersion(version string) bool {
|
||||
return semverRegex.MatchString(version)
|
||||
}
|
||||
|
||||
// normalizeVersion ensures the version starts with 'v'.
|
||||
func normalizeVersion(version string) string {
|
||||
if !strings.HasPrefix(version, "v") {
|
||||
if !core.HasPrefix(version, "v") {
|
||||
return "v" + version
|
||||
}
|
||||
return version
|
||||
|
|
@ -128,24 +138,20 @@ func normalizeVersion(version string) string {
|
|||
|
||||
// getTagOnHead returns the tag on HEAD, if any.
|
||||
func getTagOnHead(dir string) (string, error) {
|
||||
cmd := exec.Command("git", "describe", "--tags", "--exact-match", "HEAD")
|
||||
cmd.Dir = dir
|
||||
output, err := cmd.Output()
|
||||
output, err := ax.RunDir(context.Background(), dir, "git", "describe", "--tags", "--exact-match", "HEAD")
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return strings.TrimSpace(string(output)), nil
|
||||
return core.Trim(output), nil
|
||||
}
|
||||
|
||||
// getLatestTag returns the most recent tag in the repository.
|
||||
func getLatestTag(dir string) (string, error) {
|
||||
cmd := exec.Command("git", "describe", "--tags", "--abbrev=0")
|
||||
cmd.Dir = dir
|
||||
output, err := cmd.Output()
|
||||
output, err := ax.RunDir(context.Background(), dir, "git", "describe", "--tags", "--abbrev=0")
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return strings.TrimSpace(string(output)), nil
|
||||
return core.Trim(output), nil
|
||||
}
|
||||
|
||||
// CompareVersions compares two semver strings.
|
||||
|
|
@ -154,13 +160,22 @@ func getLatestTag(dir string) (string, error) {
|
|||
// -1 if a < b
|
||||
// 0 if a == b
|
||||
// 1 if a > b
|
||||
//
|
||||
// Usage example: call release.CompareVersions(...) from integrating code.
|
||||
func CompareVersions(a, b string) int {
|
||||
aMajor, aMinor, aPatch, _, _, errA := ParseVersion(a)
|
||||
bMajor, bMinor, bPatch, _, _, errB := ParseVersion(b)
|
||||
|
||||
// Invalid versions are considered less than valid ones
|
||||
if errA != nil && errB != nil {
|
||||
return strings.Compare(a, b)
|
||||
switch {
|
||||
case a < b:
|
||||
return -1
|
||||
case a > b:
|
||||
return 1
|
||||
default:
|
||||
return 0
|
||||
}
|
||||
}
|
||||
if errA != nil {
|
||||
return -1
|
||||
|
|
|
|||
|
|
@ -1,11 +1,10 @@
|
|||
package release
|
||||
|
||||
import (
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"dappco.re/go/core/build/internal/ax"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
|
@ -16,18 +15,11 @@ func setupGitRepo(t *testing.T) string {
|
|||
dir := t.TempDir()
|
||||
|
||||
// Initialize git repo
|
||||
cmd := exec.Command("git", "init")
|
||||
cmd.Dir = dir
|
||||
require.NoError(t, cmd.Run())
|
||||
runGit(t, dir, "init")
|
||||
|
||||
// Configure git user for commits
|
||||
cmd = exec.Command("git", "config", "user.email", "test@example.com")
|
||||
cmd.Dir = dir
|
||||
require.NoError(t, cmd.Run())
|
||||
|
||||
cmd = exec.Command("git", "config", "user.name", "Test User")
|
||||
cmd.Dir = dir
|
||||
require.NoError(t, cmd.Run())
|
||||
runGit(t, dir, "config", "user.email", "test@example.com")
|
||||
runGit(t, dir, "config", "user.name", "Test User")
|
||||
|
||||
return dir
|
||||
}
|
||||
|
|
@ -37,30 +29,23 @@ func createCommit(t *testing.T, dir, message string) {
|
|||
t.Helper()
|
||||
|
||||
// Create or modify a file
|
||||
filePath := filepath.Join(dir, "test.txt")
|
||||
content, _ := os.ReadFile(filePath)
|
||||
filePath := ax.Join(dir, "test.txt")
|
||||
content, _ := ax.ReadFile(filePath)
|
||||
content = append(content, []byte(message+"\n")...)
|
||||
require.NoError(t, os.WriteFile(filePath, content, 0644))
|
||||
require.NoError(t, ax.WriteFile(filePath, content, 0644))
|
||||
|
||||
// Stage and commit
|
||||
cmd := exec.Command("git", "add", ".")
|
||||
cmd.Dir = dir
|
||||
require.NoError(t, cmd.Run())
|
||||
|
||||
cmd = exec.Command("git", "commit", "-m", message)
|
||||
cmd.Dir = dir
|
||||
require.NoError(t, cmd.Run())
|
||||
runGit(t, dir, "add", ".")
|
||||
runGit(t, dir, "commit", "-m", message)
|
||||
}
|
||||
|
||||
// createTag creates a tag in the given directory.
|
||||
func createTag(t *testing.T, dir, tag string) {
|
||||
t.Helper()
|
||||
cmd := exec.Command("git", "tag", tag)
|
||||
cmd.Dir = dir
|
||||
require.NoError(t, cmd.Run())
|
||||
runGit(t, dir, "tag", tag)
|
||||
}
|
||||
|
||||
func TestDetermineVersion_Good(t *testing.T) {
|
||||
func TestVersion_DetermineVersion_Good(t *testing.T) {
|
||||
t.Run("returns tag when HEAD has tag", func(t *testing.T) {
|
||||
dir := setupGitRepo(t)
|
||||
createCommit(t, dir, "feat: initial commit")
|
||||
|
|
@ -115,7 +100,7 @@ func TestDetermineVersion_Good(t *testing.T) {
|
|||
})
|
||||
}
|
||||
|
||||
func TestDetermineVersion_Bad(t *testing.T) {
|
||||
func TestVersion_DetermineVersion_Bad(t *testing.T) {
|
||||
t.Run("returns v0.0.1 for empty repo", func(t *testing.T) {
|
||||
dir := setupGitRepo(t)
|
||||
|
||||
|
|
@ -126,7 +111,7 @@ func TestDetermineVersion_Bad(t *testing.T) {
|
|||
})
|
||||
}
|
||||
|
||||
func TestGetTagOnHead_Good(t *testing.T) {
|
||||
func TestVersion_GetTagOnHead_Good(t *testing.T) {
|
||||
t.Run("returns tag when HEAD has tag", func(t *testing.T) {
|
||||
dir := setupGitRepo(t)
|
||||
createCommit(t, dir, "feat: initial commit")
|
||||
|
|
@ -150,7 +135,7 @@ func TestGetTagOnHead_Good(t *testing.T) {
|
|||
})
|
||||
}
|
||||
|
||||
func TestGetTagOnHead_Bad(t *testing.T) {
|
||||
func TestVersion_GetTagOnHead_Bad(t *testing.T) {
|
||||
t.Run("returns error when HEAD has no tag", func(t *testing.T) {
|
||||
dir := setupGitRepo(t)
|
||||
createCommit(t, dir, "feat: initial commit")
|
||||
|
|
@ -170,7 +155,7 @@ func TestGetTagOnHead_Bad(t *testing.T) {
|
|||
})
|
||||
}
|
||||
|
||||
func TestGetLatestTag_Good(t *testing.T) {
|
||||
func TestVersion_GetLatestTag_Good(t *testing.T) {
|
||||
t.Run("returns latest tag", func(t *testing.T) {
|
||||
dir := setupGitRepo(t)
|
||||
createCommit(t, dir, "feat: initial commit")
|
||||
|
|
@ -195,7 +180,7 @@ func TestGetLatestTag_Good(t *testing.T) {
|
|||
})
|
||||
}
|
||||
|
||||
func TestGetLatestTag_Bad(t *testing.T) {
|
||||
func TestVersion_GetLatestTag_Bad(t *testing.T) {
|
||||
t.Run("returns error when no tags exist", func(t *testing.T) {
|
||||
dir := setupGitRepo(t)
|
||||
createCommit(t, dir, "feat: initial commit")
|
||||
|
|
@ -212,21 +197,21 @@ func TestGetLatestTag_Bad(t *testing.T) {
|
|||
})
|
||||
}
|
||||
|
||||
func TestIncrementMinor_Bad(t *testing.T) {
|
||||
func TestVersion_IncrementMinor_Bad(t *testing.T) {
|
||||
t.Run("returns fallback for invalid version", func(t *testing.T) {
|
||||
result := IncrementMinor("not-valid")
|
||||
assert.Equal(t, "not-valid.1", result)
|
||||
})
|
||||
}
|
||||
|
||||
func TestIncrementMajor_Bad(t *testing.T) {
|
||||
func TestVersion_IncrementMajor_Bad(t *testing.T) {
|
||||
t.Run("returns fallback for invalid version", func(t *testing.T) {
|
||||
result := IncrementMajor("not-valid")
|
||||
assert.Equal(t, "not-valid.1", result)
|
||||
})
|
||||
}
|
||||
|
||||
func TestCompareVersions_Ugly(t *testing.T) {
|
||||
func TestVersion_CompareVersions_Ugly(t *testing.T) {
|
||||
t.Run("handles both invalid versions", func(t *testing.T) {
|
||||
result := CompareVersions("invalid-a", "invalid-b")
|
||||
// Should do string comparison for invalid versions
|
||||
|
|
@ -244,7 +229,7 @@ func TestCompareVersions_Ugly(t *testing.T) {
|
|||
})
|
||||
}
|
||||
|
||||
func TestIncrementVersion_Good(t *testing.T) {
|
||||
func TestVersion_IncrementVersion_Good(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
input string
|
||||
|
|
@ -295,14 +280,14 @@ func TestIncrementVersion_Good(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestIncrementVersion_Bad(t *testing.T) {
|
||||
func TestVersion_IncrementVersion_Bad(t *testing.T) {
|
||||
t.Run("invalid semver returns original with suffix", func(t *testing.T) {
|
||||
result := IncrementVersion("not-a-version")
|
||||
assert.Equal(t, "not-a-version.1", result)
|
||||
})
|
||||
}
|
||||
|
||||
func TestIncrementMinor_Good(t *testing.T) {
|
||||
func TestVersion_IncrementMinor_Good(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
input string
|
||||
|
|
@ -333,7 +318,7 @@ func TestIncrementMinor_Good(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestIncrementMajor_Good(t *testing.T) {
|
||||
func TestVersion_IncrementMajor_Good(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
input string
|
||||
|
|
@ -364,7 +349,7 @@ func TestIncrementMajor_Good(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestParseVersion_Good(t *testing.T) {
|
||||
func TestVersion_ParseVersion_Good(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
input string
|
||||
|
|
@ -418,7 +403,7 @@ func TestParseVersion_Good(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestParseVersion_Bad(t *testing.T) {
|
||||
func TestVersion_ParseVersion_Bad(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
input string
|
||||
|
|
@ -438,7 +423,7 @@ func TestParseVersion_Bad(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestValidateVersion_Good(t *testing.T) {
|
||||
func TestVersion_ValidateVersion_Good(t *testing.T) {
|
||||
validVersions := []string{
|
||||
"v1.0.0",
|
||||
"1.0.0",
|
||||
|
|
@ -456,7 +441,7 @@ func TestValidateVersion_Good(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestValidateVersion_Bad(t *testing.T) {
|
||||
func TestVersion_ValidateVersion_Bad(t *testing.T) {
|
||||
invalidVersions := []string{
|
||||
"",
|
||||
"v1",
|
||||
|
|
@ -474,7 +459,7 @@ func TestValidateVersion_Bad(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestCompareVersions_Good(t *testing.T) {
|
||||
func TestVersion_CompareVersions_Good(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
a string
|
||||
|
|
@ -500,7 +485,7 @@ func TestCompareVersions_Good(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestNormalizeVersion_Good(t *testing.T) {
|
||||
func TestVersion_NormalizeVersion_Good(t *testing.T) {
|
||||
tests := []struct {
|
||||
input string
|
||||
expected string
|
||||
|
|
|
|||
|
|
@ -1,17 +1,17 @@
|
|||
package sdk
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"dappco.re/go/core/build/internal/ax"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
// --- Breaking Change Detection Tests (oasdiff integration) ---
|
||||
|
||||
func TestDiff_Good_AddEndpoint_NonBreaking(t *testing.T) {
|
||||
func TestBreaking_DiffAddEndpointNonBreaking_Good(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
|
||||
base := `openapi: "3.0.0"
|
||||
|
|
@ -50,10 +50,10 @@ paths:
|
|||
"200":
|
||||
description: OK
|
||||
`
|
||||
basePath := filepath.Join(tmpDir, "base.yaml")
|
||||
revPath := filepath.Join(tmpDir, "rev.yaml")
|
||||
require.NoError(t, os.WriteFile(basePath, []byte(base), 0644))
|
||||
require.NoError(t, os.WriteFile(revPath, []byte(revision), 0644))
|
||||
basePath := ax.Join(tmpDir, "base.yaml")
|
||||
revPath := ax.Join(tmpDir, "rev.yaml")
|
||||
require.NoError(t, ax.WriteFile(basePath, []byte(base), 0644))
|
||||
require.NoError(t, ax.WriteFile(revPath, []byte(revision), 0644))
|
||||
|
||||
result, err := Diff(basePath, revPath)
|
||||
require.NoError(t, err)
|
||||
|
|
@ -62,7 +62,7 @@ paths:
|
|||
assert.Equal(t, "No breaking changes", result.Summary)
|
||||
}
|
||||
|
||||
func TestDiff_Good_RemoveEndpoint_Breaking(t *testing.T) {
|
||||
func TestBreaking_DiffRemoveEndpointBreaking_Good(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
|
||||
base := `openapi: "3.0.0"
|
||||
|
|
@ -101,10 +101,10 @@ paths:
|
|||
"200":
|
||||
description: OK
|
||||
`
|
||||
basePath := filepath.Join(tmpDir, "base.yaml")
|
||||
revPath := filepath.Join(tmpDir, "rev.yaml")
|
||||
require.NoError(t, os.WriteFile(basePath, []byte(base), 0644))
|
||||
require.NoError(t, os.WriteFile(revPath, []byte(revision), 0644))
|
||||
basePath := ax.Join(tmpDir, "base.yaml")
|
||||
revPath := ax.Join(tmpDir, "rev.yaml")
|
||||
require.NoError(t, ax.WriteFile(basePath, []byte(base), 0644))
|
||||
require.NoError(t, ax.WriteFile(revPath, []byte(revision), 0644))
|
||||
|
||||
result, err := Diff(basePath, revPath)
|
||||
require.NoError(t, err)
|
||||
|
|
@ -113,7 +113,7 @@ paths:
|
|||
assert.Contains(t, result.Summary, "breaking change")
|
||||
}
|
||||
|
||||
func TestDiff_Good_AddRequiredParam_Breaking(t *testing.T) {
|
||||
func TestBreaking_DiffAddRequiredParamBreaking_Good(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
|
||||
base := `openapi: "3.0.0"
|
||||
|
|
@ -146,10 +146,10 @@ paths:
|
|||
"200":
|
||||
description: OK
|
||||
`
|
||||
basePath := filepath.Join(tmpDir, "base.yaml")
|
||||
revPath := filepath.Join(tmpDir, "rev.yaml")
|
||||
require.NoError(t, os.WriteFile(basePath, []byte(base), 0644))
|
||||
require.NoError(t, os.WriteFile(revPath, []byte(revision), 0644))
|
||||
basePath := ax.Join(tmpDir, "base.yaml")
|
||||
revPath := ax.Join(tmpDir, "rev.yaml")
|
||||
require.NoError(t, ax.WriteFile(basePath, []byte(base), 0644))
|
||||
require.NoError(t, ax.WriteFile(revPath, []byte(revision), 0644))
|
||||
|
||||
result, err := Diff(basePath, revPath)
|
||||
require.NoError(t, err)
|
||||
|
|
@ -157,7 +157,7 @@ paths:
|
|||
assert.NotEmpty(t, result.Changes)
|
||||
}
|
||||
|
||||
func TestDiff_Good_AddOptionalParam_NonBreaking(t *testing.T) {
|
||||
func TestBreaking_DiffAddOptionalParamNonBreaking_Good(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
|
||||
base := `openapi: "3.0.0"
|
||||
|
|
@ -190,17 +190,17 @@ paths:
|
|||
"200":
|
||||
description: OK
|
||||
`
|
||||
basePath := filepath.Join(tmpDir, "base.yaml")
|
||||
revPath := filepath.Join(tmpDir, "rev.yaml")
|
||||
require.NoError(t, os.WriteFile(basePath, []byte(base), 0644))
|
||||
require.NoError(t, os.WriteFile(revPath, []byte(revision), 0644))
|
||||
basePath := ax.Join(tmpDir, "base.yaml")
|
||||
revPath := ax.Join(tmpDir, "rev.yaml")
|
||||
require.NoError(t, ax.WriteFile(basePath, []byte(base), 0644))
|
||||
require.NoError(t, ax.WriteFile(revPath, []byte(revision), 0644))
|
||||
|
||||
result, err := Diff(basePath, revPath)
|
||||
require.NoError(t, err)
|
||||
assert.False(t, result.Breaking, "adding optional parameter should not be breaking")
|
||||
}
|
||||
|
||||
func TestDiff_Good_ChangeResponseType_Breaking(t *testing.T) {
|
||||
func TestBreaking_DiffChangeResponseTypeBreaking_Good(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
|
||||
base := `openapi: "3.0.0"
|
||||
|
|
@ -252,17 +252,17 @@ paths:
|
|||
name:
|
||||
type: string
|
||||
`
|
||||
basePath := filepath.Join(tmpDir, "base.yaml")
|
||||
revPath := filepath.Join(tmpDir, "rev.yaml")
|
||||
require.NoError(t, os.WriteFile(basePath, []byte(base), 0644))
|
||||
require.NoError(t, os.WriteFile(revPath, []byte(revision), 0644))
|
||||
basePath := ax.Join(tmpDir, "base.yaml")
|
||||
revPath := ax.Join(tmpDir, "rev.yaml")
|
||||
require.NoError(t, ax.WriteFile(basePath, []byte(base), 0644))
|
||||
require.NoError(t, ax.WriteFile(revPath, []byte(revision), 0644))
|
||||
|
||||
result, err := Diff(basePath, revPath)
|
||||
require.NoError(t, err)
|
||||
assert.True(t, result.Breaking, "changing response schema type should be breaking")
|
||||
}
|
||||
|
||||
func TestDiff_Good_RemoveHTTPMethod_Breaking(t *testing.T) {
|
||||
func TestBreaking_DiffRemoveHTTPMethodBreaking_Good(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
|
||||
base := `openapi: "3.0.0"
|
||||
|
|
@ -294,10 +294,10 @@ paths:
|
|||
"200":
|
||||
description: OK
|
||||
`
|
||||
basePath := filepath.Join(tmpDir, "base.yaml")
|
||||
revPath := filepath.Join(tmpDir, "rev.yaml")
|
||||
require.NoError(t, os.WriteFile(basePath, []byte(base), 0644))
|
||||
require.NoError(t, os.WriteFile(revPath, []byte(revision), 0644))
|
||||
basePath := ax.Join(tmpDir, "base.yaml")
|
||||
revPath := ax.Join(tmpDir, "rev.yaml")
|
||||
require.NoError(t, ax.WriteFile(basePath, []byte(base), 0644))
|
||||
require.NoError(t, ax.WriteFile(revPath, []byte(revision), 0644))
|
||||
|
||||
result, err := Diff(basePath, revPath)
|
||||
require.NoError(t, err)
|
||||
|
|
@ -305,7 +305,7 @@ paths:
|
|||
assert.NotEmpty(t, result.Changes)
|
||||
}
|
||||
|
||||
func TestDiff_Good_IdenticalSpecs_NonBreaking(t *testing.T) {
|
||||
func TestBreaking_DiffIdenticalSpecsNonBreaking_Good(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
|
||||
spec := `openapi: "3.0.0"
|
||||
|
|
@ -331,10 +331,10 @@ paths:
|
|||
"201":
|
||||
description: Created
|
||||
`
|
||||
basePath := filepath.Join(tmpDir, "base.yaml")
|
||||
revPath := filepath.Join(tmpDir, "rev.yaml")
|
||||
require.NoError(t, os.WriteFile(basePath, []byte(spec), 0644))
|
||||
require.NoError(t, os.WriteFile(revPath, []byte(spec), 0644))
|
||||
basePath := ax.Join(tmpDir, "base.yaml")
|
||||
revPath := ax.Join(tmpDir, "rev.yaml")
|
||||
require.NoError(t, ax.WriteFile(basePath, []byte(spec), 0644))
|
||||
require.NoError(t, ax.WriteFile(revPath, []byte(spec), 0644))
|
||||
|
||||
result, err := Diff(basePath, revPath)
|
||||
require.NoError(t, err)
|
||||
|
|
@ -345,45 +345,45 @@ paths:
|
|||
|
||||
// --- Error Handling Tests ---
|
||||
|
||||
func TestDiff_Bad_NonExistentBase(t *testing.T) {
|
||||
func TestBreaking_DiffNonExistentBase_Bad(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
|
||||
revPath := filepath.Join(tmpDir, "rev.yaml")
|
||||
require.NoError(t, os.WriteFile(revPath, []byte(`openapi: "3.0.0"
|
||||
revPath := ax.Join(tmpDir, "rev.yaml")
|
||||
require.NoError(t, ax.WriteFile(revPath, []byte(`openapi: "3.0.0"
|
||||
info:
|
||||
title: Test API
|
||||
version: "1.0.0"
|
||||
paths: {}
|
||||
`), 0644))
|
||||
|
||||
_, err := Diff(filepath.Join(tmpDir, "nonexistent.yaml"), revPath)
|
||||
_, err := Diff(ax.Join(tmpDir, "nonexistent.yaml"), revPath)
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "failed to load base spec")
|
||||
}
|
||||
|
||||
func TestDiff_Bad_NonExistentRevision(t *testing.T) {
|
||||
func TestBreaking_DiffNonExistentRevision_Bad(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
|
||||
basePath := filepath.Join(tmpDir, "base.yaml")
|
||||
require.NoError(t, os.WriteFile(basePath, []byte(`openapi: "3.0.0"
|
||||
basePath := ax.Join(tmpDir, "base.yaml")
|
||||
require.NoError(t, ax.WriteFile(basePath, []byte(`openapi: "3.0.0"
|
||||
info:
|
||||
title: Test API
|
||||
version: "1.0.0"
|
||||
paths: {}
|
||||
`), 0644))
|
||||
|
||||
_, err := Diff(basePath, filepath.Join(tmpDir, "nonexistent.yaml"))
|
||||
_, err := Diff(basePath, ax.Join(tmpDir, "nonexistent.yaml"))
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "failed to load revision spec")
|
||||
}
|
||||
|
||||
func TestDiff_Bad_InvalidYAML(t *testing.T) {
|
||||
func TestBreaking_DiffInvalidYAML_Bad(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
|
||||
basePath := filepath.Join(tmpDir, "base.yaml")
|
||||
revPath := filepath.Join(tmpDir, "rev.yaml")
|
||||
require.NoError(t, os.WriteFile(basePath, []byte("not: valid: openapi: spec: {{{{"), 0644))
|
||||
require.NoError(t, os.WriteFile(revPath, []byte(`openapi: "3.0.0"
|
||||
basePath := ax.Join(tmpDir, "base.yaml")
|
||||
revPath := ax.Join(tmpDir, "rev.yaml")
|
||||
require.NoError(t, ax.WriteFile(basePath, []byte("not: valid: openapi: spec: {{{{"), 0644))
|
||||
require.NoError(t, ax.WriteFile(revPath, []byte(`openapi: "3.0.0"
|
||||
info:
|
||||
title: Test API
|
||||
version: "1.0.0"
|
||||
|
|
@ -396,7 +396,7 @@ paths: {}
|
|||
|
||||
// --- DiffExitCode Tests ---
|
||||
|
||||
func TestDiffExitCode_Good(t *testing.T) {
|
||||
func TestBreaking_DiffExitCode_Good(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
result *DiffResult
|
||||
|
|
@ -433,7 +433,7 @@ func TestDiffExitCode_Good(t *testing.T) {
|
|||
|
||||
// --- DiffResult Structure Tests ---
|
||||
|
||||
func TestDiffResult_Good_Summary(t *testing.T) {
|
||||
func TestBreaking_DiffResultSummary_Good(t *testing.T) {
|
||||
t.Run("breaking result has count in summary", func(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
|
||||
|
|
@ -474,10 +474,10 @@ paths:
|
|||
"200":
|
||||
description: OK
|
||||
`
|
||||
basePath := filepath.Join(tmpDir, "base.yaml")
|
||||
revPath := filepath.Join(tmpDir, "rev.yaml")
|
||||
require.NoError(t, os.WriteFile(basePath, []byte(base), 0644))
|
||||
require.NoError(t, os.WriteFile(revPath, []byte(revision), 0644))
|
||||
basePath := ax.Join(tmpDir, "base.yaml")
|
||||
revPath := ax.Join(tmpDir, "rev.yaml")
|
||||
require.NoError(t, ax.WriteFile(basePath, []byte(base), 0644))
|
||||
require.NoError(t, ax.WriteFile(revPath, []byte(revision), 0644))
|
||||
|
||||
result, err := Diff(basePath, revPath)
|
||||
require.NoError(t, err)
|
||||
|
|
@ -489,7 +489,7 @@ paths:
|
|||
})
|
||||
}
|
||||
|
||||
func TestDiffResult_Good_ChangesAreHumanReadable(t *testing.T) {
|
||||
func TestBreaking_DiffResultChangesAreHumanReadable_Good(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
|
||||
base := `openapi: "3.0.0"
|
||||
|
|
@ -510,10 +510,10 @@ info:
|
|||
version: "2.0.0"
|
||||
paths: {}
|
||||
`
|
||||
basePath := filepath.Join(tmpDir, "base.yaml")
|
||||
revPath := filepath.Join(tmpDir, "rev.yaml")
|
||||
require.NoError(t, os.WriteFile(basePath, []byte(base), 0644))
|
||||
require.NoError(t, os.WriteFile(revPath, []byte(revision), 0644))
|
||||
basePath := ax.Join(tmpDir, "base.yaml")
|
||||
revPath := ax.Join(tmpDir, "rev.yaml")
|
||||
require.NoError(t, ax.WriteFile(basePath, []byte(base), 0644))
|
||||
require.NoError(t, ax.WriteFile(revPath, []byte(revision), 0644))
|
||||
|
||||
result, err := Diff(basePath, revPath)
|
||||
require.NoError(t, err)
|
||||
|
|
@ -527,7 +527,7 @@ paths: {}
|
|||
|
||||
// --- Multiple Changes Detection Tests ---
|
||||
|
||||
func TestDiff_Good_MultipleBreakingChanges(t *testing.T) {
|
||||
func TestBreaking_DiffMultipleBreakingChanges_Good(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
|
||||
base := `openapi: "3.0.0"
|
||||
|
|
@ -570,10 +570,10 @@ paths:
|
|||
"200":
|
||||
description: OK
|
||||
`
|
||||
basePath := filepath.Join(tmpDir, "base.yaml")
|
||||
revPath := filepath.Join(tmpDir, "rev.yaml")
|
||||
require.NoError(t, os.WriteFile(basePath, []byte(base), 0644))
|
||||
require.NoError(t, os.WriteFile(revPath, []byte(revision), 0644))
|
||||
basePath := ax.Join(tmpDir, "base.yaml")
|
||||
revPath := ax.Join(tmpDir, "rev.yaml")
|
||||
require.NoError(t, ax.WriteFile(basePath, []byte(base), 0644))
|
||||
require.NoError(t, ax.WriteFile(revPath, []byte(revision), 0644))
|
||||
|
||||
result, err := Diff(basePath, revPath)
|
||||
require.NoError(t, err)
|
||||
|
|
@ -586,7 +586,7 @@ paths:
|
|||
|
||||
// --- JSON Spec Support Tests ---
|
||||
|
||||
func TestDiff_Good_JSONSpecs(t *testing.T) {
|
||||
func TestBreaking_DiffJSONSpecs_Good(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
|
||||
baseJSON := `{
|
||||
|
|
@ -619,10 +619,10 @@ func TestDiff_Good_JSONSpecs(t *testing.T) {
|
|||
}
|
||||
}
|
||||
}`
|
||||
basePath := filepath.Join(tmpDir, "base.json")
|
||||
revPath := filepath.Join(tmpDir, "rev.json")
|
||||
require.NoError(t, os.WriteFile(basePath, []byte(baseJSON), 0644))
|
||||
require.NoError(t, os.WriteFile(revPath, []byte(revJSON), 0644))
|
||||
basePath := ax.Join(tmpDir, "base.json")
|
||||
revPath := ax.Join(tmpDir, "rev.json")
|
||||
require.NoError(t, ax.WriteFile(basePath, []byte(baseJSON), 0644))
|
||||
require.NoError(t, ax.WriteFile(revPath, []byte(revJSON), 0644))
|
||||
|
||||
result, err := Diff(basePath, revPath)
|
||||
require.NoError(t, err)
|
||||
|
|
|
|||
|
|
@ -1,9 +1,8 @@
|
|||
package sdk
|
||||
|
||||
import (
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"dappco.re/go/core"
|
||||
"dappco.re/go/core/build/internal/ax"
|
||||
coreio "dappco.re/go/core/io"
|
||||
coreerr "dappco.re/go/core/log"
|
||||
)
|
||||
|
|
@ -22,10 +21,11 @@ var commonSpecPaths = []string{
|
|||
|
||||
// DetectSpec finds the OpenAPI spec file.
|
||||
// Priority: config path -> common paths -> Laravel Scramble.
|
||||
// Usage example: call value.DetectSpec(...) from integrating code.
|
||||
func (s *SDK) DetectSpec() (string, error) {
|
||||
// 1. Check configured path
|
||||
if s.config.Spec != "" {
|
||||
specPath := filepath.Join(s.projectDir, s.config.Spec)
|
||||
specPath := ax.Join(s.projectDir, s.config.Spec)
|
||||
if coreio.Local.IsFile(specPath) {
|
||||
return specPath, nil
|
||||
}
|
||||
|
|
@ -34,7 +34,7 @@ func (s *SDK) DetectSpec() (string, error) {
|
|||
|
||||
// 2. Check common paths
|
||||
for _, p := range commonSpecPaths {
|
||||
specPath := filepath.Join(s.projectDir, p)
|
||||
specPath := ax.Join(s.projectDir, p)
|
||||
if coreio.Local.IsFile(specPath) {
|
||||
return specPath, nil
|
||||
}
|
||||
|
|
@ -51,7 +51,7 @@ func (s *SDK) DetectSpec() (string, error) {
|
|||
|
||||
// detectScramble checks for Laravel Scramble and exports the spec.
|
||||
func (s *SDK) detectScramble() (string, error) {
|
||||
composerPath := filepath.Join(s.projectDir, "composer.json")
|
||||
composerPath := ax.Join(s.projectDir, "composer.json")
|
||||
if !coreio.Local.IsFile(composerPath) {
|
||||
return "", coreerr.E("sdk.detectScramble", "no composer.json", nil)
|
||||
}
|
||||
|
|
@ -73,6 +73,6 @@ func (s *SDK) detectScramble() (string, error) {
|
|||
|
||||
// containsScramble checks if composer.json includes scramble.
|
||||
func containsScramble(content string) bool {
|
||||
return strings.Contains(content, "dedoc/scramble") ||
|
||||
strings.Contains(content, "\"scramble\"")
|
||||
return core.Contains(content, "dedoc/scramble") ||
|
||||
core.Contains(content, "\"scramble\"")
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,20 +1,20 @@
|
|||
package sdk
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"dappco.re/go/core/build/internal/ax"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestDetectSpec_Good_ConfigPath(t *testing.T) {
|
||||
func TestDetect_DetectSpecConfigPath_Good(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
specPath := filepath.Join(tmpDir, "api", "spec.yaml")
|
||||
err := os.MkdirAll(filepath.Dir(specPath), 0755)
|
||||
specPath := ax.Join(tmpDir, "api", "spec.yaml")
|
||||
err := ax.MkdirAll(ax.Dir(specPath), 0755)
|
||||
require.NoError(t, err)
|
||||
err = os.WriteFile(specPath, []byte("openapi: 3.0.0"), 0644)
|
||||
err = ax.WriteFile(specPath, []byte("openapi: 3.0.0"), 0644)
|
||||
require.NoError(t, err)
|
||||
|
||||
sdk := New(tmpDir, &Config{Spec: "api/spec.yaml"})
|
||||
|
|
@ -23,10 +23,10 @@ func TestDetectSpec_Good_ConfigPath(t *testing.T) {
|
|||
assert.Equal(t, specPath, got)
|
||||
}
|
||||
|
||||
func TestDetectSpec_Good_CommonPath(t *testing.T) {
|
||||
func TestDetect_DetectSpecCommonPath_Good(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
specPath := filepath.Join(tmpDir, "openapi.yaml")
|
||||
err := os.WriteFile(specPath, []byte("openapi: 3.0.0"), 0644)
|
||||
specPath := ax.Join(tmpDir, "openapi.yaml")
|
||||
err := ax.WriteFile(specPath, []byte("openapi: 3.0.0"), 0644)
|
||||
require.NoError(t, err)
|
||||
|
||||
sdk := New(tmpDir, nil)
|
||||
|
|
@ -35,7 +35,7 @@ func TestDetectSpec_Good_CommonPath(t *testing.T) {
|
|||
assert.Equal(t, specPath, got)
|
||||
}
|
||||
|
||||
func TestDetectSpec_Bad_NotFound(t *testing.T) {
|
||||
func TestDetect_DetectSpecNotFound_Bad(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
sdk := New(tmpDir, nil)
|
||||
_, err := sdk.DetectSpec()
|
||||
|
|
@ -43,7 +43,7 @@ func TestDetectSpec_Bad_NotFound(t *testing.T) {
|
|||
assert.Contains(t, err.Error(), "no OpenAPI spec found")
|
||||
}
|
||||
|
||||
func TestDetectSpec_Bad_ConfigNotFound(t *testing.T) {
|
||||
func TestDetect_DetectSpecConfigNotFound_Bad(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
sdk := New(tmpDir, &Config{Spec: "non-existent.yaml"})
|
||||
_, err := sdk.DetectSpec()
|
||||
|
|
@ -51,7 +51,7 @@ func TestDetectSpec_Bad_ConfigNotFound(t *testing.T) {
|
|||
assert.Contains(t, err.Error(), "configured spec not found")
|
||||
}
|
||||
|
||||
func TestContainsScramble(t *testing.T) {
|
||||
func TestDetect_ContainsScramble_Good(t *testing.T) {
|
||||
tests := []struct {
|
||||
data string
|
||||
expected bool
|
||||
|
|
@ -66,7 +66,7 @@ func TestContainsScramble(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestDetectScramble_Bad(t *testing.T) {
|
||||
func TestDetect_DetectScramble_Bad(t *testing.T) {
|
||||
t.Run("no composer.json", func(t *testing.T) {
|
||||
sdk := New(t.TempDir(), nil)
|
||||
_, err := sdk.detectScramble()
|
||||
|
|
@ -76,7 +76,7 @@ func TestDetectScramble_Bad(t *testing.T) {
|
|||
|
||||
t.Run("no scramble in composer.json", func(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
err := os.WriteFile(filepath.Join(tmpDir, "composer.json"), []byte(`{}`), 0644)
|
||||
err := ax.WriteFile(ax.Join(tmpDir, "composer.json"), []byte(`{}`), 0644)
|
||||
require.NoError(t, err)
|
||||
|
||||
sdk := New(tmpDir, nil)
|
||||
|
|
|
|||
|
|
@ -1,8 +1,7 @@
|
|||
package sdk
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"dappco.re/go/core"
|
||||
coreerr "dappco.re/go/core/log"
|
||||
"github.com/oasdiff/kin-openapi/openapi3"
|
||||
"github.com/oasdiff/oasdiff/checker"
|
||||
|
|
@ -11,6 +10,7 @@ import (
|
|||
)
|
||||
|
||||
// DiffResult holds the result of comparing two OpenAPI specs.
|
||||
// Usage example: declare a value of type sdk.DiffResult in integrating code.
|
||||
type DiffResult struct {
|
||||
// Breaking is true if breaking changes were detected.
|
||||
Breaking bool
|
||||
|
|
@ -21,6 +21,7 @@ type DiffResult struct {
|
|||
}
|
||||
|
||||
// Diff compares two OpenAPI specs and detects breaking changes.
|
||||
// Usage example: call sdk.Diff(...) from integrating code.
|
||||
func Diff(basePath, revisionPath string) (*DiffResult, error) {
|
||||
loader := openapi3.NewLoader()
|
||||
loader.IsExternalRefsAllowed = true
|
||||
|
|
@ -64,7 +65,7 @@ func Diff(basePath, revisionPath string) (*DiffResult, error) {
|
|||
}
|
||||
|
||||
if result.Breaking {
|
||||
result.Summary = fmt.Sprintf("%d breaking change(s) detected", len(breaks))
|
||||
result.Summary = core.Sprintf("%d breaking change(s) detected", len(breaks))
|
||||
} else {
|
||||
result.Summary = "No breaking changes"
|
||||
}
|
||||
|
|
@ -74,6 +75,7 @@ func Diff(basePath, revisionPath string) (*DiffResult, error) {
|
|||
|
||||
// DiffExitCode returns the exit code for CI integration.
|
||||
// 0 = no breaking changes, 1 = breaking changes, 2 = error
|
||||
// Usage example: call sdk.DiffExitCode(...) from integrating code.
|
||||
func DiffExitCode(result *DiffResult, err error) int {
|
||||
if err != nil {
|
||||
return 2
|
||||
|
|
|
|||
|
|
@ -1,12 +1,12 @@
|
|||
package sdk
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"dappco.re/go/core/build/internal/ax"
|
||||
)
|
||||
|
||||
func TestDiff_Good_NoBreaking(t *testing.T) {
|
||||
func TestDiff_NoBreaking_Good(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
|
||||
baseSpec := `openapi: "3.0.0"
|
||||
|
|
@ -39,10 +39,10 @@ paths:
|
|||
"200":
|
||||
description: OK
|
||||
`
|
||||
basePath := filepath.Join(tmpDir, "base.yaml")
|
||||
revPath := filepath.Join(tmpDir, "rev.yaml")
|
||||
_ = os.WriteFile(basePath, []byte(baseSpec), 0644)
|
||||
_ = os.WriteFile(revPath, []byte(revSpec), 0644)
|
||||
basePath := ax.Join(tmpDir, "base.yaml")
|
||||
revPath := ax.Join(tmpDir, "rev.yaml")
|
||||
_ = ax.WriteFile(basePath, []byte(baseSpec), 0644)
|
||||
_ = ax.WriteFile(revPath, []byte(revSpec), 0644)
|
||||
|
||||
result, err := Diff(basePath, revPath)
|
||||
if err != nil {
|
||||
|
|
@ -53,7 +53,7 @@ paths:
|
|||
}
|
||||
}
|
||||
|
||||
func TestDiff_Good_Breaking(t *testing.T) {
|
||||
func TestDiff_Breaking_Good(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
|
||||
baseSpec := `openapi: "3.0.0"
|
||||
|
|
@ -86,10 +86,10 @@ paths:
|
|||
"200":
|
||||
description: OK
|
||||
`
|
||||
basePath := filepath.Join(tmpDir, "base.yaml")
|
||||
revPath := filepath.Join(tmpDir, "rev.yaml")
|
||||
_ = os.WriteFile(basePath, []byte(baseSpec), 0644)
|
||||
_ = os.WriteFile(revPath, []byte(revSpec), 0644)
|
||||
basePath := ax.Join(tmpDir, "base.yaml")
|
||||
revPath := ax.Join(tmpDir, "rev.yaml")
|
||||
_ = ax.WriteFile(basePath, []byte(baseSpec), 0644)
|
||||
_ = ax.WriteFile(revPath, []byte(revSpec), 0644)
|
||||
|
||||
result, err := Diff(basePath, revPath)
|
||||
if err != nil {
|
||||
|
|
|
|||
|
|
@ -2,10 +2,10 @@ package sdk
|
|||
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"dappco.re/go/core/build/internal/ax"
|
||||
|
||||
"dappco.re/go/core/build/pkg/sdk/generators"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
|
@ -13,13 +13,13 @@ import (
|
|||
|
||||
// --- SDK Generation Orchestration Tests ---
|
||||
|
||||
func TestSDK_Generate_Good_AllLanguages(t *testing.T) {
|
||||
func TestGeneration_SDKGenerateAllLanguages_Good(t *testing.T) {
|
||||
t.Run("Generate iterates all configured languages", func(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
|
||||
// Create a minimal OpenAPI spec
|
||||
specPath := filepath.Join(tmpDir, "openapi.yaml")
|
||||
err := os.WriteFile(specPath, []byte(minimalSpec), 0644)
|
||||
specPath := ax.Join(tmpDir, "openapi.yaml")
|
||||
err := ax.WriteFile(specPath, []byte(minimalSpec), 0644)
|
||||
require.NoError(t, err)
|
||||
|
||||
cfg := &Config{
|
||||
|
|
@ -41,12 +41,12 @@ func TestSDK_Generate_Good_AllLanguages(t *testing.T) {
|
|||
})
|
||||
}
|
||||
|
||||
func TestSDK_GenerateLanguage_Good_OutputDir(t *testing.T) {
|
||||
func TestGeneration_SDKGenerateLanguageOutputDir_Good(t *testing.T) {
|
||||
t.Run("output directory uses language subdirectory", func(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
|
||||
specPath := filepath.Join(tmpDir, "openapi.yaml")
|
||||
err := os.WriteFile(specPath, []byte(minimalSpec), 0644)
|
||||
specPath := ax.Join(tmpDir, "openapi.yaml")
|
||||
err := ax.WriteFile(specPath, []byte(minimalSpec), 0644)
|
||||
require.NoError(t, err)
|
||||
|
||||
cfg := &Config{
|
||||
|
|
@ -69,7 +69,7 @@ func TestSDK_GenerateLanguage_Good_OutputDir(t *testing.T) {
|
|||
})
|
||||
}
|
||||
|
||||
func TestSDK_GenerateLanguage_Bad_NoSpec(t *testing.T) {
|
||||
func TestGeneration_SDKGenerateLanguageNoSpec_Bad(t *testing.T) {
|
||||
t.Run("fails when no OpenAPI spec exists", func(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
|
||||
|
|
@ -84,11 +84,11 @@ func TestSDK_GenerateLanguage_Bad_NoSpec(t *testing.T) {
|
|||
})
|
||||
}
|
||||
|
||||
func TestSDK_GenerateLanguage_Bad_UnknownLanguage(t *testing.T) {
|
||||
func TestGeneration_SDKGenerateLanguageUnknownLanguage_Bad(t *testing.T) {
|
||||
t.Run("fails for unregistered language", func(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
specPath := filepath.Join(tmpDir, "openapi.yaml")
|
||||
err := os.WriteFile(specPath, []byte(minimalSpec), 0644)
|
||||
specPath := ax.Join(tmpDir, "openapi.yaml")
|
||||
err := ax.WriteFile(specPath, []byte(minimalSpec), 0644)
|
||||
require.NoError(t, err)
|
||||
|
||||
s := New(tmpDir, nil)
|
||||
|
|
@ -100,7 +100,7 @@ func TestSDK_GenerateLanguage_Bad_UnknownLanguage(t *testing.T) {
|
|||
|
||||
// --- Generator Registry Tests ---
|
||||
|
||||
func TestRegistry_Good_RegisterAndGet(t *testing.T) {
|
||||
func TestGeneration_RegistryRegisterAndGet_Good(t *testing.T) {
|
||||
t.Run("register and retrieve all generators", func(t *testing.T) {
|
||||
registry := generators.NewRegistry()
|
||||
registry.Register(generators.NewTypeScriptGenerator())
|
||||
|
|
@ -132,7 +132,7 @@ func TestRegistry_Good_RegisterAndGet(t *testing.T) {
|
|||
})
|
||||
}
|
||||
|
||||
func TestRegistry_Good_OverwritesDuplicateLanguage(t *testing.T) {
|
||||
func TestGeneration_RegistryOverwritesDuplicateLanguage_Good(t *testing.T) {
|
||||
registry := generators.NewRegistry()
|
||||
registry.Register(generators.NewTypeScriptGenerator())
|
||||
registry.Register(generators.NewTypeScriptGenerator()) // register again
|
||||
|
|
@ -149,7 +149,7 @@ func TestRegistry_Good_OverwritesDuplicateLanguage(t *testing.T) {
|
|||
|
||||
// --- Generator Interface Compliance Tests ---
|
||||
|
||||
func TestGenerators_Good_LanguageIdentifiers(t *testing.T) {
|
||||
func TestGeneration_GeneratorsLanguageIdentifiers_Good(t *testing.T) {
|
||||
tests := []struct {
|
||||
generator generators.Generator
|
||||
expected string
|
||||
|
|
@ -167,7 +167,7 @@ func TestGenerators_Good_LanguageIdentifiers(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestGenerators_Good_InstallInstructions(t *testing.T) {
|
||||
func TestGeneration_GeneratorsInstallInstructions_Good(t *testing.T) {
|
||||
tests := []struct {
|
||||
language string
|
||||
gen generators.Generator
|
||||
|
|
@ -188,7 +188,7 @@ func TestGenerators_Good_InstallInstructions(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestGenerators_Good_AvailableDoesNotPanic(t *testing.T) {
|
||||
func TestGeneration_GeneratorsAvailableDoesNotPanic_Good(t *testing.T) {
|
||||
// Available() should never panic regardless of system state
|
||||
gens := []generators.Generator{
|
||||
generators.NewTypeScriptGenerator(),
|
||||
|
|
@ -207,7 +207,7 @@ func TestGenerators_Good_AvailableDoesNotPanic(t *testing.T) {
|
|||
|
||||
// --- SDK Config Tests ---
|
||||
|
||||
func TestSDKConfig_Good_DefaultConfig(t *testing.T) {
|
||||
func TestGeneration_SDKConfigDefaultConfig_Good(t *testing.T) {
|
||||
t.Run("default config has all four languages", func(t *testing.T) {
|
||||
cfg := DefaultConfig()
|
||||
assert.Contains(t, cfg.Languages, "typescript")
|
||||
|
|
@ -229,7 +229,7 @@ func TestSDKConfig_Good_DefaultConfig(t *testing.T) {
|
|||
})
|
||||
}
|
||||
|
||||
func TestSDKConfig_Good_SetVersion(t *testing.T) {
|
||||
func TestGeneration_SDKConfigSetVersion_Good(t *testing.T) {
|
||||
t.Run("SetVersion updates both fields", func(t *testing.T) {
|
||||
s := New("/tmp", &Config{
|
||||
Package: PackageConfig{
|
||||
|
|
@ -251,7 +251,7 @@ func TestSDKConfig_Good_SetVersion(t *testing.T) {
|
|||
})
|
||||
}
|
||||
|
||||
func TestSDKConfig_Good_NewWithNilConfig(t *testing.T) {
|
||||
func TestGeneration_SDKConfigNewWithNilConfig_Good(t *testing.T) {
|
||||
s := New("/project", nil)
|
||||
assert.NotNil(t, s.config)
|
||||
assert.Equal(t, "sdk", s.config.Output)
|
||||
|
|
@ -260,18 +260,18 @@ func TestSDKConfig_Good_NewWithNilConfig(t *testing.T) {
|
|||
|
||||
// --- Spec Detection Integration Tests ---
|
||||
|
||||
func TestSpecDetection_Good_Priority(t *testing.T) {
|
||||
func TestGeneration_SpecDetectionPriority_Good(t *testing.T) {
|
||||
t.Run("configured spec takes priority over common paths", func(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
|
||||
// Create both a common path spec and a configured spec
|
||||
commonSpec := filepath.Join(tmpDir, "openapi.yaml")
|
||||
err := os.WriteFile(commonSpec, []byte(minimalSpec), 0644)
|
||||
commonSpec := ax.Join(tmpDir, "openapi.yaml")
|
||||
err := ax.WriteFile(commonSpec, []byte(minimalSpec), 0644)
|
||||
require.NoError(t, err)
|
||||
|
||||
configuredSpec := filepath.Join(tmpDir, "custom", "api.yaml")
|
||||
require.NoError(t, os.MkdirAll(filepath.Dir(configuredSpec), 0755))
|
||||
err = os.WriteFile(configuredSpec, []byte(minimalSpec), 0644)
|
||||
configuredSpec := ax.Join(tmpDir, "custom", "api.yaml")
|
||||
require.NoError(t, ax.MkdirAll(ax.Dir(configuredSpec), 0755))
|
||||
err = ax.WriteFile(configuredSpec, []byte(minimalSpec), 0644)
|
||||
require.NoError(t, err)
|
||||
|
||||
s := New(tmpDir, &Config{Spec: "custom/api.yaml"})
|
||||
|
|
@ -284,10 +284,10 @@ func TestSpecDetection_Good_Priority(t *testing.T) {
|
|||
tmpDir := t.TempDir()
|
||||
|
||||
// Create the second common path only (api/openapi.yaml is first)
|
||||
apiDir := filepath.Join(tmpDir, "api")
|
||||
require.NoError(t, os.MkdirAll(apiDir, 0755))
|
||||
apiSpec := filepath.Join(apiDir, "openapi.json")
|
||||
err := os.WriteFile(apiSpec, []byte(`{"openapi":"3.0.0"}`), 0644)
|
||||
apiDir := ax.Join(tmpDir, "api")
|
||||
require.NoError(t, ax.MkdirAll(apiDir, 0755))
|
||||
apiSpec := ax.Join(apiDir, "openapi.json")
|
||||
err := ax.WriteFile(apiSpec, []byte(`{"openapi":"3.0.0"}`), 0644)
|
||||
require.NoError(t, err)
|
||||
|
||||
s := New(tmpDir, nil)
|
||||
|
|
@ -297,14 +297,14 @@ func TestSpecDetection_Good_Priority(t *testing.T) {
|
|||
})
|
||||
}
|
||||
|
||||
func TestSpecDetection_Good_AllCommonPaths(t *testing.T) {
|
||||
func TestGeneration_SpecDetectionAllCommonPaths_Good(t *testing.T) {
|
||||
for _, commonPath := range commonSpecPaths {
|
||||
t.Run(commonPath, func(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
|
||||
specPath := filepath.Join(tmpDir, commonPath)
|
||||
require.NoError(t, os.MkdirAll(filepath.Dir(specPath), 0755))
|
||||
err := os.WriteFile(specPath, []byte(minimalSpec), 0644)
|
||||
specPath := ax.Join(tmpDir, commonPath)
|
||||
require.NoError(t, ax.MkdirAll(ax.Dir(specPath), 0755))
|
||||
err := ax.WriteFile(specPath, []byte(minimalSpec), 0644)
|
||||
require.NoError(t, err)
|
||||
|
||||
s := New(tmpDir, nil)
|
||||
|
|
|
|||
20
pkg/sdk/generators/docker_runtime.go
Normal file
20
pkg/sdk/generators/docker_runtime.go
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
package generators
|
||||
|
||||
import (
|
||||
"context"
|
||||
"sync"
|
||||
|
||||
"dappco.re/go/core/build/internal/ax"
|
||||
)
|
||||
|
||||
var (
|
||||
dockerRuntimeOnce sync.Once
|
||||
dockerRuntimeOK bool
|
||||
)
|
||||
|
||||
func dockerRuntimeAvailable() bool {
|
||||
dockerRuntimeOnce.Do(func() {
|
||||
dockerRuntimeOK = ax.Exec(context.Background(), "docker", "info") == nil
|
||||
})
|
||||
return dockerRuntimeOK
|
||||
}
|
||||
|
|
@ -3,15 +3,16 @@ package generators
|
|||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"iter"
|
||||
"maps"
|
||||
"os"
|
||||
"runtime"
|
||||
"slices"
|
||||
"sort"
|
||||
|
||||
"dappco.re/go/core"
|
||||
"dappco.re/go/core/build/internal/ax"
|
||||
)
|
||||
|
||||
// Options holds common generation options.
|
||||
// Usage example: declare a value of type generators.Options in integrating code.
|
||||
type Options struct {
|
||||
// SpecPath is the path to the OpenAPI spec file.
|
||||
SpecPath string
|
||||
|
|
@ -24,6 +25,7 @@ type Options struct {
|
|||
}
|
||||
|
||||
// Generator defines the interface for SDK generators.
|
||||
// Usage example: declare a value of type generators.Generator in integrating code.
|
||||
type Generator interface {
|
||||
// Language returns the generator's target language identifier.
|
||||
Language() string
|
||||
|
|
@ -39,11 +41,13 @@ type Generator interface {
|
|||
}
|
||||
|
||||
// Registry holds available generators.
|
||||
// Usage example: declare a value of type generators.Registry in integrating code.
|
||||
type Registry struct {
|
||||
generators map[string]Generator
|
||||
}
|
||||
|
||||
// NewRegistry creates a registry with all available generators.
|
||||
// Usage example: call generators.NewRegistry(...) from integrating code.
|
||||
func NewRegistry() *Registry {
|
||||
r := &Registry{
|
||||
generators: make(map[string]Generator),
|
||||
|
|
@ -53,26 +57,39 @@ func NewRegistry() *Registry {
|
|||
}
|
||||
|
||||
// Get returns a generator by language.
|
||||
// Usage example: call value.Get(...) from integrating code.
|
||||
func (r *Registry) Get(lang string) (Generator, bool) {
|
||||
g, ok := r.generators[lang]
|
||||
return g, ok
|
||||
}
|
||||
|
||||
// Register adds a generator to the registry.
|
||||
// Usage example: call value.Register(...) from integrating code.
|
||||
func (r *Registry) Register(g Generator) {
|
||||
r.generators[g.Language()] = g
|
||||
}
|
||||
|
||||
// Languages returns all registered language identifiers.
|
||||
// Usage example: call value.Languages(...) from integrating code.
|
||||
func (r *Registry) Languages() []string {
|
||||
return slices.Collect(r.LanguagesIter())
|
||||
var languages []string
|
||||
for lang := range r.LanguagesIter() {
|
||||
languages = append(languages, lang)
|
||||
}
|
||||
return languages
|
||||
}
|
||||
|
||||
// LanguagesIter returns an iterator for all registered language identifiers.
|
||||
// Usage example: call value.LanguagesIter(...) from integrating code.
|
||||
func (r *Registry) LanguagesIter() iter.Seq[string] {
|
||||
return func(yield func(string) bool) {
|
||||
// Sort keys for deterministic iteration
|
||||
for _, lang := range slices.Sorted(maps.Keys(r.generators)) {
|
||||
keys := make([]string, 0, len(r.generators))
|
||||
for lang := range r.generators {
|
||||
keys = append(keys, lang)
|
||||
}
|
||||
sort.Strings(keys)
|
||||
for _, lang := range keys {
|
||||
if !yield(lang) {
|
||||
return
|
||||
}
|
||||
|
|
@ -86,5 +103,5 @@ func dockerUserArgs() []string {
|
|||
if runtime.GOOS == "windows" {
|
||||
return nil
|
||||
}
|
||||
return []string{"--user", fmt.Sprintf("%d:%d", os.Getuid(), os.Getgid())}
|
||||
return []string{"--user", core.Sprintf("%d:%d", ax.Getuid(), ax.Getgid())}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,40 +2,44 @@ package generators
|
|||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
|
||||
"dappco.re/go/core"
|
||||
"dappco.re/go/core/build/internal/ax"
|
||||
coreio "dappco.re/go/core/io"
|
||||
coreerr "dappco.re/go/core/log"
|
||||
)
|
||||
|
||||
// GoGenerator generates Go SDKs from OpenAPI specs.
|
||||
// Usage example: declare a value of type generators.GoGenerator in integrating code.
|
||||
type GoGenerator struct{}
|
||||
|
||||
// NewGoGenerator creates a new Go generator.
|
||||
// Usage example: call generators.NewGoGenerator(...) from integrating code.
|
||||
func NewGoGenerator() *GoGenerator {
|
||||
return &GoGenerator{}
|
||||
}
|
||||
|
||||
// Language returns the generator's target language identifier.
|
||||
// Usage example: call value.Language(...) from integrating code.
|
||||
func (g *GoGenerator) Language() string {
|
||||
return "go"
|
||||
}
|
||||
|
||||
// Available checks if generator dependencies are installed.
|
||||
// Usage example: call value.Available(...) from integrating code.
|
||||
func (g *GoGenerator) Available() bool {
|
||||
_, err := exec.LookPath("oapi-codegen")
|
||||
_, err := ax.LookPath("oapi-codegen")
|
||||
return err == nil
|
||||
}
|
||||
|
||||
// Install returns instructions for installing the generator.
|
||||
// Usage example: call value.Install(...) from integrating code.
|
||||
func (g *GoGenerator) Install() string {
|
||||
return "go install github.com/oapi-codegen/oapi-codegen/v2/cmd/oapi-codegen@latest"
|
||||
}
|
||||
|
||||
// Generate creates SDK from OpenAPI spec.
|
||||
// Usage example: call value.Generate(...) from integrating code.
|
||||
func (g *GoGenerator) Generate(ctx context.Context, opts Options) error {
|
||||
if err := coreio.Local.EnsureDir(opts.OutputDir); err != nil {
|
||||
return coreerr.E("go.Generate", "failed to create output dir", err)
|
||||
|
|
@ -44,32 +48,31 @@ func (g *GoGenerator) Generate(ctx context.Context, opts Options) error {
|
|||
if g.Available() {
|
||||
return g.generateNative(ctx, opts)
|
||||
}
|
||||
if !dockerRuntimeAvailable() {
|
||||
return coreerr.E("go.Generate", "Docker is required for fallback generation but not available", nil)
|
||||
}
|
||||
return g.generateDocker(ctx, opts)
|
||||
}
|
||||
|
||||
func (g *GoGenerator) generateNative(ctx context.Context, opts Options) error {
|
||||
outputFile := filepath.Join(opts.OutputDir, "client.go")
|
||||
outputFile := ax.Join(opts.OutputDir, "client.go")
|
||||
|
||||
cmd := exec.CommandContext(ctx, "oapi-codegen",
|
||||
if err := ax.Exec(ctx, "oapi-codegen",
|
||||
"-package", opts.PackageName,
|
||||
"-generate", "types,client",
|
||||
"-o", outputFile,
|
||||
opts.SpecPath,
|
||||
)
|
||||
cmd.Stdout = os.Stdout
|
||||
cmd.Stderr = os.Stderr
|
||||
|
||||
if err := cmd.Run(); err != nil {
|
||||
); err != nil {
|
||||
return coreerr.E("go.generateNative", "oapi-codegen failed", err)
|
||||
}
|
||||
|
||||
goMod := fmt.Sprintf("module %s\n\ngo 1.21\n", opts.PackageName)
|
||||
return coreio.Local.Write(filepath.Join(opts.OutputDir, "go.mod"), goMod)
|
||||
goMod := core.Sprintf("module %s\n\ngo 1.21\n", opts.PackageName)
|
||||
return coreio.Local.Write(ax.Join(opts.OutputDir, "go.mod"), goMod)
|
||||
}
|
||||
|
||||
func (g *GoGenerator) generateDocker(ctx context.Context, opts Options) error {
|
||||
specDir := filepath.Dir(opts.SpecPath)
|
||||
specName := filepath.Base(opts.SpecPath)
|
||||
specDir := ax.Dir(opts.SpecPath)
|
||||
specName := ax.Base(opts.SpecPath)
|
||||
|
||||
args := []string{"run", "--rm"}
|
||||
args = append(args, dockerUserArgs()...)
|
||||
|
|
@ -83,8 +86,5 @@ func (g *GoGenerator) generateDocker(ctx context.Context, opts Options) error {
|
|||
"--additional-properties=packageName="+opts.PackageName,
|
||||
)
|
||||
|
||||
cmd := exec.CommandContext(ctx, "docker", args...)
|
||||
cmd.Stdout = os.Stdout
|
||||
cmd.Stderr = os.Stderr
|
||||
return cmd.Run()
|
||||
return ax.Exec(ctx, "docker", args...)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,13 +2,13 @@ package generators
|
|||
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"dappco.re/go/core/build/internal/ax"
|
||||
)
|
||||
|
||||
func TestGoGenerator_Good_Available(t *testing.T) {
|
||||
func TestGo_GoGeneratorAvailable_Good(t *testing.T) {
|
||||
g := NewGoGenerator()
|
||||
|
||||
// These should not panic
|
||||
|
|
@ -25,7 +25,7 @@ func TestGoGenerator_Good_Available(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestGoGenerator_Good_Generate(t *testing.T) {
|
||||
func TestGo_GoGeneratorGenerate_Good(t *testing.T) {
|
||||
g := NewGoGenerator()
|
||||
if !g.Available() && !dockerAvailable() {
|
||||
t.Skip("no Go generator available (neither native nor docker)")
|
||||
|
|
@ -34,7 +34,7 @@ func TestGoGenerator_Good_Generate(t *testing.T) {
|
|||
// Create temp directories
|
||||
tmpDir := t.TempDir()
|
||||
specPath := createTestSpec(t, tmpDir)
|
||||
outputDir := filepath.Join(tmpDir, "output")
|
||||
outputDir := ax.Join(tmpDir, "output")
|
||||
|
||||
opts := Options{
|
||||
SpecPath: specPath,
|
||||
|
|
@ -52,7 +52,7 @@ func TestGoGenerator_Good_Generate(t *testing.T) {
|
|||
}
|
||||
|
||||
// Verify output directory was created
|
||||
if _, err := os.Stat(outputDir); os.IsNotExist(err) {
|
||||
if !ax.Exists(outputDir) {
|
||||
t.Error("output directory was not created")
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,39 +2,42 @@ package generators
|
|||
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
|
||||
"dappco.re/go/core/build/internal/ax"
|
||||
coreio "dappco.re/go/core/io"
|
||||
coreerr "dappco.re/go/core/log"
|
||||
)
|
||||
|
||||
// PHPGenerator generates PHP SDKs from OpenAPI specs.
|
||||
// Usage example: declare a value of type generators.PHPGenerator in integrating code.
|
||||
type PHPGenerator struct{}
|
||||
|
||||
// NewPHPGenerator creates a new PHP generator.
|
||||
// Usage example: call generators.NewPHPGenerator(...) from integrating code.
|
||||
func NewPHPGenerator() *PHPGenerator {
|
||||
return &PHPGenerator{}
|
||||
}
|
||||
|
||||
// Language returns the generator's target language identifier.
|
||||
// Usage example: call value.Language(...) from integrating code.
|
||||
func (g *PHPGenerator) Language() string {
|
||||
return "php"
|
||||
}
|
||||
|
||||
// Available checks if generator dependencies are installed.
|
||||
// Usage example: call value.Available(...) from integrating code.
|
||||
func (g *PHPGenerator) Available() bool {
|
||||
_, err := exec.LookPath("docker")
|
||||
return err == nil
|
||||
return dockerRuntimeAvailable()
|
||||
}
|
||||
|
||||
// Install returns instructions for installing the generator.
|
||||
// Usage example: call value.Install(...) from integrating code.
|
||||
func (g *PHPGenerator) Install() string {
|
||||
return "Docker is required for PHP SDK generation"
|
||||
}
|
||||
|
||||
// Generate creates SDK from OpenAPI spec.
|
||||
// Usage example: call value.Generate(...) from integrating code.
|
||||
func (g *PHPGenerator) Generate(ctx context.Context, opts Options) error {
|
||||
if !g.Available() {
|
||||
return coreerr.E("php.Generate", "Docker is required but not available", nil)
|
||||
|
|
@ -44,8 +47,8 @@ func (g *PHPGenerator) Generate(ctx context.Context, opts Options) error {
|
|||
return coreerr.E("php.Generate", "failed to create output dir", err)
|
||||
}
|
||||
|
||||
specDir := filepath.Dir(opts.SpecPath)
|
||||
specName := filepath.Base(opts.SpecPath)
|
||||
specDir := ax.Dir(opts.SpecPath)
|
||||
specName := ax.Base(opts.SpecPath)
|
||||
|
||||
args := []string{"run", "--rm"}
|
||||
args = append(args, dockerUserArgs()...)
|
||||
|
|
@ -59,11 +62,7 @@ func (g *PHPGenerator) Generate(ctx context.Context, opts Options) error {
|
|||
"--additional-properties=invokerPackage="+opts.PackageName,
|
||||
)
|
||||
|
||||
cmd := exec.CommandContext(ctx, "docker", args...)
|
||||
cmd.Stdout = os.Stdout
|
||||
cmd.Stderr = os.Stderr
|
||||
|
||||
if err := cmd.Run(); err != nil {
|
||||
if err := ax.Exec(ctx, "docker", args...); err != nil {
|
||||
return coreerr.E("php.Generate", "docker run failed", err)
|
||||
}
|
||||
return nil
|
||||
|
|
|
|||
|
|
@ -2,13 +2,13 @@ package generators
|
|||
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"dappco.re/go/core/build/internal/ax"
|
||||
)
|
||||
|
||||
func TestPHPGenerator_Good_Available(t *testing.T) {
|
||||
func TestPHP_PHPGeneratorAvailable_Good(t *testing.T) {
|
||||
g := NewPHPGenerator()
|
||||
|
||||
// These should not panic
|
||||
|
|
@ -25,7 +25,7 @@ func TestPHPGenerator_Good_Available(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestPHPGenerator_Good_Generate(t *testing.T) {
|
||||
func TestPHP_PHPGeneratorGenerate_Good(t *testing.T) {
|
||||
g := NewPHPGenerator()
|
||||
if !g.Available() {
|
||||
t.Skip("no PHP generator available (docker not installed)")
|
||||
|
|
@ -34,7 +34,7 @@ func TestPHPGenerator_Good_Generate(t *testing.T) {
|
|||
// Create temp directories
|
||||
tmpDir := t.TempDir()
|
||||
specPath := createTestSpec(t, tmpDir)
|
||||
outputDir := filepath.Join(tmpDir, "output")
|
||||
outputDir := ax.Join(tmpDir, "output")
|
||||
|
||||
opts := Options{
|
||||
SpecPath: specPath,
|
||||
|
|
@ -52,7 +52,7 @@ func TestPHPGenerator_Good_Generate(t *testing.T) {
|
|||
}
|
||||
|
||||
// Verify output directory was created
|
||||
if _, err := os.Stat(outputDir); os.IsNotExist(err) {
|
||||
if !ax.Exists(outputDir) {
|
||||
t.Error("output directory was not created")
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,39 +2,43 @@ package generators
|
|||
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
|
||||
"dappco.re/go/core/build/internal/ax"
|
||||
coreio "dappco.re/go/core/io"
|
||||
coreerr "dappco.re/go/core/log"
|
||||
)
|
||||
|
||||
// PythonGenerator generates Python SDKs from OpenAPI specs.
|
||||
// Usage example: declare a value of type generators.PythonGenerator in integrating code.
|
||||
type PythonGenerator struct{}
|
||||
|
||||
// NewPythonGenerator creates a new Python generator.
|
||||
// Usage example: call generators.NewPythonGenerator(...) from integrating code.
|
||||
func NewPythonGenerator() *PythonGenerator {
|
||||
return &PythonGenerator{}
|
||||
}
|
||||
|
||||
// Language returns the generator's target language identifier.
|
||||
// Usage example: call value.Language(...) from integrating code.
|
||||
func (g *PythonGenerator) Language() string {
|
||||
return "python"
|
||||
}
|
||||
|
||||
// Available checks if generator dependencies are installed.
|
||||
// Usage example: call value.Available(...) from integrating code.
|
||||
func (g *PythonGenerator) Available() bool {
|
||||
_, err := exec.LookPath("openapi-python-client")
|
||||
_, err := ax.LookPath("openapi-python-client")
|
||||
return err == nil
|
||||
}
|
||||
|
||||
// Install returns instructions for installing the generator.
|
||||
// Usage example: call value.Install(...) from integrating code.
|
||||
func (g *PythonGenerator) Install() string {
|
||||
return "pip install openapi-python-client"
|
||||
}
|
||||
|
||||
// Generate creates SDK from OpenAPI spec.
|
||||
// Usage example: call value.Generate(...) from integrating code.
|
||||
func (g *PythonGenerator) Generate(ctx context.Context, opts Options) error {
|
||||
if err := coreio.Local.EnsureDir(opts.OutputDir); err != nil {
|
||||
return coreerr.E("python.Generate", "failed to create output dir", err)
|
||||
|
|
@ -43,25 +47,24 @@ func (g *PythonGenerator) Generate(ctx context.Context, opts Options) error {
|
|||
if g.Available() {
|
||||
return g.generateNative(ctx, opts)
|
||||
}
|
||||
if !dockerRuntimeAvailable() {
|
||||
return coreerr.E("python.Generate", "Docker is required for fallback generation but not available", nil)
|
||||
}
|
||||
return g.generateDocker(ctx, opts)
|
||||
}
|
||||
|
||||
func (g *PythonGenerator) generateNative(ctx context.Context, opts Options) error {
|
||||
parentDir := filepath.Dir(opts.OutputDir)
|
||||
parentDir := ax.Dir(opts.OutputDir)
|
||||
|
||||
cmd := exec.CommandContext(ctx, "openapi-python-client", "generate",
|
||||
return ax.ExecDir(ctx, parentDir, "openapi-python-client", "generate",
|
||||
"--path", opts.SpecPath,
|
||||
"--output-path", opts.OutputDir,
|
||||
)
|
||||
cmd.Dir = parentDir
|
||||
cmd.Stdout = os.Stdout
|
||||
cmd.Stderr = os.Stderr
|
||||
return cmd.Run()
|
||||
}
|
||||
|
||||
func (g *PythonGenerator) generateDocker(ctx context.Context, opts Options) error {
|
||||
specDir := filepath.Dir(opts.SpecPath)
|
||||
specName := filepath.Base(opts.SpecPath)
|
||||
specDir := ax.Dir(opts.SpecPath)
|
||||
specName := ax.Base(opts.SpecPath)
|
||||
|
||||
args := []string{"run", "--rm"}
|
||||
args = append(args, dockerUserArgs()...)
|
||||
|
|
@ -75,8 +78,5 @@ func (g *PythonGenerator) generateDocker(ctx context.Context, opts Options) erro
|
|||
"--additional-properties=packageName="+opts.PackageName,
|
||||
)
|
||||
|
||||
cmd := exec.CommandContext(ctx, "docker", args...)
|
||||
cmd.Stdout = os.Stdout
|
||||
cmd.Stderr = os.Stderr
|
||||
return cmd.Run()
|
||||
return ax.Exec(ctx, "docker", args...)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,13 +2,13 @@ package generators
|
|||
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"dappco.re/go/core/build/internal/ax"
|
||||
)
|
||||
|
||||
func TestPythonGenerator_Good_Available(t *testing.T) {
|
||||
func TestPython_PythonGeneratorAvailable_Good(t *testing.T) {
|
||||
g := NewPythonGenerator()
|
||||
|
||||
// These should not panic
|
||||
|
|
@ -25,7 +25,7 @@ func TestPythonGenerator_Good_Available(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestPythonGenerator_Good_Generate(t *testing.T) {
|
||||
func TestPython_PythonGeneratorGenerate_Good(t *testing.T) {
|
||||
g := NewPythonGenerator()
|
||||
if !g.Available() && !dockerAvailable() {
|
||||
t.Skip("no Python generator available (neither native nor docker)")
|
||||
|
|
@ -34,7 +34,7 @@ func TestPythonGenerator_Good_Generate(t *testing.T) {
|
|||
// Create temp directories
|
||||
tmpDir := t.TempDir()
|
||||
specPath := createTestSpec(t, tmpDir)
|
||||
outputDir := filepath.Join(tmpDir, "output")
|
||||
outputDir := ax.Join(tmpDir, "output")
|
||||
|
||||
opts := Options{
|
||||
SpecPath: specPath,
|
||||
|
|
@ -52,7 +52,7 @@ func TestPythonGenerator_Good_Generate(t *testing.T) {
|
|||
}
|
||||
|
||||
// Verify output directory was created
|
||||
if _, err := os.Stat(outputDir); os.IsNotExist(err) {
|
||||
if !ax.Exists(outputDir) {
|
||||
t.Error("output directory was not created")
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,43 +2,47 @@ package generators
|
|||
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
|
||||
"dappco.re/go/core/build/internal/ax"
|
||||
coreio "dappco.re/go/core/io"
|
||||
coreerr "dappco.re/go/core/log"
|
||||
)
|
||||
|
||||
// TypeScriptGenerator generates TypeScript SDKs from OpenAPI specs.
|
||||
// Usage example: declare a value of type generators.TypeScriptGenerator in integrating code.
|
||||
type TypeScriptGenerator struct{}
|
||||
|
||||
// NewTypeScriptGenerator creates a new TypeScript generator.
|
||||
// Usage example: call generators.NewTypeScriptGenerator(...) from integrating code.
|
||||
func NewTypeScriptGenerator() *TypeScriptGenerator {
|
||||
return &TypeScriptGenerator{}
|
||||
}
|
||||
|
||||
// Language returns the generator's target language identifier.
|
||||
// Usage example: call value.Language(...) from integrating code.
|
||||
func (g *TypeScriptGenerator) Language() string {
|
||||
return "typescript"
|
||||
}
|
||||
|
||||
// Available checks if generator dependencies are installed.
|
||||
// Usage example: call value.Available(...) from integrating code.
|
||||
func (g *TypeScriptGenerator) Available() bool {
|
||||
_, err := exec.LookPath("openapi-typescript-codegen")
|
||||
_, err := ax.LookPath("openapi-typescript-codegen")
|
||||
if err == nil {
|
||||
return true
|
||||
}
|
||||
_, err = exec.LookPath("npx")
|
||||
_, err = ax.LookPath("npx")
|
||||
return err == nil
|
||||
}
|
||||
|
||||
// Install returns instructions for installing the generator.
|
||||
// Usage example: call value.Install(...) from integrating code.
|
||||
func (g *TypeScriptGenerator) Install() string {
|
||||
return "npm install -g openapi-typescript-codegen"
|
||||
}
|
||||
|
||||
// Generate creates SDK from OpenAPI spec.
|
||||
// Usage example: call value.Generate(...) from integrating code.
|
||||
func (g *TypeScriptGenerator) Generate(ctx context.Context, opts Options) error {
|
||||
if err := coreio.Local.EnsureDir(opts.OutputDir); err != nil {
|
||||
return coreerr.E("typescript.Generate", "failed to create output dir", err)
|
||||
|
|
@ -50,44 +54,41 @@ func (g *TypeScriptGenerator) Generate(ctx context.Context, opts Options) error
|
|||
if g.npxAvailable() {
|
||||
return g.generateNpx(ctx, opts)
|
||||
}
|
||||
if !dockerRuntimeAvailable() {
|
||||
return coreerr.E("typescript.Generate", "Docker is required for fallback generation but not available", nil)
|
||||
}
|
||||
return g.generateDocker(ctx, opts)
|
||||
}
|
||||
|
||||
func (g *TypeScriptGenerator) nativeAvailable() bool {
|
||||
_, err := exec.LookPath("openapi-typescript-codegen")
|
||||
_, err := ax.LookPath("openapi-typescript-codegen")
|
||||
return err == nil
|
||||
}
|
||||
|
||||
func (g *TypeScriptGenerator) npxAvailable() bool {
|
||||
_, err := exec.LookPath("npx")
|
||||
_, err := ax.LookPath("npx")
|
||||
return err == nil
|
||||
}
|
||||
|
||||
func (g *TypeScriptGenerator) generateNative(ctx context.Context, opts Options) error {
|
||||
cmd := exec.CommandContext(ctx, "openapi-typescript-codegen",
|
||||
return ax.Exec(ctx, "openapi-typescript-codegen",
|
||||
"--input", opts.SpecPath,
|
||||
"--output", opts.OutputDir,
|
||||
"--name", opts.PackageName,
|
||||
)
|
||||
cmd.Stdout = os.Stdout
|
||||
cmd.Stderr = os.Stderr
|
||||
return cmd.Run()
|
||||
}
|
||||
|
||||
func (g *TypeScriptGenerator) generateNpx(ctx context.Context, opts Options) error {
|
||||
cmd := exec.CommandContext(ctx, "npx", "openapi-typescript-codegen",
|
||||
return ax.Exec(ctx, "npx", "openapi-typescript-codegen",
|
||||
"--input", opts.SpecPath,
|
||||
"--output", opts.OutputDir,
|
||||
"--name", opts.PackageName,
|
||||
)
|
||||
cmd.Stdout = os.Stdout
|
||||
cmd.Stderr = os.Stderr
|
||||
return cmd.Run()
|
||||
}
|
||||
|
||||
func (g *TypeScriptGenerator) generateDocker(ctx context.Context, opts Options) error {
|
||||
specDir := filepath.Dir(opts.SpecPath)
|
||||
specName := filepath.Base(opts.SpecPath)
|
||||
specDir := ax.Dir(opts.SpecPath)
|
||||
specName := ax.Base(opts.SpecPath)
|
||||
|
||||
args := []string{"run", "--rm"}
|
||||
args = append(args, dockerUserArgs()...)
|
||||
|
|
@ -101,11 +102,7 @@ func (g *TypeScriptGenerator) generateDocker(ctx context.Context, opts Options)
|
|||
"--additional-properties=npmName="+opts.PackageName,
|
||||
)
|
||||
|
||||
cmd := exec.CommandContext(ctx, "docker", args...)
|
||||
cmd.Stdout = os.Stdout
|
||||
cmd.Stderr = os.Stderr
|
||||
|
||||
if err := cmd.Run(); err != nil {
|
||||
if err := ax.Exec(ctx, "docker", args...); err != nil {
|
||||
return coreerr.E("typescript.generateDocker", "docker run failed", err)
|
||||
}
|
||||
return nil
|
||||
|
|
|
|||
|
|
@ -2,17 +2,15 @@ package generators
|
|||
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"dappco.re/go/core/build/internal/ax"
|
||||
)
|
||||
|
||||
// dockerAvailable checks if docker is available for fallback generation.
|
||||
func dockerAvailable() bool {
|
||||
_, err := exec.LookPath("docker")
|
||||
return err == nil
|
||||
return ax.Exec(context.Background(), "docker", "info") == nil
|
||||
}
|
||||
|
||||
// createTestSpec creates a minimal OpenAPI spec for testing.
|
||||
|
|
@ -30,14 +28,14 @@ paths:
|
|||
"200":
|
||||
description: OK
|
||||
`
|
||||
specPath := filepath.Join(dir, "openapi.yaml")
|
||||
if err := os.WriteFile(specPath, []byte(spec), 0644); err != nil {
|
||||
specPath := ax.Join(dir, "openapi.yaml")
|
||||
if err := ax.WriteFile(specPath, []byte(spec), 0o644); err != nil {
|
||||
t.Fatalf("failed to write test spec: %v", err)
|
||||
}
|
||||
return specPath
|
||||
}
|
||||
|
||||
func TestTypeScriptGenerator_Good_Available(t *testing.T) {
|
||||
func TestTypeScript_TypeScriptGeneratorAvailable_Good(t *testing.T) {
|
||||
g := NewTypeScriptGenerator()
|
||||
|
||||
// These should not panic
|
||||
|
|
@ -54,7 +52,7 @@ func TestTypeScriptGenerator_Good_Available(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestTypeScriptGenerator_Good_Generate(t *testing.T) {
|
||||
func TestTypeScript_TypeScriptGeneratorGenerate_Good(t *testing.T) {
|
||||
g := NewTypeScriptGenerator()
|
||||
if !g.Available() && !dockerAvailable() {
|
||||
t.Skip("no TypeScript generator available (neither native nor docker)")
|
||||
|
|
@ -63,7 +61,7 @@ func TestTypeScriptGenerator_Good_Generate(t *testing.T) {
|
|||
// Create temp directories
|
||||
tmpDir := t.TempDir()
|
||||
specPath := createTestSpec(t, tmpDir)
|
||||
outputDir := filepath.Join(tmpDir, "output")
|
||||
outputDir := ax.Join(tmpDir, "output")
|
||||
|
||||
opts := Options{
|
||||
SpecPath: specPath,
|
||||
|
|
@ -81,7 +79,7 @@ func TestTypeScriptGenerator_Good_Generate(t *testing.T) {
|
|||
}
|
||||
|
||||
// Verify output directory was created
|
||||
if _, err := os.Stat(outputDir); os.IsNotExist(err) {
|
||||
if !ax.Exists(outputDir) {
|
||||
t.Error("output directory was not created")
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,14 +3,15 @@ package sdk
|
|||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"path/filepath"
|
||||
|
||||
"dappco.re/go/core"
|
||||
"dappco.re/go/core/build/internal/ax"
|
||||
"dappco.re/go/core/build/pkg/sdk/generators"
|
||||
coreerr "dappco.re/go/core/log"
|
||||
)
|
||||
|
||||
// Config holds SDK generation configuration from .core/release.yaml.
|
||||
// Usage example: declare a value of type sdk.Config in integrating code.
|
||||
type Config struct {
|
||||
// Spec is the path to the OpenAPI spec file (auto-detected if empty).
|
||||
Spec string `yaml:"spec,omitempty"`
|
||||
|
|
@ -27,6 +28,7 @@ type Config struct {
|
|||
}
|
||||
|
||||
// PackageConfig holds package naming configuration.
|
||||
// Usage example: declare a value of type sdk.PackageConfig in integrating code.
|
||||
type PackageConfig struct {
|
||||
// Name is the base package name.
|
||||
Name string `yaml:"name,omitempty"`
|
||||
|
|
@ -35,6 +37,7 @@ type PackageConfig struct {
|
|||
}
|
||||
|
||||
// DiffConfig holds breaking change detection configuration.
|
||||
// Usage example: declare a value of type sdk.DiffConfig in integrating code.
|
||||
type DiffConfig struct {
|
||||
// Enabled determines whether to run diff checks.
|
||||
Enabled bool `yaml:"enabled,omitempty"`
|
||||
|
|
@ -43,6 +46,7 @@ type DiffConfig struct {
|
|||
}
|
||||
|
||||
// PublishConfig holds monorepo publishing configuration.
|
||||
// Usage example: declare a value of type sdk.PublishConfig in integrating code.
|
||||
type PublishConfig struct {
|
||||
// Repo is the SDK monorepo (e.g., "myorg/sdks").
|
||||
Repo string `yaml:"repo,omitempty"`
|
||||
|
|
@ -51,6 +55,7 @@ type PublishConfig struct {
|
|||
}
|
||||
|
||||
// SDK orchestrates OpenAPI SDK generation.
|
||||
// Usage example: declare a value of type sdk.SDK in integrating code.
|
||||
type SDK struct {
|
||||
config *Config
|
||||
projectDir string
|
||||
|
|
@ -58,6 +63,7 @@ type SDK struct {
|
|||
}
|
||||
|
||||
// New creates a new SDK instance.
|
||||
// Usage example: call sdk.New(...) from integrating code.
|
||||
func New(projectDir string, config *Config) *SDK {
|
||||
if config == nil {
|
||||
config = DefaultConfig()
|
||||
|
|
@ -70,6 +76,7 @@ func New(projectDir string, config *Config) *SDK {
|
|||
|
||||
// SetVersion sets the SDK version for generation.
|
||||
// This updates both the internal version field and the config's Package.Version.
|
||||
// Usage example: call value.SetVersion(...) from integrating code.
|
||||
func (s *SDK) SetVersion(version string) {
|
||||
s.version = version
|
||||
if s.config != nil {
|
||||
|
|
@ -78,6 +85,7 @@ func (s *SDK) SetVersion(version string) {
|
|||
}
|
||||
|
||||
// DefaultConfig returns sensible defaults for SDK configuration.
|
||||
// Usage example: call sdk.DefaultConfig(...) from integrating code.
|
||||
func DefaultConfig() *Config {
|
||||
return &Config{
|
||||
Languages: []string{"typescript", "python", "go", "php"},
|
||||
|
|
@ -90,6 +98,7 @@ func DefaultConfig() *Config {
|
|||
}
|
||||
|
||||
// Generate generates SDKs for all configured languages.
|
||||
// Usage example: call value.Generate(...) from integrating code.
|
||||
func (s *SDK) Generate(ctx context.Context) error {
|
||||
// Generate for each language
|
||||
for _, lang := range s.config.Languages {
|
||||
|
|
@ -102,6 +111,7 @@ func (s *SDK) Generate(ctx context.Context) error {
|
|||
}
|
||||
|
||||
// GenerateLanguage generates SDK for a specific language.
|
||||
// Usage example: call value.GenerateLanguage(...) from integrating code.
|
||||
func (s *SDK) GenerateLanguage(ctx context.Context, lang string) error {
|
||||
specPath, err := s.DetectSpec()
|
||||
if err != nil {
|
||||
|
|
@ -120,11 +130,11 @@ func (s *SDK) GenerateLanguage(ctx context.Context, lang string) error {
|
|||
}
|
||||
|
||||
if !gen.Available() {
|
||||
fmt.Printf("Warning: %s generator not available. Install with: %s\n", lang, gen.Install())
|
||||
fmt.Printf("Falling back to Docker...\n")
|
||||
core.Print(nil, "Warning: %s generator not available. Install with: %s", lang, gen.Install())
|
||||
core.Print(nil, "Falling back to Docker...")
|
||||
}
|
||||
|
||||
outputDir := filepath.Join(s.projectDir, s.config.Output, lang)
|
||||
outputDir := ax.Join(s.projectDir, s.config.Output, lang)
|
||||
opts := generators.Options{
|
||||
SpecPath: specPath,
|
||||
OutputDir: outputDir,
|
||||
|
|
@ -132,11 +142,11 @@ func (s *SDK) GenerateLanguage(ctx context.Context, lang string) error {
|
|||
Version: s.config.Package.Version,
|
||||
}
|
||||
|
||||
fmt.Printf("Generating %s SDK...\n", lang)
|
||||
core.Print(nil, "Generating %s SDK...", lang)
|
||||
if err := gen.Generate(ctx, opts); err != nil {
|
||||
return coreerr.E("sdk.GenerateLanguage", lang+" generation failed", err)
|
||||
}
|
||||
fmt.Printf("Generated %s SDK at %s\n", lang, outputDir)
|
||||
core.Print(nil, "Generated %s SDK at %s", lang, outputDir)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,22 +2,22 @@ package sdk
|
|||
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"dappco.re/go/core/build/internal/ax"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestSDK_Good_SetVersion(t *testing.T) {
|
||||
func TestSDK_SetVersion_Good(t *testing.T) {
|
||||
s := New("/tmp", nil)
|
||||
s.SetVersion("v1.2.3")
|
||||
|
||||
assert.Equal(t, "v1.2.3", s.version)
|
||||
}
|
||||
|
||||
func TestSDK_Good_VersionPassedToGenerator(t *testing.T) {
|
||||
func TestSDK_VersionPassedToGenerator_Good(t *testing.T) {
|
||||
config := &Config{
|
||||
Languages: []string{"typescript"},
|
||||
Output: "sdk",
|
||||
|
|
@ -31,14 +31,14 @@ func TestSDK_Good_VersionPassedToGenerator(t *testing.T) {
|
|||
assert.Equal(t, "v2.0.0", s.config.Package.Version)
|
||||
}
|
||||
|
||||
func TestDefaultConfig(t *testing.T) {
|
||||
func TestSDK_DefaultConfig_Good(t *testing.T) {
|
||||
cfg := DefaultConfig()
|
||||
assert.Contains(t, cfg.Languages, "typescript")
|
||||
assert.Equal(t, "sdk", cfg.Output)
|
||||
assert.True(t, cfg.Diff.Enabled)
|
||||
}
|
||||
|
||||
func TestSDK_New(t *testing.T) {
|
||||
func TestSDK_New_Good(t *testing.T) {
|
||||
t.Run("with nil config", func(t *testing.T) {
|
||||
s := New("/tmp", nil)
|
||||
assert.NotNil(t, s.config)
|
||||
|
|
@ -58,9 +58,9 @@ func TestSDK_GenerateLanguage_Bad(t *testing.T) {
|
|||
|
||||
tmpDir := t.TempDir()
|
||||
|
||||
specPath := filepath.Join(tmpDir, "openapi.yaml")
|
||||
specPath := ax.Join(tmpDir, "openapi.yaml")
|
||||
|
||||
err := os.WriteFile(specPath, []byte("openapi: 3.0.0"), 0644)
|
||||
err := ax.WriteFile(specPath, []byte("openapi: 3.0.0"), 0644)
|
||||
|
||||
require.NoError(t, err)
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue