* 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>
546 lines
14 KiB
Go
546 lines
14 KiB
Go
package publishers
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"os"
|
|
"os/exec"
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/host-uk/core/pkg/build"
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
func TestParseGitHubRepo_Good(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
input string
|
|
expected string
|
|
}{
|
|
{
|
|
name: "SSH URL",
|
|
input: "git@github.com:owner/repo.git",
|
|
expected: "owner/repo",
|
|
},
|
|
{
|
|
name: "HTTPS URL with .git",
|
|
input: "https://github.com/owner/repo.git",
|
|
expected: "owner/repo",
|
|
},
|
|
{
|
|
name: "HTTPS URL without .git",
|
|
input: "https://github.com/owner/repo",
|
|
expected: "owner/repo",
|
|
},
|
|
{
|
|
name: "SSH URL without .git",
|
|
input: "git@github.com:owner/repo",
|
|
expected: "owner/repo",
|
|
},
|
|
}
|
|
|
|
for _, tc := range tests {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
result, err := parseGitHubRepo(tc.input)
|
|
assert.NoError(t, err)
|
|
assert.Equal(t, tc.expected, result)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestParseGitHubRepo_Bad(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
input string
|
|
}{
|
|
{
|
|
name: "GitLab URL",
|
|
input: "https://gitlab.com/owner/repo.git",
|
|
},
|
|
{
|
|
name: "Bitbucket URL",
|
|
input: "git@bitbucket.org:owner/repo.git",
|
|
},
|
|
{
|
|
name: "Random URL",
|
|
input: "https://example.com/something",
|
|
},
|
|
{
|
|
name: "Not a URL",
|
|
input: "owner/repo",
|
|
},
|
|
}
|
|
|
|
for _, tc := range tests {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
_, err := parseGitHubRepo(tc.input)
|
|
assert.Error(t, err)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestGitHubPublisher_Name_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) {
|
|
t.Run("creates release struct", func(t *testing.T) {
|
|
r := NewRelease("v1.0.0", nil, "changelog", "/project")
|
|
assert.Equal(t, "v1.0.0", r.Version)
|
|
assert.Equal(t, "changelog", r.Changelog)
|
|
assert.Equal(t, "/project", r.ProjectDir)
|
|
assert.Nil(t, r.Artifacts)
|
|
})
|
|
}
|
|
|
|
func TestNewPublisherConfig_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)
|
|
assert.True(t, cfg.Prerelease)
|
|
assert.False(t, cfg.Draft)
|
|
assert.Nil(t, cfg.Extended)
|
|
})
|
|
|
|
t.Run("creates config with extended", func(t *testing.T) {
|
|
ext := map[string]any{"key": "value"}
|
|
cfg := NewPublisherConfig("docker", false, false, ext)
|
|
assert.Equal(t, "docker", cfg.Type)
|
|
assert.Equal(t, ext, cfg.Extended)
|
|
})
|
|
}
|
|
|
|
func TestBuildCreateArgs_Good(t *testing.T) {
|
|
p := NewGitHubPublisher()
|
|
|
|
t.Run("basic args", func(t *testing.T) {
|
|
release := &Release{
|
|
Version: "v1.0.0",
|
|
Changelog: "## v1.0.0\n\nChanges",
|
|
}
|
|
cfg := PublisherConfig{
|
|
Type: "github",
|
|
}
|
|
|
|
args := p.buildCreateArgs(release, cfg, "owner/repo")
|
|
|
|
assert.Contains(t, args, "release")
|
|
assert.Contains(t, args, "create")
|
|
assert.Contains(t, args, "v1.0.0")
|
|
assert.Contains(t, args, "--repo")
|
|
assert.Contains(t, args, "owner/repo")
|
|
assert.Contains(t, args, "--title")
|
|
assert.Contains(t, args, "--notes")
|
|
})
|
|
|
|
t.Run("with draft flag", func(t *testing.T) {
|
|
release := &Release{
|
|
Version: "v1.0.0",
|
|
}
|
|
cfg := PublisherConfig{
|
|
Type: "github",
|
|
Draft: true,
|
|
}
|
|
|
|
args := p.buildCreateArgs(release, cfg, "owner/repo")
|
|
|
|
assert.Contains(t, args, "--draft")
|
|
})
|
|
|
|
t.Run("with prerelease flag", func(t *testing.T) {
|
|
release := &Release{
|
|
Version: "v1.0.0",
|
|
}
|
|
cfg := PublisherConfig{
|
|
Type: "github",
|
|
Prerelease: true,
|
|
}
|
|
|
|
args := p.buildCreateArgs(release, cfg, "owner/repo")
|
|
|
|
assert.Contains(t, args, "--prerelease")
|
|
})
|
|
|
|
t.Run("generates notes when no changelog", func(t *testing.T) {
|
|
release := &Release{
|
|
Version: "v1.0.0",
|
|
Changelog: "",
|
|
}
|
|
cfg := PublisherConfig{
|
|
Type: "github",
|
|
}
|
|
|
|
args := p.buildCreateArgs(release, cfg, "owner/repo")
|
|
|
|
assert.Contains(t, args, "--generate-notes")
|
|
})
|
|
|
|
t.Run("with draft and prerelease flags", func(t *testing.T) {
|
|
release := &Release{
|
|
Version: "v1.0.0-alpha",
|
|
}
|
|
cfg := PublisherConfig{
|
|
Type: "github",
|
|
Draft: true,
|
|
Prerelease: true,
|
|
}
|
|
|
|
args := p.buildCreateArgs(release, cfg, "owner/repo")
|
|
|
|
assert.Contains(t, args, "--draft")
|
|
assert.Contains(t, args, "--prerelease")
|
|
})
|
|
|
|
t.Run("without repo includes version", func(t *testing.T) {
|
|
release := &Release{
|
|
Version: "v2.0.0",
|
|
Changelog: "Some changes",
|
|
}
|
|
cfg := PublisherConfig{
|
|
Type: "github",
|
|
}
|
|
|
|
args := p.buildCreateArgs(release, cfg, "")
|
|
|
|
assert.Contains(t, args, "release")
|
|
assert.Contains(t, args, "create")
|
|
assert.Contains(t, args, "v2.0.0")
|
|
assert.NotContains(t, args, "--repo")
|
|
})
|
|
}
|
|
|
|
func TestGitHubPublisher_DryRunPublish_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",
|
|
ProjectDir: "/project",
|
|
}
|
|
cfg := PublisherConfig{
|
|
Type: "github",
|
|
Draft: false,
|
|
Prerelease: false,
|
|
}
|
|
|
|
err := p.dryRunPublish(release, cfg, "owner/repo")
|
|
|
|
_ = w.Close()
|
|
var buf bytes.Buffer
|
|
_, _ = buf.ReadFrom(r)
|
|
os.Stdout = oldStdout
|
|
|
|
require.NoError(t, err)
|
|
output := buf.String()
|
|
|
|
assert.Contains(t, output, "DRY RUN: GitHub Release")
|
|
assert.Contains(t, output, "Repository: owner/repo")
|
|
assert.Contains(t, output, "Version: v1.0.0")
|
|
assert.Contains(t, output, "Draft: false")
|
|
assert.Contains(t, output, "Prerelease: false")
|
|
assert.Contains(t, output, "Would create release with command:")
|
|
assert.Contains(t, output, "gh release create")
|
|
assert.Contains(t, output, "Changelog:")
|
|
assert.Contains(t, output, "## Changes")
|
|
assert.Contains(t, output, "END DRY RUN")
|
|
})
|
|
|
|
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",
|
|
ProjectDir: "/project",
|
|
Artifacts: []build.Artifact{
|
|
{Path: "/dist/myapp-darwin-amd64.tar.gz"},
|
|
{Path: "/dist/myapp-linux-amd64.tar.gz"},
|
|
},
|
|
}
|
|
cfg := PublisherConfig{Type: "github"}
|
|
|
|
err := p.dryRunPublish(release, cfg, "owner/repo")
|
|
|
|
_ = w.Close()
|
|
var buf bytes.Buffer
|
|
_, _ = buf.ReadFrom(r)
|
|
os.Stdout = oldStdout
|
|
|
|
require.NoError(t, err)
|
|
output := buf.String()
|
|
|
|
assert.Contains(t, output, "Would upload artifacts:")
|
|
assert.Contains(t, output, "myapp-darwin-amd64.tar.gz")
|
|
assert.Contains(t, output, "myapp-linux-amd64.tar.gz")
|
|
})
|
|
|
|
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",
|
|
ProjectDir: "/project",
|
|
}
|
|
cfg := PublisherConfig{
|
|
Type: "github",
|
|
Draft: true,
|
|
Prerelease: true,
|
|
}
|
|
|
|
err := p.dryRunPublish(release, cfg, "owner/repo")
|
|
|
|
_ = w.Close()
|
|
var buf bytes.Buffer
|
|
_, _ = buf.ReadFrom(r)
|
|
os.Stdout = oldStdout
|
|
|
|
require.NoError(t, err)
|
|
output := buf.String()
|
|
|
|
assert.Contains(t, output, "Draft: true")
|
|
assert.Contains(t, output, "Prerelease: true")
|
|
assert.Contains(t, output, "--draft")
|
|
assert.Contains(t, output, "--prerelease")
|
|
})
|
|
}
|
|
|
|
func TestGitHubPublisher_Publish_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",
|
|
ProjectDir: "/tmp",
|
|
}
|
|
pubCfg := PublisherConfig{Type: "github"}
|
|
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
|
|
|
|
require.NoError(t, err)
|
|
output := buf.String()
|
|
assert.Contains(t, output, "Repository: custom/repo")
|
|
})
|
|
}
|
|
|
|
func TestGitHubPublisher_Publish_Bad(t *testing.T) {
|
|
if testing.Short() {
|
|
t.Skip("skipping integration test in short mode")
|
|
}
|
|
|
|
p := NewGitHubPublisher()
|
|
|
|
t.Run("fails when gh CLI not available and not dry run", func(t *testing.T) {
|
|
// This test will fail if gh is installed but not authenticated
|
|
// or succeed if gh is not installed
|
|
release := &Release{
|
|
Version: "v1.0.0",
|
|
Changelog: "Changes",
|
|
ProjectDir: "/nonexistent",
|
|
}
|
|
pubCfg := PublisherConfig{Type: "github"}
|
|
relCfg := &mockReleaseConfig{repository: "owner/repo"}
|
|
|
|
err := p.Publish(context.Background(), release, pubCfg, relCfg, false)
|
|
|
|
// Should fail due to either gh not found or not authenticated
|
|
assert.Error(t, err)
|
|
})
|
|
|
|
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) }()
|
|
|
|
release := &Release{
|
|
Version: "v1.0.0",
|
|
Changelog: "Changes",
|
|
ProjectDir: tmpDir,
|
|
}
|
|
pubCfg := PublisherConfig{Type: "github"}
|
|
relCfg := &mockReleaseConfig{repository: ""} // Empty repository
|
|
|
|
err = p.Publish(context.Background(), release, pubCfg, relCfg, true)
|
|
|
|
// Should fail because detectRepository will fail on non-git dir
|
|
assert.Error(t, err)
|
|
assert.Contains(t, err.Error(), "could not determine repository")
|
|
})
|
|
}
|
|
|
|
func TestDetectRepository_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) }()
|
|
|
|
// 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())
|
|
|
|
repo, err := detectRepository(tmpDir)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, "test-owner/test-repo", repo)
|
|
})
|
|
|
|
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())
|
|
|
|
repo, err := detectRepository(tmpDir)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, "another-owner/another-repo", repo)
|
|
})
|
|
}
|
|
|
|
func TestDetectRepository_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) }()
|
|
|
|
_, err = detectRepository(tmpDir)
|
|
assert.Error(t, err)
|
|
assert.Contains(t, err.Error(), "failed to get git remote")
|
|
})
|
|
|
|
t.Run("fails when directory does not exist", func(t *testing.T) {
|
|
_, err := detectRepository("/nonexistent/directory/that/does/not/exist")
|
|
assert.Error(t, err)
|
|
})
|
|
|
|
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) }()
|
|
|
|
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)
|
|
assert.Error(t, err)
|
|
assert.Contains(t, err.Error(), "not a GitHub URL")
|
|
})
|
|
}
|
|
|
|
func TestValidateGhCli_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
|
|
t.Run("returns error when gh not installed", func(t *testing.T) {
|
|
// We can't force gh to not be installed, but we can verify
|
|
// the function signature works correctly
|
|
err := validateGhCli()
|
|
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"),
|
|
"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) {
|
|
// 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")
|
|
}
|
|
|
|
p := NewGitHubPublisher()
|
|
|
|
t.Run("executePublish builds command with artifacts", func(t *testing.T) {
|
|
// We test the command building by checking that it fails appropriately
|
|
// with a non-existent release (rather than testing actual release creation)
|
|
release := &Release{
|
|
Version: "v999.999.999-test-nonexistent",
|
|
Changelog: "Test changelog",
|
|
ProjectDir: "/tmp",
|
|
Artifacts: []build.Artifact{
|
|
{Path: "/tmp/nonexistent-artifact.tar.gz"},
|
|
},
|
|
}
|
|
cfg := PublisherConfig{
|
|
Type: "github",
|
|
Draft: true,
|
|
Prerelease: true,
|
|
}
|
|
|
|
// This will fail because the artifact doesn't exist, but it proves
|
|
// the code path runs
|
|
err := p.executePublish(context.Background(), release, cfg, "test-owner/test-repo-nonexistent")
|
|
assert.Error(t, err) // Expected to fail
|
|
})
|
|
}
|
|
|
|
func TestReleaseExists_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")
|
|
}
|
|
|
|
t.Run("returns false for non-existent release", func(t *testing.T) {
|
|
ctx := context.Background()
|
|
// Use a non-existent repo and version
|
|
exists := ReleaseExists(ctx, "nonexistent-owner-12345/nonexistent-repo-67890", "v999.999.999")
|
|
assert.False(t, exists)
|
|
})
|
|
|
|
t.Run("checks release existence", func(t *testing.T) {
|
|
ctx := context.Background()
|
|
// Test against a known public repository with releases
|
|
// This tests the true path if the release exists
|
|
exists := ReleaseExists(ctx, "cli/cli", "v2.0.0")
|
|
// We don't assert the result since it depends on network access
|
|
// and the release may or may not exist
|
|
_ = exists // Just verify function runs without panic
|
|
})
|
|
}
|