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:
Snider 2026-04-27 15:07:24 +01:00
parent c43090e2ca
commit 907c5fa64c
25 changed files with 387 additions and 121 deletions

View file

@ -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

View file

@ -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)
}

View file

@ -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) {

View file

@ -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)

View file

@ -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)

View file

@ -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)
}

View file

@ -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])
}

View file

@ -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"

View 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")
}
}

View file

@ -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")

View file

@ -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))

View file

@ -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)
}

View 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)
}

View file

@ -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 .

View file

@ -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

View 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")
}
}

View file

@ -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

View file

@ -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))

View 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)
}
}

View file

@ -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
View file

@ -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=

View file

@ -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")
}

View 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)
}
}

View file

@ -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}"

View file

@ -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)