diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 5977440..b52125c 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -11,6 +11,10 @@ jobs: steps: - name: Checkout code uses: actions/checkout@v5 + - name: Configure git identity + run: | + git config user.name "CI" + git config user.email "ci@localhost" - name: Set up Go ${{ matrix.go-version }} uses: actions/setup-go@v6 with: diff --git a/.goreleaser.yml b/.goreleaser.yml index 5e90d30..ca7cfc2 100644 --- a/.goreleaser.yml +++ b/.goreleaser.yml @@ -35,7 +35,7 @@ builds: archives: - name_template: >- - {{.Binary}}_{{.Version}}_{{.Os}}_{{.Arch}} + {{.Binary}}_{{.Version}}_ {{- if eq .Os "darwin"}}macOS {{- else if eq .Os "linux"}}Linux {{- else if eq .Os "windows"}}Windows diff --git a/magefiles/targets/release_test.go b/magefiles/targets/release_test.go new file mode 100644 index 0000000..98e00e8 --- /dev/null +++ b/magefiles/targets/release_test.go @@ -0,0 +1,103 @@ +//go:build go1.24 + +// These tests fail on older Go versions because of the change in go.mod format. +// But it doesn't matter because we only call release with modern go versions. + +package targets + +import ( + "fmt" + "os" + "path/filepath" + "strings" + "testing" +) + +// expectedReleaseFiles lists the release artifacts goreleaser should produce. +// Each entry uses %s as a placeholder for the version string. +var expectedReleaseFiles = []string{ + "mage_%s_checksums.txt", + "mage_%s_DragonFlyBSD-64bit.tar.gz", + "mage_%s_FreeBSD-64bit.tar.gz", + "mage_%s_FreeBSD-ARM.tar.gz", + "mage_%s_FreeBSD-ARM64.tar.gz", + "mage_%s_Linux-64bit.tar.gz", + "mage_%s_Linux-ARM.tar.gz", + "mage_%s_Linux-ARM64.tar.gz", + "mage_%s_macOS-64bit.tar.gz", + "mage_%s_macOS-ARM64.tar.gz", + "mage_%s_NetBSD-64bit.tar.gz", + "mage_%s_NetBSD-ARM.tar.gz", + "mage_%s_NetBSD-ARM64.tar.gz", + "mage_%s_OpenBSD-64bit.tar.gz", + "mage_%s_OpenBSD-ARM64.tar.gz", + "mage_%s_Windows-64bit.zip", + "mage_%s_Windows-ARM64.zip", +} + +func TestRelease(t *testing.T) { + if testing.Short() { + t.Skip("skipping release test in short mode") + } + + // goreleaser must run from the repo root where .goreleaser.yml lives. + repoRoot, err := filepath.Abs(filepath.Join("..", "..")) + if err != nil { + t.Fatal(err) + } + origDir, err := os.Getwd() + if err != nil { + t.Fatal(err) + } + if err := os.Chdir(repoRoot); err != nil { + t.Fatal(err) + } + t.Cleanup(func() { _ = os.RemoveAll(filepath.Join(repoRoot, "dist")) }) + t.Cleanup(func() { _ = os.Chdir(origDir) }) + + const tag = "v1.0.99" + + dryRun := true + if err := Release(tag, &dryRun); err != nil { + t.Fatal(err) + } + + // goreleaser strips the leading "v" from the tag for artifact names. + version := strings.TrimPrefix(tag, "v") + + entries, err := os.ReadDir("dist") + if err != nil { + t.Fatal(err) + } + + // Build expected set, initially marking each as not found. + expected := make(map[string]bool, len(expectedReleaseFiles)) + for _, pattern := range expectedReleaseFiles { + expected[fmt.Sprintf(pattern, version)] = false + } + + // Walk dist/ and match release artifacts. + for _, e := range entries { + if e.IsDir() { + continue + } + name := e.Name() + isArtifact := strings.HasSuffix(name, ".tar.gz") || + strings.HasSuffix(name, ".zip") || + strings.HasSuffix(name, "_checksums.txt") + if !isArtifact { + continue + } + if _, ok := expected[name]; ok { + expected[name] = true + } else { + t.Errorf("unexpected release artifact: %s", name) + } + } + + for name, found := range expected { + if !found { + t.Errorf("expected release artifact not found: %s", name) + } + } +} diff --git a/magefiles/targets/targets.go b/magefiles/targets/targets.go index eff9919..1a1627a 100644 --- a/magefiles/targets/targets.go +++ b/magefiles/targets/targets.go @@ -54,13 +54,25 @@ func Install() error { var releaseTag = regexp.MustCompile(`^v1\.\d+\.\d+$`) // Release generates a new release. Expects a version tag in v1.x.x format. -func Release(tag string) (err error) { - mg.Deps(Tools) +// If dryRun is true, it creates a local tag and runs goreleaser without +// publishing, then deletes the tag. This can be used to verify release artifacts. +func Release(tag string, dryRun *bool) (err error) { + if err := installTool("goreleaser"); err != nil { + return err + } if !releaseTag.MatchString(tag) { return errors.New("TAG environment variable must be in semver v1.x.x format, but was " + tag) } + if dryRun != nil && *dryRun { + if err := sh.RunV("git", "tag", "-a", tag, "-m", tag); err != nil { + return err + } + defer func() { _ = sh.RunV("git", "tag", "--delete", tag) }() + return sh.RunV("goreleaser", "release", "--skip=publish", "--skip=validate", "--clean") + } + if err := sh.RunV("git", "tag", "-a", tag, "-m", tag); err != nil { return err } @@ -73,7 +85,7 @@ func Release(tag string) (err error) { _ = sh.RunV("git", "push", "--delete", "origin", tag) } }() - return sh.RunV("goreleaser", "release") + return sh.RunV("goreleaser", "release", "--clean") } // Clean removes the temporarily generated files from Release. @@ -81,23 +93,38 @@ func Clean() error { return sh.Rm("dist") } -var goTools = []string{ - "github.com/golangci/golangci-lint/v2/cmd/golangci-lint@v2.11.2", - "github.com/goreleaser/goreleaser/v2@v2.14.3", +var goTools = map[string]string{ + "lint": "github.com/golangci/golangci-lint/v2/cmd/golangci-lint@v2.11.2", + "goreleaser": "github.com/goreleaser/goreleaser/v2@v2.14.3", } // Tools installs the dev tools used by mage, such as golangci-lint. func Tools() error { for _, tool := range goTools { - if err := sh.Run("go", "install", tool); err != nil { - return fmt.Errorf("failed to install %s: %w", tool, err) + if err := installTool(tool); err != nil { + return err } } return nil } +func installTool(tool string) error { + version, ok := goTools[tool] + if !ok { + return fmt.Errorf("unknown tool %q", tool) + } + if err := sh.Run("go", "install", version); err != nil { + return fmt.Errorf("failed to install %s: %w", version, err) + } + return nil +} + // Lint runs golangci-lint on the codebase. func Lint() error { - mg.Deps(Tools) + err := installTool("lint") + if err != nil { + return err + } + return sh.RunV("golangci-lint", "run") }