Yeet those builds out!

feat: enforce semver in package versions (#17)

* chore: disable copilot

Signed-off-by: Xe Iaso <me@xeiaso.net>

* feat(internal): additional version and package build validations

This adds additional test logic to ensure that versions passed to
package build functions are semantic versions and that the version
strings can be properly parsed after the package is built.

Signed-off-by: Xe Iaso <me@xeiaso.net>

* chore: go mod tidy

Signed-off-by: Xe Iaso <me@xeiaso.net>

* test(internal): test an intentionally failing build

Signed-off-by: Xe Iaso <me@xeiaso.net>

---------

Signed-off-by: Xe Iaso <me@xeiaso.net>

authored by Xe Iaso and committed by GitHub 178f1796 56e6fa97

Changed files
+184 -2
.vscode
internal
+5
.vscode/settings.json
··· 1 + { 2 + "github.copilot.enable": { 3 + "*": false 4 + } 5 + }
+6 -1
go.mod
··· 4 4 5 5 require ( 6 6 al.essio.dev/pkg/shellescape v1.6.0 7 + github.com/Masterminds/semver/v3 v3.3.1 7 8 github.com/Songmu/gitconfig v0.2.0 9 + github.com/cavaliergopher/rpm v1.3.0 8 10 github.com/dop251/goja v0.0.0-20250309171923-bcd7cc6bf64c 9 11 github.com/goreleaser/nfpm/v2 v2.42.0 10 12 github.com/pkg/errors v0.9.1 13 + pault.ag/go/debian v0.18.0 11 14 ) 12 15 13 16 require ( ··· 15 18 github.com/AlekSi/pointer v1.2.0 // indirect 16 19 github.com/BurntSushi/toml v1.4.1-0.20240526193622-a339e1f7089c // indirect 17 20 github.com/Masterminds/goutils v1.1.1 // indirect 18 - github.com/Masterminds/semver/v3 v3.3.1 // indirect 19 21 github.com/Masterminds/sprig/v3 v3.3.0 // indirect 20 22 github.com/Microsoft/go-winio v0.6.2 // indirect 21 23 github.com/ProtonMail/go-crypto v1.1.6 // indirect ··· 44 46 github.com/huandu/xstrings v1.5.0 // indirect 45 47 github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect 46 48 github.com/kevinburke/ssh_config v1.2.0 // indirect 49 + github.com/kjk/lzma v0.0.0-20161016003348-3fd93898850d // indirect 47 50 github.com/klauspost/compress v1.18.0 // indirect 48 51 github.com/klauspost/pgzip v1.2.6 // indirect 49 52 github.com/mattn/go-colorable v0.1.13 // indirect ··· 58 61 github.com/spf13/cast v1.7.1 // indirect 59 62 github.com/ulikunitz/xz v0.5.12 // indirect 60 63 github.com/xanzy/ssh-agent v0.3.3 // indirect 64 + github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8 // indirect 61 65 gitlab.com/digitalxero/go-conventional-commit v1.0.7 // indirect 62 66 golang.org/x/crypto v0.37.0 // indirect 63 67 golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 // indirect ··· 72 76 gopkg.in/warnings.v0 v0.1.2 // indirect 73 77 gopkg.in/yaml.v3 v3.0.1 // indirect 74 78 honnef.co/go/tools v0.6.1 // indirect 79 + pault.ag/go/topsort v0.1.1 // indirect 75 80 ) 76 81 77 82 tool (
+8
go.sum
··· 36 36 github.com/caarlos0/testfs v0.4.4/go.mod h1:bRN55zgG4XCUVVHZCeU+/Tz1Q6AxEJOEJTliBy+1DMk= 37 37 github.com/cavaliergopher/cpio v1.0.1 h1:KQFSeKmZhv0cr+kawA3a0xTQCU4QxXF1vhU7P7av2KM= 38 38 github.com/cavaliergopher/cpio v1.0.1/go.mod h1:pBdaqQjnvXxdS/6CvNDwIANIFSP0xRKI16PX4xejRQc= 39 + github.com/cavaliergopher/rpm v1.3.0 h1:UHX46sasX8MesUXXQ+UbkFLUX4eUWTlEcX8jcnRBIgI= 40 + github.com/cavaliergopher/rpm v1.3.0/go.mod h1:vEumo1vvtrHM1Ov86f6+k8j7zNKOxQfHDCAIcR/36ZI= 39 41 github.com/cli/browser v1.1.0/go.mod h1:HKMQAt9t12kov91Mn7RfZxyJQQgWgyS/3SZswlZ5iTI= 40 42 github.com/cli/go-gh v0.1.0 h1:kMqFmC3ECBrV2UKzlOHjNOTTchExVc5tjNHtCqk/zYk= 41 43 github.com/cli/go-gh v0.1.0/go.mod h1:eTGWl99EMZ+3Iau5C6dHyGAJRRia65MtdBtuhWc+84o= ··· 121 123 github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= 122 124 github.com/kevinburke/ssh_config v1.2.0 h1:x584FjTGwHzMwvHx18PXxbBVzfnxogHaAReU4gf13a4= 123 125 github.com/kevinburke/ssh_config v1.2.0/go.mod h1:CT57kijsi8u/K/BOFA39wgDQJ9CxiF4nAY/ojJ6r6mM= 126 + github.com/kjk/lzma v0.0.0-20161016003348-3fd93898850d h1:RnWZeH8N8KXfbwMTex/KKMYMj0FJRCF6tQubUuQ02GM= 127 + github.com/kjk/lzma v0.0.0-20161016003348-3fd93898850d/go.mod h1:phT/jsRPBAEqjAibu1BurrabCBNTYiVI+zbmyCZJY6Q= 124 128 github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo= 125 129 github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ= 126 130 github.com/klauspost/pgzip v1.2.6 h1:8RXeL5crjEUFnR2/Sn6GJNWtSQ3Dk8pq4CL3jvdDyjU= ··· 272 276 gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 273 277 honnef.co/go/tools v0.6.1 h1:R094WgE8K4JirYjBaOpz/AvTyUu/3wbmAoskKN/pxTI= 274 278 honnef.co/go/tools v0.6.1/go.mod h1:3puzxxljPCe8RGJX7BIy1plGbxEOZni5mR2aXe3/uk4= 279 + pault.ag/go/debian v0.18.0 h1:nr0iiyOU5QlG1VPnhZLNhnCcHx58kukvBJp+dvaM6CQ= 280 + pault.ag/go/debian v0.18.0/go.mod h1:JFl0XWRCv9hWBrB5MDDZjA5GSEs1X3zcFK/9kCNIUmE= 281 + pault.ag/go/topsort v0.1.1 h1:L0QnhUly6LmTv0e3DEzbN2q6/FGgAcQvaEw65S53Bg4= 282 + pault.ag/go/topsort v0.1.1/go.mod h1:r1kc/L0/FZ3HhjezBIPaNVhkqv8L0UJ9bxRuHRVZ0q4=
+5
internal/git_test.go
··· 25 25 input: "1.0.0", 26 26 want: "1.0.0", 27 27 }, 28 + { 29 + name: "with version with v and -", 30 + input: "v1.0.0-abc123", 31 + want: "1.0.0-abc123", 32 + }, 28 33 } { 29 34 t.Run(tt.name, func(t *testing.T) { 30 35 yeet.ForceGitVersion = &tt.input
+5
internal/mkdeb/mkdeb.go
··· 8 8 "runtime" 9 9 "time" 10 10 11 + "github.com/Masterminds/semver/v3" 11 12 "github.com/TecharoHQ/yeet/internal" 12 13 "github.com/TecharoHQ/yeet/internal/pkgmeta" 13 14 "github.com/goreleaser/nfpm/v2" ··· 26 27 } 27 28 } 28 29 }() 30 + 31 + if _, err := semver.NewVersion(p.Version); err != nil { 32 + return "", err 33 + } 29 34 30 35 os.MkdirAll("./var", 0755) 31 36 os.WriteFile(filepath.Join("./var", ".gitignore"), []byte("*\n!.gitignore"), 0644)
+26
internal/mkdeb/mkdeb_test.go
··· 1 + package mkdeb 2 + 3 + import ( 4 + "testing" 5 + 6 + "github.com/TecharoHQ/yeet/internal/yeettest" 7 + "pault.ag/go/debian/deb" 8 + ) 9 + 10 + func TestBuild(t *testing.T) { 11 + fname := yeettest.BuildHello(t, Build, "1.0.0", true) 12 + 13 + debFile, close, err := deb.LoadFile(fname) 14 + if err != nil { 15 + t.Fatalf("failed to load deb file: %v", err) 16 + } 17 + defer close() 18 + 19 + if debFile.Control.Version.Empty() { 20 + t.Error("version is empty") 21 + } 22 + } 23 + 24 + func TestBuildError(t *testing.T) { 25 + yeettest.BuildHello(t, Build, ".0.0", false) 26 + }
+5
internal/mkrpm/mkrpm.go
··· 8 8 "runtime" 9 9 "time" 10 10 11 + "github.com/Masterminds/semver/v3" 11 12 "github.com/TecharoHQ/yeet/internal" 12 13 "github.com/TecharoHQ/yeet/internal/pkgmeta" 13 14 "github.com/goreleaser/nfpm/v2" ··· 26 27 } 27 28 } 28 29 }() 30 + 31 + if _, err := semver.NewVersion(p.Version); err != nil { 32 + return "", err 33 + } 29 34 30 35 os.MkdirAll("./var", 0755) 31 36 os.WriteFile(filepath.Join("./var", ".gitignore"), []byte("*\n!.gitignore"), 0644)
+37
internal/mkrpm/mkrpm_test.go
··· 1 + package mkrpm 2 + 3 + import ( 4 + "os" 5 + "testing" 6 + 7 + "github.com/Masterminds/semver/v3" 8 + "github.com/TecharoHQ/yeet/internal/yeettest" 9 + "github.com/cavaliergopher/rpm" 10 + ) 11 + 12 + func TestBuild(t *testing.T) { 13 + fname := yeettest.BuildHello(t, Build, "1.0.0", true) 14 + 15 + pkg, err := rpm.Open(fname) 16 + if err != nil { 17 + t.Fatalf("failed to open rpm file: %v", err) 18 + } 19 + 20 + version, err := semver.NewVersion(pkg.Version()) 21 + if err != nil { 22 + t.Fatalf("failed to parse version: %v", err) 23 + } 24 + if version == nil { 25 + t.Error("version is nil") 26 + } 27 + 28 + fin, err := os.Open(fname) 29 + if err != nil { 30 + t.Fatalf("failed to open rpm file: %v", err) 31 + } 32 + defer fin.Close() 33 + } 34 + 35 + func TestBuildError(t *testing.T) { 36 + yeettest.BuildHello(t, Build, ".0.0", false) 37 + }
+4
internal/mktarball/mktarball.go
··· 10 10 "path/filepath" 11 11 "runtime" 12 12 13 + "github.com/Masterminds/semver/v3" 13 14 "github.com/TecharoHQ/yeet/internal" 14 15 "github.com/TecharoHQ/yeet/internal/pkgmeta" 15 16 ) ··· 29 30 } 30 31 } 31 32 }() 33 + if _, err := semver.NewVersion(p.Version); err != nil { 34 + return "", err 35 + } 32 36 33 37 os.MkdirAll("./var", 0755) 34 38 os.WriteFile(filepath.Join("./var", ".gitignore"), []byte("*\n!.gitignore"), 0644)
+15
internal/mktarball/mktarball_test.go
··· 1 + package mktarball 2 + 3 + import ( 4 + "testing" 5 + 6 + "github.com/TecharoHQ/yeet/internal/yeettest" 7 + ) 8 + 9 + func TestBuild(t *testing.T) { 10 + yeettest.BuildHello(t, Build, "1.0.0", true) 11 + } 12 + 13 + func TestBuildError(t *testing.T) { 14 + yeettest.BuildHello(t, Build, ".0.0", false) 15 + }
+7
internal/testdata/hello/main.go
··· 1 + package main 2 + 3 + import "fmt" 4 + 5 + func main() { 6 + fmt.Println("Hello, world!") 7 + }
+8 -1
internal/yeet/yeet.go
··· 10 10 "strings" 11 11 "time" 12 12 13 + "github.com/Masterminds/semver/v3" 13 14 "github.com/pkg/errors" 14 15 ) 15 16 ··· 83 84 return "", err 84 85 } 85 86 86 - return strings.TrimSuffix(s, "\n"), nil 87 + ver, err := semver.NewVersion(strings.TrimSuffix(s, "\n")) 88 + if err != nil { 89 + // probably no git tag 90 + return "devel", nil 91 + } 92 + 93 + return ver.String(), nil 87 94 } 88 95 89 96 // DockerTag tags a docker image
+53
internal/yeettest/buildpackage.go
··· 1 + package yeettest 2 + 3 + import ( 4 + "os" 5 + "path/filepath" 6 + "runtime" 7 + "testing" 8 + 9 + "github.com/TecharoHQ/yeet/internal/pkgmeta" 10 + "github.com/TecharoHQ/yeet/internal/yeet" 11 + ) 12 + 13 + type Impl func(p pkgmeta.Package) (string, error) 14 + 15 + func BuildHello(t *testing.T, build Impl, version string, fatal bool) string { 16 + t.Helper() 17 + 18 + p := pkgmeta.Package{ 19 + Name: "hello", 20 + Version: version, 21 + Description: "Hello world", 22 + Homepage: "https://example.com", 23 + License: "MIT", 24 + Platform: runtime.GOOS, 25 + Goarch: runtime.GOARCH, 26 + Build: func(p pkgmeta.BuildInput) { 27 + yeet.ShouldWork(t.Context(), nil, yeet.WD, "go", "build", "-o", filepath.Join(p.Bin, "hello"), "../testdata/hello") 28 + }, 29 + } 30 + 31 + foutpath, err := build(p) 32 + switch fatal { 33 + case true: 34 + if err != nil { 35 + t.Fatalf("Build() error = %v", err) 36 + } 37 + case false: 38 + if err != nil { 39 + t.Logf("Build() error = %v", err) 40 + } 41 + return "" 42 + } 43 + 44 + if foutpath == "" { 45 + t.Fatal("Build() returned empty path") 46 + } 47 + 48 + t.Cleanup(func() { 49 + os.RemoveAll(filepath.Dir(foutpath)) 50 + }) 51 + 52 + return foutpath 53 + }