cli/pkg/release/release_test.go

701 lines
20 KiB
Go
Raw Normal View History

package release
import (
"context"
"os"
"os/exec"
"path/filepath"
"testing"
"github.com/host-uk/core/pkg/build"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestFindArtifacts_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))
// 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))
artifacts, err := findArtifacts(distDir)
require.NoError(t, err)
assert.Len(t, artifacts, 2)
})
t.Run("finds zip artifacts", 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-windows-amd64.zip"), []byte("test"), 0644))
artifacts, err := findArtifacts(distDir)
require.NoError(t, err)
assert.Len(t, artifacts, 1)
assert.Contains(t, artifacts[0].Path, "app-windows-amd64.zip")
})
t.Run("finds checksum files", 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, "CHECKSUMS.txt"), []byte("checksums"), 0644))
artifacts, err := findArtifacts(distDir)
require.NoError(t, err)
assert.Len(t, artifacts, 1)
assert.Contains(t, artifacts[0].Path, "CHECKSUMS.txt")
})
t.Run("finds signature files", 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.sig"), []byte("signature"), 0644))
artifacts, err := findArtifacts(distDir)
require.NoError(t, err)
assert.Len(t, artifacts, 1)
})
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))
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))
artifacts, err := findArtifacts(distDir)
require.NoError(t, err)
assert.Len(t, artifacts, 4)
})
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))
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))
artifacts, err := findArtifacts(distDir)
require.NoError(t, err)
assert.Len(t, artifacts, 1)
assert.Contains(t, artifacts[0].Path, "app.tar.gz")
})
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))
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))
artifacts, err := findArtifacts(distDir)
require.NoError(t, err)
// Should only find the top-level artifact
assert.Len(t, artifacts, 1)
})
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))
artifacts, err := findArtifacts(distDir)
require.NoError(t, err)
assert.Empty(t, artifacts)
})
}
func TestFindArtifacts_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")
_, err := findArtifacts(distDir)
assert.Error(t, err)
assert.Contains(t, err.Error(), "dist/ directory not found")
})
t.Run("returns error when dist directory is unreadable", func(t *testing.T) {
dir := t.TempDir()
distDir := filepath.Join(dir, "dist")
require.NoError(t, os.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) }()
_, err := findArtifacts(distDir)
assert.Error(t, err)
assert.Contains(t, err.Error(), "failed to read dist/")
})
}
func TestGetBuilder_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)
assert.NotNil(t, builder)
assert.Equal(t, "go", builder.Name())
})
t.Run("returns Wails builder for wails project type", func(t *testing.T) {
builder, err := getBuilder(build.ProjectTypeWails)
require.NoError(t, err)
assert.NotNil(t, builder)
assert.Equal(t, "wails", builder.Name())
})
}
func TestGetBuilder_Bad(t *testing.T) {
t.Run("returns error for Node project type", func(t *testing.T) {
_, err := getBuilder(build.ProjectTypeNode)
assert.Error(t, err)
feat: infrastructure packages and lint cleanup (#281) * ci: consolidate duplicate workflows and merge CodeQL configs Remove 17 duplicate workflow files that were split copies of the combined originals. Each family (CI, CodeQL, Coverage, PR Build, Alpha Release) had the same job duplicated across separate push/pull_request/schedule/manual trigger files. Merge codeql.yml and codescan.yml into a single codeql.yml with a language matrix covering go, javascript-typescript, python, and actions — matching the previous default setup coverage. Remaining workflows (one per family): - ci.yml (push + PR + manual) - codeql.yml (push + PR + schedule, all languages) - coverage.yml (push + PR + manual) - alpha-release.yml (push + manual) - pr-build.yml (PR + manual) - release.yml (tag push) - agent-verify.yml, auto-label.yml, auto-project.yml Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * feat: add collect, config, crypt, plugin packages and fix all lint issues Add four new infrastructure packages with CLI commands: - pkg/config: layered configuration (defaults → file → env → flags) - pkg/crypt: crypto primitives (Argon2id, AES-GCM, ChaCha20, HMAC, checksums) - pkg/plugin: plugin system with GitHub-based install/update/remove - pkg/collect: collection subsystem (GitHub, BitcoinTalk, market, papers, excavate) Fix all golangci-lint issues across the entire codebase (~100 errcheck, staticcheck SA1012/SA1019/ST1005, unused, ineffassign fixes) so that `core go qa` passes with 0 issues. Closes #167, #168, #170, #250, #251, #252, #253, #254, #255, #256 Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-04 11:34:43 +00:00
assert.Contains(t, err.Error(), "node.js builder not yet implemented")
})
t.Run("returns error for PHP project type", func(t *testing.T) {
_, err := getBuilder(build.ProjectTypePHP)
assert.Error(t, err)
assert.Contains(t, err.Error(), "PHP builder not yet implemented")
})
t.Run("returns error for unsupported project type", func(t *testing.T) {
_, err := getBuilder(build.ProjectType("unknown"))
assert.Error(t, err)
assert.Contains(t, err.Error(), "unsupported project type")
})
}
func TestGetPublisher_Good(t *testing.T) {
tests := []struct {
pubType string
expectedName string
}{
{"github", "github"},
{"linuxkit", "linuxkit"},
{"docker", "docker"},
{"npm", "npm"},
{"homebrew", "homebrew"},
{"scoop", "scoop"},
{"aur", "aur"},
{"chocolatey", "chocolatey"},
}
for _, tc := range tests {
t.Run(tc.pubType, func(t *testing.T) {
publisher, err := getPublisher(tc.pubType)
require.NoError(t, err)
assert.NotNil(t, publisher)
assert.Equal(t, tc.expectedName, publisher.Name())
})
}
}
func TestGetPublisher_Bad(t *testing.T) {
t.Run("returns error for unsupported publisher type", func(t *testing.T) {
_, err := getPublisher("unsupported")
assert.Error(t, err)
assert.Contains(t, err.Error(), "unsupported publisher type: unsupported")
})
t.Run("returns error for empty publisher type", func(t *testing.T) {
_, err := getPublisher("")
assert.Error(t, err)
assert.Contains(t, err.Error(), "unsupported publisher type")
})
}
func TestBuildExtendedConfig_Good(t *testing.T) {
t.Run("returns empty map for minimal config", func(t *testing.T) {
cfg := PublisherConfig{
Type: "github",
}
ext := buildExtendedConfig(cfg)
assert.Empty(t, ext)
})
t.Run("includes LinuxKit config", func(t *testing.T) {
cfg := PublisherConfig{
Type: "linuxkit",
Config: "linuxkit.yaml",
Formats: []string{"iso", "qcow2"},
Platforms: []string{"linux/amd64", "linux/arm64"},
}
ext := buildExtendedConfig(cfg)
assert.Equal(t, "linuxkit.yaml", ext["config"])
assert.Equal(t, []any{"iso", "qcow2"}, ext["formats"])
assert.Equal(t, []any{"linux/amd64", "linux/arm64"}, ext["platforms"])
})
t.Run("includes Docker config", func(t *testing.T) {
cfg := PublisherConfig{
Type: "docker",
Registry: "ghcr.io",
Image: "owner/repo",
Dockerfile: "Dockerfile.prod",
Tags: []string{"latest", "v1.0.0"},
BuildArgs: map[string]string{"VERSION": "1.0.0"},
}
ext := buildExtendedConfig(cfg)
assert.Equal(t, "ghcr.io", ext["registry"])
assert.Equal(t, "owner/repo", ext["image"])
assert.Equal(t, "Dockerfile.prod", ext["dockerfile"])
assert.Equal(t, []any{"latest", "v1.0.0"}, ext["tags"])
buildArgs := ext["build_args"].(map[string]any)
assert.Equal(t, "1.0.0", buildArgs["VERSION"])
})
t.Run("includes npm config", func(t *testing.T) {
cfg := PublisherConfig{
Type: "npm",
Package: "@host-uk/core",
Access: "public",
}
ext := buildExtendedConfig(cfg)
assert.Equal(t, "@host-uk/core", ext["package"])
assert.Equal(t, "public", ext["access"])
})
t.Run("includes Homebrew config", func(t *testing.T) {
cfg := PublisherConfig{
Type: "homebrew",
Tap: "host-uk/tap",
Formula: "core",
}
ext := buildExtendedConfig(cfg)
assert.Equal(t, "host-uk/tap", ext["tap"])
assert.Equal(t, "core", ext["formula"])
})
t.Run("includes Scoop config", func(t *testing.T) {
cfg := PublisherConfig{
Type: "scoop",
Bucket: "host-uk/bucket",
}
ext := buildExtendedConfig(cfg)
assert.Equal(t, "host-uk/bucket", ext["bucket"])
})
t.Run("includes AUR config", func(t *testing.T) {
cfg := PublisherConfig{
Type: "aur",
Maintainer: "John Doe <john@example.com>",
}
ext := buildExtendedConfig(cfg)
assert.Equal(t, "John Doe <john@example.com>", ext["maintainer"])
})
t.Run("includes Chocolatey config", func(t *testing.T) {
cfg := PublisherConfig{
Type: "chocolatey",
Push: true,
}
ext := buildExtendedConfig(cfg)
assert.True(t, ext["push"].(bool))
})
t.Run("includes Official config", func(t *testing.T) {
cfg := PublisherConfig{
Type: "homebrew",
Official: &OfficialConfig{
Enabled: true,
Output: "/path/to/output",
},
}
ext := buildExtendedConfig(cfg)
official := ext["official"].(map[string]any)
assert.True(t, official["enabled"].(bool))
assert.Equal(t, "/path/to/output", official["output"])
})
t.Run("Official config without output", func(t *testing.T) {
cfg := PublisherConfig{
Type: "scoop",
Official: &OfficialConfig{
Enabled: true,
},
}
ext := buildExtendedConfig(cfg)
official := ext["official"].(map[string]any)
assert.True(t, official["enabled"].(bool))
_, hasOutput := official["output"]
assert.False(t, hasOutput)
})
}
func TestToAnySlice_Good(t *testing.T) {
t.Run("converts string slice to any slice", func(t *testing.T) {
input := []string{"a", "b", "c"}
result := toAnySlice(input)
assert.Len(t, result, 3)
assert.Equal(t, "a", result[0])
assert.Equal(t, "b", result[1])
assert.Equal(t, "c", result[2])
})
t.Run("handles empty slice", func(t *testing.T) {
input := []string{}
result := toAnySlice(input)
assert.Empty(t, result)
})
t.Run("handles single element", func(t *testing.T) {
input := []string{"only"}
result := toAnySlice(input)
assert.Len(t, result, 1)
assert.Equal(t, "only", result[0])
})
}
func TestPublish_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))
cfg := DefaultConfig()
cfg.SetProjectDir(dir)
cfg.SetVersion("v1.0.0")
cfg.Publishers = nil // No publishers to avoid network calls
release, err := Publish(context.Background(), cfg, true)
require.NoError(t, err)
assert.Equal(t, "v1.0.0", release.Version)
assert.Len(t, release.Artifacts, 1)
})
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))
cfg := DefaultConfig()
cfg.SetProjectDir(dir)
cfg.SetVersion("v1.0.0")
cfg.Publishers = nil
release, err := Publish(context.Background(), cfg, true)
require.NoError(t, err)
assert.Len(t, release.Artifacts, 3)
})
}
func TestPublish_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)
assert.Contains(t, err.Error(), "config is nil")
})
t.Run("returns error when dist directory missing", func(t *testing.T) {
dir := t.TempDir()
cfg := DefaultConfig()
cfg.SetProjectDir(dir)
cfg.SetVersion("v1.0.0")
_, err := Publish(context.Background(), cfg, true)
assert.Error(t, err)
assert.Contains(t, err.Error(), "dist/ directory not found")
})
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))
cfg := DefaultConfig()
cfg.SetProjectDir(dir)
cfg.SetVersion("v1.0.0")
_, err := Publish(context.Background(), cfg, true)
assert.Error(t, err)
assert.Contains(t, err.Error(), "no artifacts found")
})
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))
cfg := DefaultConfig()
cfg.SetProjectDir(dir)
cfg.SetVersion("v1.0.0")
cfg.Publishers = []PublisherConfig{
{Type: "unsupported"},
}
_, err := Publish(context.Background(), cfg, true)
assert.Error(t, err)
assert.Contains(t, err.Error(), "unsupported publisher type")
})
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))
cfg := DefaultConfig()
cfg.SetProjectDir(dir)
// Don't set version - let it try to determine from git
cfg.Publishers = nil
// In a non-git directory, DetermineVersion returns v0.0.1 as default
// so we verify that the publish proceeds without error
release, err := Publish(context.Background(), cfg, true)
require.NoError(t, err)
assert.Equal(t, "v0.0.1", release.Version)
})
}
func TestRun_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()
// Create go.mod
goMod := `module testapp
go 1.21
`
require.NoError(t, os.WriteFile(filepath.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))
cfg := DefaultConfig()
cfg.SetProjectDir(dir)
cfg.SetVersion("v1.0.0")
cfg.Project.Name = "testapp"
cfg.Build.Targets = []TargetConfig{} // Empty targets to use defaults
cfg.Publishers = nil // No publishers to avoid network calls
// Note: This test will actually try to build, which may fail in CI
// So we just test that the function accepts the config properly
release, err := Run(context.Background(), cfg, true)
if err != nil {
// Build might fail in test environment, but we still verify the error message
assert.Contains(t, err.Error(), "build")
} else {
assert.Equal(t, "v1.0.0", release.Version)
}
})
}
func TestRun_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)
assert.Contains(t, err.Error(), "config is nil")
})
}
func TestRelease_Structure(t *testing.T) {
t.Run("Release struct holds expected fields", func(t *testing.T) {
release := &Release{
Version: "v1.0.0",
Artifacts: []build.Artifact{{Path: "/path/to/artifact"}},
Changelog: "## v1.0.0\n\nChanges",
ProjectDir: "/project",
}
assert.Equal(t, "v1.0.0", release.Version)
assert.Len(t, release.Artifacts, 1)
assert.Contains(t, release.Changelog, "v1.0.0")
assert.Equal(t, "/project", release.ProjectDir)
})
}
func TestPublish_VersionFromGit(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))
cfg := DefaultConfig()
cfg.SetProjectDir(dir)
// Don't set version - let it be determined from git
cfg.Publishers = nil
release, err := Publish(context.Background(), cfg, true)
require.NoError(t, err)
assert.Equal(t, "v1.2.3", release.Version)
})
}
func TestPublish_ChangelogGeneration(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")
createPublishTag(t, dir, "v1.0.0")
createPublishCommit(t, dir, "fix: fix bug")
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))
cfg := DefaultConfig()
cfg.SetProjectDir(dir)
cfg.SetVersion("v1.0.1")
cfg.Publishers = nil
release, err := Publish(context.Background(), cfg, true)
require.NoError(t, err)
// Changelog should contain either the commit message or the version
assert.Contains(t, release.Changelog, "v1.0.1")
})
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))
cfg := DefaultConfig()
cfg.SetProjectDir(dir)
cfg.SetVersion("v1.0.0")
cfg.Publishers = nil
release, err := Publish(context.Background(), cfg, true)
require.NoError(t, err)
// Should use fallback changelog
assert.Contains(t, release.Changelog, "Release v1.0.0")
})
}
func TestPublish_DefaultProjectDir(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))
cfg := DefaultConfig()
cfg.SetProjectDir(dir)
cfg.SetVersion("v1.0.0")
cfg.Publishers = nil
release, err := Publish(context.Background(), cfg, true)
require.NoError(t, err)
assert.NotEmpty(t, release.ProjectDir)
})
}
// Helper functions for publish tests
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())
return dir
}
func createPublishCommit(t *testing.T, dir, message string) {
t.Helper()
filePath := filepath.Join(dir, "publish_test.txt")
content, _ := os.ReadFile(filePath)
content = append(content, []byte(message+"\n")...)
require.NoError(t, os.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())
}
func createPublishTag(t *testing.T, dir, tag string) {
t.Helper()
cmd := exec.Command("git", "tag", tag)
cmd.Dir = dir
require.NoError(t, cmd.Run())
}