fix(devops): address CodeRabbit findings on PR #2
15+ findings dispositioned. AX-6-conformant — no testify reintroduced.
Code fixes:
- cmd/dev/service.go: no-op now returns OK:true, unchecked prompt type assertion guarded
- cmd/workspace/config.go: relative parent traversal blocked + regression test, fmt.Errorf wrapping
- cmd/dev/cmd_issues.go + cmd_reviews.go: import ordering
- tests/cli/devops/main.go: raw WalkDir errors wrapped
- tests/cli/devops/Taskfile.yaml: strict shell flags
- cmd/vanity-import/Dockerfile + docs/development.md: Go 1.26 alignment
- locales/embed.go: missing dappco.re/go/i18n checksum
Test infra:
- New local test helpers in cmd/dev, cmd/setup, devkit, snapshot
- All testify usages already absent — local stdlib helpers preferred
per AX-6 ban
- Test naming aligned (Test{Filename}_{Function}_{Good,Bad,Ugly} per AX-10)
Disposition replies (RESOLVED-COMMENT, no testify added):
- cmd/dev/cmd_apply_test.go, cmd/setup/cmd_ci_test.go, snapshot_test.go,
devkit/coverage_test.go: CodeRabbit testify suggestions get reasoning
reply per AX-6 ban; local helpers are convention.
- SonarCloud/GHAS: no PR checks/annotations found; code-scanning API
returned no analysis, secret scanning disabled.
Verification: gofmt clean, git diff --check clean, no testify imports.
Targeted go vet + go test pass for cmd/workspace + devkit + snapshot.
Full go vet ./... blocked by pre-existing dappco.re/go/scm
codeberg.org/forgejo/go-sdk auth/replacement issue (out of scope).
Closes findings on https://github.com/dAppCore/go-devops/pull/2
Co-authored-by: Codex <noreply@openai.com>
This commit is contained in:
parent
c43090e2ca
commit
907c5fa64c
25 changed files with 387 additions and 121 deletions
|
|
@ -4,7 +4,7 @@ Infrastructure and build automation library for the Lethean ecosystem. Provides
|
|||
|
||||
**Module**: `forge.lthn.ai/core/go-devops`
|
||||
**Licence**: EUPL-1.2
|
||||
**Language**: Go 1.25
|
||||
**Language**: Go 1.26
|
||||
|
||||
## Quick Start
|
||||
|
||||
|
|
|
|||
|
|
@ -3,7 +3,6 @@ package dev
|
|||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"reflect"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
|
|
@ -81,7 +80,7 @@ func TestGeneratePublicAPITestFile_Good(t *testing.T) {
|
|||
mustTrue(t, strings.Contains(content, `const _ = impl.Answer`))
|
||||
}
|
||||
|
||||
func TestGetExportedSymbols_Good_MultiFile(t *testing.T) {
|
||||
func TestGetExportedSymbols_MultiFile_Good(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
|
||||
serviceDir := filepath.Join(tmpDir, "demo")
|
||||
|
|
@ -111,7 +110,5 @@ type Ignored struct{}
|
|||
{Name: "Run", Kind: "func"},
|
||||
{Name: "Value", Kind: "var"},
|
||||
}
|
||||
if !reflect.DeepEqual(want, symbols) {
|
||||
t.Fatalf("want %v, got %v", want, symbols)
|
||||
}
|
||||
mustDeepEqual(t, want, symbols)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
package dev
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"dappco.re/go/cli/pkg/cli"
|
||||
|
|
@ -14,30 +13,20 @@ func TestAddFileSyncCommand_Good(t *testing.T) {
|
|||
|
||||
syncCmd, _, err := root.Find([]string{"dev", "sync"})
|
||||
mustNoError(t, err)
|
||||
if syncCmd == nil {
|
||||
t.Fatal("expected non-nil sync command")
|
||||
}
|
||||
mustNotNil(t, syncCmd)
|
||||
|
||||
yesFlag := syncCmd.Flags().Lookup("yes")
|
||||
if yesFlag == nil {
|
||||
t.Fatal("expected yes flag")
|
||||
}
|
||||
mustNotNil(t, yesFlag)
|
||||
mustEqual(t, "y", yesFlag.Shorthand)
|
||||
|
||||
if syncCmd.Flags().Lookup("dry-run") == nil {
|
||||
t.Fatal("expected dry-run flag")
|
||||
}
|
||||
if syncCmd.Flags().Lookup("push") == nil {
|
||||
t.Fatal("expected push flag")
|
||||
}
|
||||
mustNotNil(t, syncCmd.Flags().Lookup("dry-run"))
|
||||
mustNotNil(t, syncCmd.Flags().Lookup("push"))
|
||||
}
|
||||
|
||||
func TestSplitPatterns_Good(t *testing.T) {
|
||||
patterns := splitPatterns("packages/core-*, apps/* ,services/*,")
|
||||
want := []string{"packages/core-*", "apps/*", "services/*"}
|
||||
if !reflect.DeepEqual(want, patterns) {
|
||||
t.Fatalf("want %v, got %v", want, patterns)
|
||||
}
|
||||
mustDeepEqual(t, want, patterns)
|
||||
}
|
||||
|
||||
func TestMatchGlob_Good(t *testing.T) {
|
||||
|
|
|
|||
|
|
@ -5,10 +5,10 @@ import (
|
|||
"strings"
|
||||
"time"
|
||||
|
||||
"code.gitea.io/sdk/gitea"
|
||||
|
||||
"dappco.re/go/cli/pkg/cli"
|
||||
"dappco.re/go/i18n"
|
||||
|
||||
"code.gitea.io/sdk/gitea"
|
||||
)
|
||||
|
||||
// Issue-specific styles (aliases to shared)
|
||||
|
|
|
|||
|
|
@ -5,10 +5,10 @@ import (
|
|||
"strings"
|
||||
"time"
|
||||
|
||||
"code.gitea.io/sdk/gitea"
|
||||
|
||||
"dappco.re/go/cli/pkg/cli"
|
||||
"dappco.re/go/i18n"
|
||||
|
||||
"code.gitea.io/sdk/gitea"
|
||||
)
|
||||
|
||||
// PR-specific styles (aliases to shared)
|
||||
|
|
|
|||
|
|
@ -13,28 +13,12 @@ func TestAddVMStatusCommand_Good(t *testing.T) {
|
|||
|
||||
statusCmd, _, err := root.Find([]string{"dev", "status"})
|
||||
mustNoError(t, err)
|
||||
if statusCmd == nil {
|
||||
t.Fatal("expected non-nil status command")
|
||||
}
|
||||
mustNotNil(t, statusCmd)
|
||||
mustEqual(t, "status", statusCmd.Use)
|
||||
mustContainsAlias(t, statusCmd.Aliases, "vm-status")
|
||||
mustContainsString(t, statusCmd.Aliases, "vm-status")
|
||||
|
||||
aliasCmd, _, err := root.Find([]string{"dev", "vm-status"})
|
||||
mustNoError(t, err)
|
||||
if aliasCmd == nil {
|
||||
t.Fatal("expected non-nil alias command")
|
||||
}
|
||||
if statusCmd != aliasCmd {
|
||||
t.Fatalf("want alias to be same command, got %v vs %v", statusCmd, aliasCmd)
|
||||
}
|
||||
}
|
||||
|
||||
func mustContainsAlias(t *testing.T, haystack []string, needle string) {
|
||||
t.Helper()
|
||||
for _, s := range haystack {
|
||||
if s == needle {
|
||||
return
|
||||
}
|
||||
}
|
||||
t.Fatalf("expected %v to contain %q", haystack, needle)
|
||||
mustNotNil(t, aliasCmd)
|
||||
mustTrue(t, statusCmd == aliasCmd)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -13,27 +13,18 @@ func TestFindWorkflows_Good(t *testing.T) {
|
|||
// Create a temp directory with workflow files
|
||||
tmpDir := t.TempDir()
|
||||
workflowsDir := filepath.Join(tmpDir, ".github", "workflows")
|
||||
if err := io.Local.EnsureDir(workflowsDir); err != nil {
|
||||
t.Fatalf("Failed to create workflows dir: %v", err)
|
||||
}
|
||||
mustNoError(t, io.Local.EnsureDir(workflowsDir))
|
||||
|
||||
// Create some workflow files
|
||||
for _, name := range []string{"qa.yml", "tests.yml", "codeql.yaml"} {
|
||||
if err := io.Local.Write(filepath.Join(workflowsDir, name), "name: Test"); err != nil {
|
||||
t.Fatalf("Failed to create workflow file: %v", err)
|
||||
}
|
||||
mustNoError(t, io.Local.Write(filepath.Join(workflowsDir, name), "name: Test"))
|
||||
}
|
||||
|
||||
// Create a non-workflow file (should be ignored)
|
||||
if err := io.Local.Write(filepath.Join(workflowsDir, "readme.md"), "# Workflows"); err != nil {
|
||||
t.Fatalf("Failed to create readme file: %v", err)
|
||||
}
|
||||
mustNoError(t, io.Local.Write(filepath.Join(workflowsDir, "readme.md"), "# Workflows"))
|
||||
|
||||
workflows := findWorkflows(tmpDir)
|
||||
|
||||
if len(workflows) != 3 {
|
||||
t.Errorf("Expected 3 workflows, got %d", len(workflows))
|
||||
}
|
||||
mustLen(t, workflows, 3)
|
||||
|
||||
// Check that all expected workflows are found
|
||||
found := make(map[string]bool)
|
||||
|
|
@ -42,71 +33,51 @@ func TestFindWorkflows_Good(t *testing.T) {
|
|||
}
|
||||
|
||||
for _, expected := range []string{"qa.yml", "tests.yml", "codeql.yaml"} {
|
||||
if !found[expected] {
|
||||
t.Errorf("Expected to find workflow %s", expected)
|
||||
}
|
||||
mustTrue(t, found[expected])
|
||||
}
|
||||
}
|
||||
|
||||
func TestFindWorkflows_NoWorkflowsDir(t *testing.T) {
|
||||
func TestFindWorkflows_NoWorkflowsDir_Bad(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
workflows := findWorkflows(tmpDir)
|
||||
|
||||
if len(workflows) != 0 {
|
||||
t.Errorf("Expected 0 workflows for non-existent dir, got %d", len(workflows))
|
||||
}
|
||||
mustLen(t, workflows, 0)
|
||||
}
|
||||
|
||||
func TestFindTemplateWorkflow_Good(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
templatesDir := filepath.Join(tmpDir, ".github", "workflow-templates")
|
||||
if err := io.Local.EnsureDir(templatesDir); err != nil {
|
||||
t.Fatalf("Failed to create templates dir: %v", err)
|
||||
}
|
||||
mustNoError(t, io.Local.EnsureDir(templatesDir))
|
||||
|
||||
templateContent := "name: QA\non: [push]"
|
||||
if err := io.Local.Write(filepath.Join(templatesDir, "qa.yml"), templateContent); err != nil {
|
||||
t.Fatalf("Failed to create template file: %v", err)
|
||||
}
|
||||
mustNoError(t, io.Local.Write(filepath.Join(templatesDir, "qa.yml"), templateContent))
|
||||
|
||||
// Test finding with .yml extension
|
||||
result := findTemplateWorkflow(tmpDir, "qa.yml")
|
||||
if result == "" {
|
||||
t.Error("Expected to find qa.yml template")
|
||||
}
|
||||
mustTrue(t, result != "")
|
||||
|
||||
// Test finding without extension (should auto-add .yml)
|
||||
result = findTemplateWorkflow(tmpDir, "qa")
|
||||
if result == "" {
|
||||
t.Error("Expected to find qa template without extension")
|
||||
}
|
||||
mustTrue(t, result != "")
|
||||
}
|
||||
|
||||
func TestFindTemplateWorkflow_FallbackToWorkflows(t *testing.T) {
|
||||
func TestFindTemplateWorkflow_FallbackToWorkflows_Good(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
workflowsDir := filepath.Join(tmpDir, ".github", "workflows")
|
||||
if err := io.Local.EnsureDir(workflowsDir); err != nil {
|
||||
t.Fatalf("Failed to create workflows dir: %v", err)
|
||||
}
|
||||
mustNoError(t, io.Local.EnsureDir(workflowsDir))
|
||||
|
||||
templateContent := "name: Tests\non: [push]"
|
||||
if err := io.Local.Write(filepath.Join(workflowsDir, "tests.yml"), templateContent); err != nil {
|
||||
t.Fatalf("Failed to create workflow file: %v", err)
|
||||
}
|
||||
mustNoError(t, io.Local.Write(filepath.Join(workflowsDir, "tests.yml"), templateContent))
|
||||
|
||||
result := findTemplateWorkflow(tmpDir, "tests.yml")
|
||||
if result == "" {
|
||||
t.Error("Expected to find tests.yml in workflows dir")
|
||||
}
|
||||
mustTrue(t, result != "")
|
||||
}
|
||||
|
||||
func TestFindTemplateWorkflow_NotFound(t *testing.T) {
|
||||
func TestFindTemplateWorkflow_NotFound_Bad(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
|
||||
result := findTemplateWorkflow(tmpDir, "nonexistent.yml")
|
||||
if result != "" {
|
||||
t.Errorf("Expected empty string for non-existent template, got %s", result)
|
||||
}
|
||||
mustEqual(t, "", result)
|
||||
}
|
||||
|
||||
func TestTemplateNames_Good(t *testing.T) {
|
||||
|
|
@ -118,11 +89,8 @@ func TestTemplateNames_Good(t *testing.T) {
|
|||
|
||||
names := slices.Sorted(maps.Keys(templateSet))
|
||||
|
||||
if len(names) != 3 {
|
||||
t.Fatalf("Expected 3 template names, got %d", len(names))
|
||||
}
|
||||
|
||||
if names[0] != "a.yml" || names[1] != "m.yml" || names[2] != "z.yml" {
|
||||
t.Fatalf("Expected sorted template names, got %v", names)
|
||||
}
|
||||
mustLen(t, names, 3)
|
||||
mustEqual(t, "a.yml", names[0])
|
||||
mustEqual(t, "m.yml", names[1])
|
||||
mustEqual(t, "z.yml", names[2])
|
||||
}
|
||||
|
|
|
|||
|
|
@ -20,14 +20,18 @@ type Service struct {
|
|||
}
|
||||
|
||||
func (s *Service) handleAction(_ *core.Core, _ core.Message) core.Result {
|
||||
return core.Result{}
|
||||
return core.Result{OK: true}
|
||||
}
|
||||
|
||||
// doCommit shells out to claude for AI-assisted commit.
|
||||
func doCommit(ctx context.Context, repoPath string, allowEdit bool) error {
|
||||
prompt := ""
|
||||
if r := lib.Prompt("commit"); r.OK {
|
||||
prompt = r.Value.(string)
|
||||
value, ok := r.Value.(string)
|
||||
if !ok {
|
||||
return core.E("dev.commit", "commit prompt was not a string", nil)
|
||||
}
|
||||
prompt = value
|
||||
}
|
||||
|
||||
tools := "Bash,Read,Glob,Grep"
|
||||
|
|
|
|||
80
cmd/dev/test_helpers_test.go
Normal file
80
cmd/dev/test_helpers_test.go
Normal file
|
|
@ -0,0 +1,80 @@
|
|||
package dev
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func mustNoError(t *testing.T, err error) {
|
||||
t.Helper()
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func mustEqual[T comparable](t *testing.T, want, got T) {
|
||||
t.Helper()
|
||||
if want != got {
|
||||
t.Fatalf("want %v, got %v", want, got)
|
||||
}
|
||||
}
|
||||
|
||||
func mustDeepEqual(t *testing.T, want, got any) {
|
||||
t.Helper()
|
||||
if !reflect.DeepEqual(want, got) {
|
||||
t.Fatalf("want %v, got %v", want, got)
|
||||
}
|
||||
}
|
||||
|
||||
func mustContains(t *testing.T, s, sub string) {
|
||||
t.Helper()
|
||||
if !strings.Contains(s, sub) {
|
||||
t.Fatalf("expected %q to contain %q", s, sub)
|
||||
}
|
||||
}
|
||||
|
||||
func mustContainsString(t *testing.T, haystack []string, needle string) {
|
||||
t.Helper()
|
||||
for _, s := range haystack {
|
||||
if s == needle {
|
||||
return
|
||||
}
|
||||
}
|
||||
t.Fatalf("expected %v to contain %q", haystack, needle)
|
||||
}
|
||||
|
||||
func mustNotContains(t *testing.T, s, sub string) {
|
||||
t.Helper()
|
||||
if strings.Contains(s, sub) {
|
||||
t.Fatalf("expected %q to not contain %q", s, sub)
|
||||
}
|
||||
}
|
||||
|
||||
func mustTrue(t *testing.T, cond bool) {
|
||||
t.Helper()
|
||||
if !cond {
|
||||
t.Fatal("expected true")
|
||||
}
|
||||
}
|
||||
|
||||
func mustFalse(t *testing.T, cond bool) {
|
||||
t.Helper()
|
||||
if cond {
|
||||
t.Fatal("expected false")
|
||||
}
|
||||
}
|
||||
|
||||
func mustLen[T any](t *testing.T, got []T, want int) {
|
||||
t.Helper()
|
||||
if len(got) != want {
|
||||
t.Fatalf("want length %d, got %d", want, len(got))
|
||||
}
|
||||
}
|
||||
|
||||
func mustNotNil(t *testing.T, v any) {
|
||||
t.Helper()
|
||||
if v == nil {
|
||||
t.Fatal("expected non-nil")
|
||||
}
|
||||
}
|
||||
|
|
@ -38,7 +38,7 @@ func TestCopyZensicalReadme_Good(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestResetOutputDir_ClearsExistingFiles(t *testing.T) {
|
||||
func TestResetOutputDir_ClearsExistingFiles_Good(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
|
||||
stale := filepath.Join(dir, "stale.md")
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ import (
|
|||
"testing"
|
||||
)
|
||||
|
||||
func TestRunRepoSetup_CreatesCoreConfigs(t *testing.T) {
|
||||
func TestRunRepoSetup_CreatesCoreConfigs_Good(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
mustNoError(t, os.WriteFile(filepath.Join(dir, "go.mod"), []byte("module example.com/test\n"), 0o644))
|
||||
|
||||
|
|
@ -19,7 +19,7 @@ func TestRunRepoSetup_CreatesCoreConfigs(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestDetectProjectType_PrefersPackageOverComposer(t *testing.T) {
|
||||
func TestDetectProjectType_PrefersPackageOverComposer_Good(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
mustNoError(t, os.WriteFile(filepath.Join(dir, "package.json"), []byte("{}\n"), 0o644))
|
||||
mustNoError(t, os.WriteFile(filepath.Join(dir, "composer.json"), []byte("{}\n"), 0o644))
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
package setup
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"dappco.re/go/scm/repos"
|
||||
|
|
@ -30,7 +29,5 @@ func TestFilterReposByTypes_EmptyFilter_Good(t *testing.T) {
|
|||
filtered := filterReposByTypes(reposList, nil)
|
||||
|
||||
mustLen(t, filtered, 2)
|
||||
if !reflect.DeepEqual(reposList, filtered) {
|
||||
t.Fatalf("want %v, got %v", reposList, filtered)
|
||||
}
|
||||
mustDeepEqual(t, reposList, filtered)
|
||||
}
|
||||
|
|
|
|||
66
cmd/setup/test_helpers_test.go
Normal file
66
cmd/setup/test_helpers_test.go
Normal file
|
|
@ -0,0 +1,66 @@
|
|||
package setup
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func mustNoError(t *testing.T, err error) {
|
||||
t.Helper()
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func mustNoErrorf(t *testing.T, err error, format string, args ...any) {
|
||||
t.Helper()
|
||||
if err != nil {
|
||||
t.Fatalf(format+": %v", append(args, err)...)
|
||||
}
|
||||
}
|
||||
|
||||
func mustEqual[T comparable](t *testing.T, want, got T) {
|
||||
t.Helper()
|
||||
if want != got {
|
||||
t.Fatalf("want %v, got %v", want, got)
|
||||
}
|
||||
}
|
||||
|
||||
func mustDeepEqual(t *testing.T, want, got any) {
|
||||
t.Helper()
|
||||
if !reflect.DeepEqual(want, got) {
|
||||
t.Fatalf("want %v, got %v", want, got)
|
||||
}
|
||||
}
|
||||
|
||||
func mustContains(t *testing.T, s, sub string) {
|
||||
t.Helper()
|
||||
if !strings.Contains(s, sub) {
|
||||
t.Fatalf("expected %q to contain %q", s, sub)
|
||||
}
|
||||
}
|
||||
|
||||
func mustNotContains(t *testing.T, s, sub string) {
|
||||
t.Helper()
|
||||
if strings.Contains(s, sub) {
|
||||
t.Fatalf("expected %q to not contain %q", s, sub)
|
||||
}
|
||||
}
|
||||
|
||||
func mustLen[T any](t *testing.T, got []T, want int) {
|
||||
t.Helper()
|
||||
if len(got) != want {
|
||||
t.Fatalf("want length %d, got %d", want, len(got))
|
||||
}
|
||||
}
|
||||
|
||||
func mustContainsString(t *testing.T, haystack []string, needle string) {
|
||||
t.Helper()
|
||||
for _, s := range haystack {
|
||||
if s == needle {
|
||||
return
|
||||
}
|
||||
}
|
||||
t.Fatalf("expected %v to contain %q", haystack, needle)
|
||||
}
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
FROM golang:1.25-alpine AS build
|
||||
FROM golang:1.26-alpine AS build
|
||||
WORKDIR /src
|
||||
COPY go.mod main.go ./
|
||||
RUN go build -trimpath -ldflags="-w -s" -o /vanity-import .
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@
|
|||
package workspace
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
|
|
@ -36,24 +37,33 @@ func DefaultConfig() *WorkspaceConfig {
|
|||
// LoadConfig reads .core/workspace.yaml from the given directory, walking up to parent dirs.
|
||||
// Returns nil (no error) if no config file is found.
|
||||
func LoadConfig(dir string) (*WorkspaceConfig, error) {
|
||||
absDir, err := filepath.Abs(dir)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("workspace.LoadConfig: resolve %q: %w", dir, err)
|
||||
}
|
||||
|
||||
return loadConfig(filepath.Clean(absDir))
|
||||
}
|
||||
|
||||
func loadConfig(dir string) (*WorkspaceConfig, error) {
|
||||
path := filepath.Join(dir, ".core", "workspace.yaml")
|
||||
|
||||
if !coreio.Local.IsFile(path) {
|
||||
parent := filepath.Dir(dir)
|
||||
if parent != dir {
|
||||
return LoadConfig(parent)
|
||||
return loadConfig(parent)
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
data, err := coreio.Local.Read(path)
|
||||
if err != nil {
|
||||
return nil, log.E("workspace.LoadConfig", "failed to read workspace config", err)
|
||||
return nil, fmt.Errorf("workspace.LoadConfig: failed to read workspace config: %w", err)
|
||||
}
|
||||
|
||||
cfg := DefaultConfig()
|
||||
if err := yaml.Unmarshal([]byte(data), cfg); err != nil {
|
||||
return nil, log.E("workspace.LoadConfig", "failed to parse workspace config", err)
|
||||
return nil, fmt.Errorf("workspace.LoadConfig: failed to parse workspace config: %w", err)
|
||||
}
|
||||
|
||||
return cfg, nil
|
||||
|
|
|
|||
51
cmd/workspace/config_test.go
Normal file
51
cmd/workspace/config_test.go
Normal file
|
|
@ -0,0 +1,51 @@
|
|||
package workspace
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestLoadConfig_RelativeDirFindsParentConfig_Good(t *testing.T) {
|
||||
root := t.TempDir()
|
||||
mustNoError(t, os.MkdirAll(filepath.Join(root, ".core"), 0o755))
|
||||
mustNoError(t, os.MkdirAll(filepath.Join(root, "packages", "app"), 0o755))
|
||||
mustNoError(t, os.WriteFile(filepath.Join(root, ".core", "workspace.yaml"), []byte(`version: 1
|
||||
active: app
|
||||
packages_dir: ./packages
|
||||
`), 0o600))
|
||||
|
||||
originalWD, err := os.Getwd()
|
||||
mustNoError(t, err)
|
||||
t.Cleanup(func() {
|
||||
mustNoError(t, os.Chdir(originalWD))
|
||||
})
|
||||
mustNoError(t, os.Chdir(filepath.Join(root, "packages", "app")))
|
||||
|
||||
cfg, err := LoadConfig(".")
|
||||
mustNoError(t, err)
|
||||
mustNotNil(t, cfg)
|
||||
mustEqual(t, "app", cfg.Active)
|
||||
mustEqual(t, "./packages", cfg.PackagesDir)
|
||||
}
|
||||
|
||||
func mustNoError(t *testing.T, err error) {
|
||||
t.Helper()
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func mustEqual[T comparable](t *testing.T, want, got T) {
|
||||
t.Helper()
|
||||
if want != got {
|
||||
t.Fatalf("want %v, got %v", want, got)
|
||||
}
|
||||
}
|
||||
|
||||
func mustNotNil(t *testing.T, got any) {
|
||||
t.Helper()
|
||||
if got == nil {
|
||||
t.Fatal("expected non-nil")
|
||||
}
|
||||
}
|
||||
|
|
@ -36,7 +36,7 @@ aws-access-key-id,creds.txt,7,1,AWS access key detected,AKIA1234567890ABCDEF
|
|||
mustEqual(t, "AKIA1234567890ABCDEF", findings[1].Snippet)
|
||||
}
|
||||
|
||||
func TestScanSecrets_ReportsFindingsOnExitError(t *testing.T) {
|
||||
func TestScanSecrets_ReportsFindingsOnExitError_Good(t *testing.T) {
|
||||
originalRunner := scanSecretsRunner
|
||||
t.Cleanup(func() {
|
||||
scanSecretsRunner = originalRunner
|
||||
|
|
|
|||
|
|
@ -29,7 +29,7 @@ api_key: "ghp_abcdefghijklmnopqrstuvwxyz1234"
|
|||
mustEqual(t, "creds.txt", filepath.Base(findings[1].Path))
|
||||
}
|
||||
|
||||
func TestScanDir_SkipsBinaryAndIgnoredDirs(t *testing.T) {
|
||||
func TestScanDir_SkipsBinaryAndIgnoredDirs_Good(t *testing.T) {
|
||||
root := t.TempDir()
|
||||
|
||||
mustNoError(t, os.Mkdir(filepath.Join(root, ".git"), 0o755))
|
||||
|
|
@ -41,7 +41,7 @@ func TestScanDir_SkipsBinaryAndIgnoredDirs(t *testing.T) {
|
|||
mustEmpty(t, findings)
|
||||
}
|
||||
|
||||
func TestScanDir_ReportsGenericAssignments(t *testing.T) {
|
||||
func TestScanDir_ReportsGenericAssignments_Bad(t *testing.T) {
|
||||
root := t.TempDir()
|
||||
|
||||
mustNoError(t, os.WriteFile(filepath.Join(root, "secrets.env"), []byte("client_secret: abcdefghijklmnop\n"), 0o600))
|
||||
|
|
|
|||
48
devkit/test_helpers_test.go
Normal file
48
devkit/test_helpers_test.go
Normal file
|
|
@ -0,0 +1,48 @@
|
|||
package devkit
|
||||
|
||||
import (
|
||||
"math"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func mustNoError(t *testing.T, err error) {
|
||||
t.Helper()
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func mustError(t *testing.T, err error) {
|
||||
t.Helper()
|
||||
if err == nil {
|
||||
t.Fatal("expected error, got nil")
|
||||
}
|
||||
}
|
||||
|
||||
func mustEqual[T comparable](t *testing.T, want, got T) {
|
||||
t.Helper()
|
||||
if want != got {
|
||||
t.Fatalf("want %v, got %v", want, got)
|
||||
}
|
||||
}
|
||||
|
||||
func mustLen[T any](t *testing.T, got []T, want int) {
|
||||
t.Helper()
|
||||
if len(got) != want {
|
||||
t.Fatalf("want length %d, got %d", want, len(got))
|
||||
}
|
||||
}
|
||||
|
||||
func mustEmpty[T any](t *testing.T, got []T) {
|
||||
t.Helper()
|
||||
if len(got) != 0 {
|
||||
t.Fatalf("expected empty, got %d entries", len(got))
|
||||
}
|
||||
}
|
||||
|
||||
func mustInDelta(t *testing.T, want, got, delta float64) {
|
||||
t.Helper()
|
||||
if math.Abs(want-got) > delta {
|
||||
t.Fatalf("want %v±%v, got %v", want, delta, got)
|
||||
}
|
||||
}
|
||||
|
|
@ -4,7 +4,7 @@
|
|||
|
||||
| Tool | Minimum version | Purpose |
|
||||
|------|----------------|---------|
|
||||
| Go | 1.25 | Build and test |
|
||||
| Go | 1.26 | Build and test |
|
||||
| Task | any | Taskfile automation (optional, used by some builders) |
|
||||
| `govulncheck` | latest | Vulnerability scanning (`devkit.VulnCheck`) |
|
||||
| `gitleaks` | any | Secret scanning (`devkit.ScanSecrets`) |
|
||||
|
|
|
|||
18
go.sum
18
go.sum
|
|
@ -2,8 +2,16 @@ code.gitea.io/sdk/gitea v0.23.2 h1:iJB1FDmLegwfwjX8gotBDHdPSbk/ZR8V9VmEJaVsJYg=
|
|||
code.gitea.io/sdk/gitea v0.23.2/go.mod h1:yyF5+GhljqvA30sRDreoyHILruNiy4ASufugzYg0VHM=
|
||||
codeberg.org/mvdkleijn/forgejo-sdk/forgejo/v2 v2.2.0 h1:HTCWpzyWQOHDWt3LzI6/d2jvUDsw/vgGRWm/8BTvcqI=
|
||||
codeberg.org/mvdkleijn/forgejo-sdk/forgejo/v2 v2.2.0/go.mod h1:ZglEEDj+qkxYUb+SQIeqGtFxQrbaMYqIOgahNKb7uxs=
|
||||
dappco.re/go/agent v0.8.0-alpha.1 h1:7jtrDGh5CHUVsvvQiG8gjQxfdlI+ncJrIHXEMksJ8bc=
|
||||
dappco.re/go/agent v0.8.0-alpha.1/go.mod h1:jiShGsIfHS7b7rJXMdb30K+wKL8Kx8w/VUrLNDYRbCo=
|
||||
dappco.re/go/agent v0.11.0 h1:5PKzxJf+z0WF+QsxgkMwvDUODj38DGCx0uMk1KxtWkg=
|
||||
dappco.re/go/agent v0.11.0/go.mod h1:nBF4HMMSZD/YJg+MTHqTv71csgFlCyy62Ux084yjw+U=
|
||||
dappco.re/go/cli v0.8.0-alpha.1 h1:UUnkSvAgNeRtu4kc96hr4WUpe9WTBxDY+1Co5IDVlbk=
|
||||
dappco.re/go/cli v0.8.0-alpha.1/go.mod h1:jRJuSyEB7pAmyiAyTPSh7l1ens627vfxhBcUhi3sOEY=
|
||||
dappco.re/go/config v0.8.0-alpha.1 h1:YpfPi7PHId0Wc2C/h07rmTZG06a+ONHrBLG9KDg45Uo=
|
||||
dappco.re/go/config v0.8.0-alpha.1/go.mod h1:Ryvf7Fncq4p+mZQnHjP5h8OmDcbE2JBf99E6hDdpeN4=
|
||||
dappco.re/go/container v0.8.0-alpha.1 h1:jrC308wXpooaHMjvhEvPwPfK4KOXTuFYz4y/Es+uhY4=
|
||||
dappco.re/go/container v0.8.0-alpha.1/go.mod h1:5F+NPSBG3LtgfBTGvmGcVWLmax4LrmxBgexOHG4gnKc=
|
||||
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/cli v0.5.2 h1:mo+PERo3lUytE+r3ArHr8o2nTftXjgPPsU/rn3ETXDM=
|
||||
|
|
@ -22,6 +30,16 @@ dappco.re/go/core/log v0.1.2 h1:pQSZxKD8VycdvjNJmatXbPSq2OxcP2xHbF20zgFIiZI=
|
|||
dappco.re/go/core/log v0.1.2/go.mod h1:Nkqb8gsXhZAO8VLpx7B8i1iAmohhzqA20b9Zr8VUcJs=
|
||||
dappco.re/go/core/scm v0.6.1 h1:nQWr2AGreLzhp//2zZolol87TCKlzV2/I/hpBVkv0Gc=
|
||||
dappco.re/go/core/scm v0.6.1/go.mod h1:fYy/xazjyv84X8sxBIpTBikSdU5nQq4qf/IR2hXnd5E=
|
||||
dappco.re/go/i18n v0.8.0-alpha.1 h1:9LI/PrF41XeQu69eOaBTz3LMrXTJ08O2f1EEATq9k5A=
|
||||
dappco.re/go/i18n v0.8.0-alpha.1/go.mod h1:aSfWSAW2EVh/aMbMplc27URnjl6DvRVvWfvRC2my7AY=
|
||||
dappco.re/go/inference v0.8.0-alpha.1 h1:Cc3YZr04rNSqqHQBm7v53mzfn6e17sf7oDe+TqQnzwo=
|
||||
dappco.re/go/inference v0.8.0-alpha.1/go.mod h1:vMXtaGSKvom7B5rjOjzl4taSOXbbVmnsLlYd0X/PFo0=
|
||||
dappco.re/go/io v0.8.0-alpha.1 h1:tIJ/Nd6lGr2DFEUj2HzGM8dPglS5bEAI4h2RAgzGCNE=
|
||||
dappco.re/go/io v0.8.0-alpha.1/go.mod h1:5u1TImtXPdJKDgh59Nw4rsbMUkq02uVDDsL5bE1mhBk=
|
||||
dappco.re/go/log v0.8.0-alpha.1 h1:eXTdrt88Ovbdm0KJkJDaEpgLUHUZgJ2xYEu2uN3eV4I=
|
||||
dappco.re/go/log v0.8.0-alpha.1/go.mod h1:IC04Em9SfVTcXiWc1BqZDQfa1MtOuMDEermZkQcTz9c=
|
||||
dappco.re/go/scm v0.8.0-alpha.1 h1:pXiO5Hp5tky3shekYERUK9KsQy9xoWQQW0I40mPyKvA=
|
||||
dappco.re/go/scm v0.8.0-alpha.1/go.mod h1:11xL67SU5TJ+fTBLyqYDDwotl7Y1qy5rWY+JgEQ16UQ=
|
||||
github.com/42wim/httpsig v1.2.3 h1:xb0YyWhkYj57SPtfSttIobJUPJZB9as1nsfo7KWVcEs=
|
||||
github.com/42wim/httpsig v1.2.3/go.mod h1:nZq9OlYKDrUBhptd77IHx4/sZZD+IxTBADvAPI9G/EM=
|
||||
github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k=
|
||||
|
|
|
|||
|
|
@ -52,7 +52,7 @@ func TestGenerate_Good(t *testing.T) {
|
|||
mustDeepEqual(t, []string{"core/media"}, snap.Modules)
|
||||
}
|
||||
|
||||
func TestGenerate_Good_NoDaemons(t *testing.T) {
|
||||
func TestGenerate_NoDaemons_Good(t *testing.T) {
|
||||
m := &manifest.Manifest{
|
||||
Code: "simple",
|
||||
Name: "Simple",
|
||||
|
|
@ -75,7 +75,7 @@ func TestGenerate_Good_NoDaemons(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestGenerate_Bad_NilManifest(t *testing.T) {
|
||||
func TestGenerate_NilManifest_Bad(t *testing.T) {
|
||||
_, err := Generate(nil, "abc123", "v1.0.0")
|
||||
mustErrorContains(t, err, "manifest is nil")
|
||||
}
|
||||
|
|
|
|||
45
snapshot/test_helpers_test.go
Normal file
45
snapshot/test_helpers_test.go
Normal file
|
|
@ -0,0 +1,45 @@
|
|||
package snapshot
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func mustNoError(t *testing.T, err error) {
|
||||
t.Helper()
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func mustEqual[T comparable](t *testing.T, want, got T) {
|
||||
t.Helper()
|
||||
if want != got {
|
||||
t.Fatalf("want %v, got %v", want, got)
|
||||
}
|
||||
}
|
||||
|
||||
func mustDeepEqual(t *testing.T, want, got any) {
|
||||
t.Helper()
|
||||
if !reflect.DeepEqual(want, got) {
|
||||
t.Fatalf("want %v, got %v", want, got)
|
||||
}
|
||||
}
|
||||
|
||||
func mustLenMap[K comparable, V any](t *testing.T, m map[K]V, want int) {
|
||||
t.Helper()
|
||||
if len(m) != want {
|
||||
t.Fatalf("want length %d, got %d", want, len(m))
|
||||
}
|
||||
}
|
||||
|
||||
func mustErrorContains(t *testing.T, err error, sub string) {
|
||||
t.Helper()
|
||||
if err == nil {
|
||||
t.Fatal("expected error, got nil")
|
||||
}
|
||||
if !strings.Contains(err.Error(), sub) {
|
||||
t.Fatalf("expected error %q to contain %q", err.Error(), sub)
|
||||
}
|
||||
}
|
||||
|
|
@ -16,6 +16,9 @@ tasks:
|
|||
dir: ../../..
|
||||
cmds:
|
||||
- |
|
||||
set -euo pipefail
|
||||
IFS=$'\n\t'
|
||||
|
||||
export GOWORK=off
|
||||
export GOCACHE="${GOCACHE:-/tmp/go-devops-gocache}"
|
||||
export GOMODCACHE="${GOMODCACHE:-/tmp/go-devops-gomodcache}"
|
||||
|
|
@ -35,6 +38,9 @@ tasks:
|
|||
dir: ../../..
|
||||
cmds:
|
||||
- |
|
||||
set -euo pipefail
|
||||
IFS=$'\n\t'
|
||||
|
||||
export GOWORK=off
|
||||
export GOCACHE="${GOCACHE:-/tmp/go-devops-gocache}"
|
||||
export GOMODCACHE="${GOMODCACHE:-/tmp/go-devops-gomodcache}"
|
||||
|
|
@ -54,6 +60,9 @@ tasks:
|
|||
dir: ../../..
|
||||
cmds:
|
||||
- |
|
||||
set -euo pipefail
|
||||
IFS=$'\n\t'
|
||||
|
||||
export GOWORK=off
|
||||
export GOCACHE="${GOCACHE:-/tmp/go-devops-gocache}"
|
||||
export GOMODCACHE="${GOMODCACHE:-/tmp/go-devops-gomodcache}"
|
||||
|
|
|
|||
|
|
@ -54,7 +54,7 @@ func runPlaybookSmoke(cmd *cli.Command, args []string) error {
|
|||
count := 0
|
||||
err := filepath.WalkDir(dir, func(path string, entry os.DirEntry, err error) error {
|
||||
if err != nil {
|
||||
return err
|
||||
return fmt.Errorf("%s: %w", path, err)
|
||||
}
|
||||
if entry.IsDir() || !isYAML(path) {
|
||||
return nil
|
||||
|
|
@ -62,7 +62,7 @@ func runPlaybookSmoke(cmd *cli.Command, args []string) error {
|
|||
|
||||
raw, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
return err
|
||||
return fmt.Errorf("%s: %w", path, err)
|
||||
}
|
||||
|
||||
var document any
|
||||
|
|
@ -73,7 +73,7 @@ func runPlaybookSmoke(cmd *cli.Command, args []string) error {
|
|||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
return fmt.Errorf("walk %s: %w", dir, err)
|
||||
}
|
||||
if count == 0 {
|
||||
return fmt.Errorf("no playbook YAML files found in %s", dir)
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue