refactor: fix contextcheck lint errors on magefile by passing mage context

This commit is contained in:
John Starich 2026-03-06 01:12:23 -06:00 committed by kolaente
parent cea8c7807d
commit 0a1104b75c
1 changed files with 116 additions and 106 deletions

View File

@ -87,8 +87,8 @@ func goDetectVerboseFlag() string {
return fmt.Sprintf("-v=%t", mg.Verbose()) return fmt.Sprintf("-v=%t", mg.Verbose())
} }
func runGitCommandWithOutput(arg ...string) (output []byte, err error) { func runGitCommandWithOutput(ctx context.Context, arg ...string) (output []byte, err error) {
cmd := exec.Command("git", arg...) cmd := exec.CommandContext(ctx, "git", arg...)
output, err = cmd.Output() output, err = cmd.Output()
if err != nil { if err != nil {
var ee *exec.ExitError var ee *exec.ExitError
@ -101,8 +101,8 @@ func runGitCommandWithOutput(arg ...string) (output []byte, err error) {
return output, nil return output, nil
} }
func getRawVersionString() (version string, err error) { func getRawVersionString(ctx context.Context) (version string, err error) {
version, err = getRawVersionNumber() version, err = getRawVersionNumber(ctx)
if err != nil { if err != nil {
return return
} }
@ -118,7 +118,7 @@ func getRawVersionString() (version string, err error) {
return return
} }
func getRawVersionNumber() (version string, err error) { func getRawVersionNumber(ctx context.Context) (version string, err error) {
versionEnv := os.Getenv("RELEASE_VERSION") versionEnv := os.Getenv("RELEASE_VERSION")
if versionEnv != "" { if versionEnv != "" {
return versionEnv, nil return versionEnv, nil
@ -132,19 +132,19 @@ func getRawVersionNumber() (version string, err error) {
return strings.Replace(os.Getenv("DRONE_BRANCH"), "release/v", "", 1), nil return strings.Replace(os.Getenv("DRONE_BRANCH"), "release/v", "", 1), nil
} }
versionBytes, err := runGitCommandWithOutput("describe", "--tags", "--always", "--abbrev=10") versionBytes, err := runGitCommandWithOutput(ctx, "describe", "--tags", "--always", "--abbrev=10")
return string(versionBytes), err return string(versionBytes), err
} }
func setVersion() error { func setVersion(ctx context.Context) error {
versionNumber, err := getRawVersionNumber() versionNumber, err := getRawVersionNumber(ctx)
if err != nil { if err != nil {
return err return err
} }
VersionNumber = strings.Trim(versionNumber, "\n") VersionNumber = strings.Trim(versionNumber, "\n")
VersionNumber = strings.Replace(VersionNumber, "-g", "-", 1) VersionNumber = strings.Replace(VersionNumber, "-g", "-", 1)
version, err := getRawVersionString() version, err := getRawVersionString(ctx)
if err != nil { if err != nil {
return fmt.Errorf("error getting version: %w", err) return fmt.Errorf("error getting version: %w", err)
} }
@ -178,13 +178,13 @@ func init() {
} }
// Some variables have external dependencies (like git) which may not always be available. // Some variables have external dependencies (like git) which may not always be available.
func initVars() error { func initVars(ctx context.Context) error {
// Always include osusergo to use pure Go os/user implementation instead of CGO. // Always include osusergo to use pure Go os/user implementation instead of CGO.
// This prevents SIGFPE crashes when running under systemd without HOME set, // This prevents SIGFPE crashes when running under systemd without HOME set,
// caused by glibc's getpwuid_r() failing in certain environments. // caused by glibc's getpwuid_r() failing in certain environments.
// See: https://github.com/go-vikunja/vikunja/issues/2170 // See: https://github.com/go-vikunja/vikunja/issues/2170
Tags = "osusergo " + strings.ReplaceAll(os.Getenv("TAGS"), ",", " ") Tags = "osusergo " + strings.ReplaceAll(os.Getenv("TAGS"), ",", " ")
if err := setVersion(); err != nil { if err := setVersion(ctx); err != nil {
return err return err
} }
setBinLocation() setBinLocation()
@ -193,8 +193,8 @@ func initVars() error {
return nil return nil
} }
func runAndStreamOutput(cmd string, args ...string) error { func runAndStreamOutput(ctx context.Context, cmd string, args ...string) error {
c := exec.Command(cmd, args...) c := exec.CommandContext(ctx, cmd, args...)
c.Env = os.Environ() c.Env = os.Environ()
c.Stdout = os.Stdout c.Stdout = os.Stdout
@ -206,10 +206,10 @@ func runAndStreamOutput(cmd string, args ...string) error {
// Will check if the tool exists and if not install it from the provided import path // Will check if the tool exists and if not install it from the provided import path
// If any errors occur, it will exit with a status code of 1. // If any errors occur, it will exit with a status code of 1.
func checkAndInstallGoTool(tool, importPath string) error { func checkAndInstallGoTool(ctx context.Context, tool, importPath string) error {
if err := exec.Command(tool).Run(); err != nil && strings.Contains(err.Error(), "executable file not found") { if err := exec.CommandContext(ctx, tool).Run(); err != nil && strings.Contains(err.Error(), "executable file not found") {
fmt.Printf("%s not installed, installing %s...\n", tool, importPath) fmt.Printf("%s not installed, installing %s...\n", tool, importPath)
if err := exec.Command("go", "install", goDetectVerboseFlag(), importPath).Run(); err != nil { //nolint:gosec // Every caller to checkAndInstallGoTool is hard-coded at time of writing, so no injection possible. if err := exec.CommandContext(ctx, "go", "install", goDetectVerboseFlag(), importPath).Run(); err != nil { //nolint:gosec // Every caller to checkAndInstallGoTool is hard-coded at time of writing, so no injection possible.
return fmt.Errorf("error installing %s: %w", tool, err) return fmt.Errorf("error installing %s: %w", tool, err)
} }
fmt.Println("Installed.") fmt.Println("Installed.")
@ -324,16 +324,16 @@ func printSuccess(text string, args ...any) {
} }
// getE2EPort returns the port from the given env var, or a random available port. // getE2EPort returns the port from the given env var, or a random available port.
func getE2EPort(envVar string) (int, error) { func getE2EPort(ctx context.Context, envVar string) (int, error) {
if v := os.Getenv(envVar); v != "" { if v := os.Getenv(envVar); v != "" {
return strconv.Atoi(v) return strconv.Atoi(v)
} }
return getRandomPort() return getRandomPort(ctx)
} }
// getRandomPort finds a random available TCP port. // getRandomPort finds a random available TCP port.
func getRandomPort() (int, error) { func getRandomPort(ctx context.Context) (int, error) {
l, err := net.Listen("tcp", "127.0.0.1:0") l, err := (&net.ListenConfig{}).Listen(ctx, "tcp", "127.0.0.1:0")
if err != nil { if err != nil {
return 0, err return 0, err
} }
@ -362,11 +362,15 @@ func killProcessGroup(cmd *exec.Cmd) error {
} }
// waitForHTTP polls a URL until it returns a 200 status or the timeout expires. // waitForHTTP polls a URL until it returns a 200 status or the timeout expires.
func waitForHTTP(url string, timeout time.Duration) error { func waitForHTTP(ctx context.Context, url string, timeout time.Duration) error {
deadline := time.Now().Add(timeout) deadline := time.Now().Add(timeout)
client := &http.Client{Timeout: 2 * time.Second} client := &http.Client{Timeout: 2 * time.Second}
for time.Now().Before(deadline) { for time.Now().Before(deadline) {
resp, err := client.Get(url) req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
if err != nil {
return err
}
resp, err := client.Do(req)
if err == nil { if err == nil {
resp.Body.Close() resp.Body.Close()
if resp.StatusCode == http.StatusOK { if resp.StatusCode == http.StatusOK {
@ -379,7 +383,7 @@ func waitForHTTP(url string, timeout time.Duration) error {
} }
// Fmt formats the code using go fmt // Fmt formats the code using go fmt
func Fmt() error { func Fmt(ctx context.Context) error {
mg.Deps(initVars) mg.Deps(initVars)
var goFiles []string var goFiles []string
err := filepath.Walk(".", func(path string, info fs.FileInfo, err error) error { err := filepath.Walk(".", func(path string, info fs.FileInfo, err error) error {
@ -395,37 +399,37 @@ func Fmt() error {
return err return err
} }
args := append([]string{"-s", "-w"}, goFiles...) args := append([]string{"-s", "-w"}, goFiles...)
return runAndStreamOutput("gofmt", args...) return runAndStreamOutput(ctx, "gofmt", args...)
} }
type Test mg.Namespace type Test mg.Namespace
// Feature runs the feature tests // Feature runs the feature tests
func (Test) Feature() error { func (Test) Feature(ctx context.Context) error {
mg.Deps(initVars) mg.Deps(initVars)
// We run everything sequentially and not in parallel to prevent issues with real test databases // We run everything sequentially and not in parallel to prevent issues with real test databases
return runAndStreamOutput("go", "test", goDetectVerboseFlag(), "-p", "1", "-coverprofile", "cover.out", "-timeout", "45m", "-short", "./...") return runAndStreamOutput(ctx, "go", "test", goDetectVerboseFlag(), "-p", "1", "-coverprofile", "cover.out", "-timeout", "45m", "-short", "./...")
} }
// Coverage runs the tests and builds the coverage html file from coverage output // Coverage runs the tests and builds the coverage html file from coverage output
func (Test) Coverage() error { func (Test) Coverage(ctx context.Context) error {
mg.Deps(initVars) mg.Deps(initVars)
mg.Deps(Test.Feature) mg.Deps(Test.Feature)
return runAndStreamOutput("go", "tool", "cover", "-html=cover.out", "-o", "cover.html") return runAndStreamOutput(ctx, "go", "tool", "cover", "-html=cover.out", "-o", "cover.html")
} }
// Web runs the web tests // Web runs the web tests
func (Test) Web() error { func (Test) Web(ctx context.Context) error {
mg.Deps(initVars) mg.Deps(initVars)
// We run everything sequentially and not in parallel to prevent issues with real test databases // We run everything sequentially and not in parallel to prevent issues with real test databases
args := []string{"test", goDetectVerboseFlag(), "-p", "1", "-timeout", "45m", "./pkg/webtests"} args := []string{"test", goDetectVerboseFlag(), "-p", "1", "-timeout", "45m", "./pkg/webtests"}
return runAndStreamOutput("go", args...) return runAndStreamOutput(ctx, "go", args...)
} }
func (Test) Filter(filter string) error { func (Test) Filter(ctx context.Context, filter string) error {
mg.Deps(initVars) mg.Deps(initVars)
// We run everything sequentially and not in parallel to prevent issues with real test databases // We run everything sequentially and not in parallel to prevent issues with real test databases
return runAndStreamOutput("go", "test", goDetectVerboseFlag(), "-p", "1", "-timeout", "45m", "-run", filter, "-short", "./...") return runAndStreamOutput(ctx, "go", "test", goDetectVerboseFlag(), "-p", "1", "-timeout", "45m", "-run", filter, "-short", "./...")
} }
func (Test) All() { func (Test) All() {
@ -436,9 +440,9 @@ func (Test) All() {
// E2EApi runs the end-to-end API tests in pkg/e2etests. // E2EApi runs the end-to-end API tests in pkg/e2etests.
// These tests use the real event system (not events.Fake()) to verify // These tests use the real event system (not events.Fake()) to verify
// the full async pipeline: web handler → DB → event dispatch → watermill → listener. // the full async pipeline: web handler → DB → event dispatch → watermill → listener.
func (Test) E2EApi() error { func (Test) E2EApi(ctx context.Context) error {
mg.Deps(initVars) mg.Deps(initVars)
return runAndStreamOutput("go", "test", goDetectVerboseFlag(), "-p", "1", "-timeout", "45m", "./pkg/e2etests") return runAndStreamOutput(ctx, "go", "test", goDetectVerboseFlag(), "-p", "1", "-timeout", "45m", "./pkg/e2etests")
} }
// E2E builds the API, starts it with an in-memory database and the frontend dev server, // E2E builds the API, starts it with an in-memory database and the frontend dev server,
@ -458,15 +462,15 @@ func (Test) E2EApi() error {
// - VIKUNJA_E2E_FRONTEND_PORT: Frontend port (default: random) // - VIKUNJA_E2E_FRONTEND_PORT: Frontend port (default: random)
// - VIKUNJA_E2E_TESTING_TOKEN: Testing token for seed endpoints (default: random) // - VIKUNJA_E2E_TESTING_TOKEN: Testing token for seed endpoints (default: random)
// - VIKUNJA_E2E_SKIP_BUILD: Set to "true" to skip rebuilding the API binary (default: false) // - VIKUNJA_E2E_SKIP_BUILD: Set to "true" to skip rebuilding the API binary (default: false)
func (Test) E2E(args string) error { func (Test) E2E(ctx context.Context, args string) error {
mg.Deps(initVars) mg.Deps(initVars)
// Determine ports // Determine ports
apiPort, err := getE2EPort("VIKUNJA_E2E_API_PORT") apiPort, err := getE2EPort(ctx, "VIKUNJA_E2E_API_PORT")
if err != nil { if err != nil {
return fmt.Errorf("could not get API port: %w", err) return fmt.Errorf("could not get API port: %w", err)
} }
frontendPort, err := getE2EPort("VIKUNJA_E2E_FRONTEND_PORT") frontendPort, err := getE2EPort(ctx, "VIKUNJA_E2E_FRONTEND_PORT")
if err != nil { if err != nil {
return fmt.Errorf("could not get frontend port: %w", err) return fmt.Errorf("could not get frontend port: %w", err)
} }
@ -485,7 +489,7 @@ func (Test) E2E(args string) error {
// Build the API binary (unless skipped) // Build the API binary (unless skipped)
if os.Getenv("VIKUNJA_E2E_SKIP_BUILD") != "true" { if os.Getenv("VIKUNJA_E2E_SKIP_BUILD") != "true" {
fmt.Println("\n--- Building API binary ---") fmt.Println("\n--- Building API binary ---")
if err := (Build{}).Build(); err != nil { if err := (Build{}).Build(ctx); err != nil {
return fmt.Errorf("failed to build API: %w", err) return fmt.Errorf("failed to build API: %w", err)
} }
} }
@ -507,7 +511,7 @@ func (Test) E2E(args string) error {
// Start the API server — all config via env vars, no config file // Start the API server — all config via env vars, no config file
// Uses in-memory SQLite (no DB file on disk) // Uses in-memory SQLite (no DB file on disk)
fmt.Println("\n--- Starting API server ---") fmt.Println("\n--- Starting API server ---")
apiCmd := exec.Command("./vikunja", "web") apiCmd := exec.CommandContext(ctx, "./vikunja", "web")
apiCmd.Env = append(os.Environ(), apiCmd.Env = append(os.Environ(),
fmt.Sprintf("VIKUNJA_SERVICE_INTERFACE=:%d", apiPort), fmt.Sprintf("VIKUNJA_SERVICE_INTERFACE=:%d", apiPort),
fmt.Sprintf("VIKUNJA_SERVICE_PUBLICURL=http://127.0.0.1:%d/", apiPort), fmt.Sprintf("VIKUNJA_SERVICE_PUBLICURL=http://127.0.0.1:%d/", apiPort),
@ -538,14 +542,14 @@ func (Test) E2E(args string) error {
// Wait for API to be ready // Wait for API to be ready
apiBase := fmt.Sprintf("http://127.0.0.1:%d/api/v1", apiPort) apiBase := fmt.Sprintf("http://127.0.0.1:%d/api/v1", apiPort)
fmt.Printf("Waiting for API at %s ...\n", apiBase) fmt.Printf("Waiting for API at %s ...\n", apiBase)
if err := waitForHTTP(apiBase+"/info", 30*time.Second); err != nil { if err := waitForHTTP(ctx, apiBase+"/info", 30*time.Second); err != nil {
return fmt.Errorf("API failed to start: %w", err) return fmt.Errorf("API failed to start: %w", err)
} }
printSuccess("API is ready!") printSuccess("API is ready!")
// Build the frontend // Build the frontend
fmt.Println("\n--- Building frontend ---") fmt.Println("\n--- Building frontend ---")
buildFrontendCmd := exec.Command("pnpm", "build:dev") buildFrontendCmd := exec.CommandContext(ctx, "pnpm", "build:dev")
buildFrontendCmd.Dir = "frontend" buildFrontendCmd.Dir = "frontend"
buildFrontendCmd.Stdout = os.Stdout buildFrontendCmd.Stdout = os.Stdout
buildFrontendCmd.Stderr = os.Stderr buildFrontendCmd.Stderr = os.Stderr
@ -556,7 +560,7 @@ func (Test) E2E(args string) error {
// Serve the built frontend with vite preview (static, no file watchers) // Serve the built frontend with vite preview (static, no file watchers)
fmt.Println("\n--- Starting frontend preview server ---") fmt.Println("\n--- Starting frontend preview server ---")
frontendCmd := exec.Command("pnpm", "preview:dev", "--port", strconv.Itoa(frontendPort)) //nolint:gosec // This mage task runs end to end tests with environment-based configuration, it must use the port environment variable to suit its current environment. frontendCmd := exec.CommandContext(ctx, "pnpm", "preview:dev", "--port", strconv.Itoa(frontendPort)) //nolint:gosec // This mage task runs end to end tests with environment-based configuration, it must use the port environment variable to suit its current environment.
frontendCmd.Dir = "frontend" frontendCmd.Dir = "frontend"
frontendCmd.Stdout = os.Stdout frontendCmd.Stdout = os.Stdout
frontendCmd.Stderr = os.Stderr frontendCmd.Stderr = os.Stderr
@ -574,7 +578,7 @@ func (Test) E2E(args string) error {
// Wait for frontend to be ready // Wait for frontend to be ready
frontendBase := fmt.Sprintf("http://127.0.0.1:%d", frontendPort) frontendBase := fmt.Sprintf("http://127.0.0.1:%d", frontendPort)
fmt.Printf("Waiting for frontend at %s ...\n", frontendBase) fmt.Printf("Waiting for frontend at %s ...\n", frontendBase)
if err := waitForHTTP(frontendBase, 60*time.Second); err != nil { if err := waitForHTTP(ctx, frontendBase, 60*time.Second); err != nil {
return fmt.Errorf("frontend failed to start: %w", err) return fmt.Errorf("frontend failed to start: %w", err)
} }
printSuccess("Frontend is ready!") printSuccess("Frontend is ready!")
@ -585,7 +589,7 @@ func (Test) E2E(args string) error {
if strings.TrimSpace(args) != "" { if strings.TrimSpace(args) != "" {
playwrightArgs = append(playwrightArgs, strings.Fields(args)...) playwrightArgs = append(playwrightArgs, strings.Fields(args)...)
} }
playwrightCmd := exec.Command("pnpm", playwrightArgs...) playwrightCmd := exec.CommandContext(ctx, "pnpm", playwrightArgs...)
playwrightCmd.Dir = "frontend" playwrightCmd.Dir = "frontend"
playwrightCmd.Env = append(os.Environ(), playwrightCmd.Env = append(os.Environ(),
fmt.Sprintf("API_URL=%s/", apiBase), fmt.Sprintf("API_URL=%s/", apiBase),
@ -609,7 +613,7 @@ func (Test) E2E(args string) error {
type Check mg.Namespace type Check mg.Namespace
// GotSwag checks if the swagger docs need to be re-generated from the code annotations // GotSwag checks if the swagger docs need to be re-generated from the code annotations
func (Check) GotSwag() error { func (Check) GotSwag(ctx context.Context) error {
mg.Deps(initVars) mg.Deps(initVars)
// The check is pretty cheaply done: We take the hash of the swagger.json file, generate the docs, // The check is pretty cheaply done: We take the hash of the swagger.json file, generate the docs,
// hash the file again and compare the two hashes to see if anything changed. If that's the case, // hash the file again and compare the two hashes to see if anything changed. If that's the case,
@ -622,7 +626,7 @@ func (Check) GotSwag() error {
return fmt.Errorf("error getting old hash of the swagger docs: %w", err) return fmt.Errorf("error getting old hash of the swagger docs: %w", err)
} }
if generateErr := (Generate{}).SwaggerDocs(); generateErr != nil { if generateErr := (Generate{}).SwaggerDocs(ctx); generateErr != nil {
return generateErr return generateErr
} }
@ -793,26 +797,26 @@ func extractTranslationKeysFromFile(filePath string) ([]TranslationKey, error) {
return keys, nil return keys, nil
} }
func checkGolangCiLintInstalled() error { func checkGolangCiLintInstalled(ctx context.Context) error {
mg.Deps(initVars) mg.Deps(initVars)
if err := exec.Command("golangci-lint").Run(); err != nil && strings.Contains(err.Error(), "executable file not found") { if err := exec.CommandContext(ctx, "golangci-lint").Run(); err != nil && strings.Contains(err.Error(), "executable file not found") {
return fmt.Errorf("golangci-lint executable failed to run, please manually install golangci-lint by running the command: curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b $(go env GOPATH)/bin v2.4.0") return fmt.Errorf("golangci-lint executable failed to run, please manually install golangci-lint by running the command: curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b $(go env GOPATH)/bin v2.4.0")
} }
return nil return nil
} }
func (Check) Golangci() error { func (Check) Golangci(ctx context.Context) error {
if err := checkGolangCiLintInstalled(); err != nil { if err := checkGolangCiLintInstalled(ctx); err != nil {
return err return err
} }
return runAndStreamOutput("golangci-lint", "run") return runAndStreamOutput(ctx, "golangci-lint", "run")
} }
func (Check) GolangciFix() error { func (Check) GolangciFix(ctx context.Context) error {
if err := checkGolangCiLintInstalled(); err != nil { if err := checkGolangCiLintInstalled(ctx); err != nil {
return err return err
} }
return runAndStreamOutput("golangci-lint", "run", "--fix") return runAndStreamOutput(ctx, "golangci-lint", "run", "--fix")
} }
// All runs golangci and the swagger test in parallel // All runs golangci and the swagger test in parallel
@ -828,9 +832,9 @@ func (Check) All() {
type Build mg.Namespace type Build mg.Namespace
// Clean cleans all build, executable and bindata files // Clean cleans all build, executable and bindata files
func (Build) Clean() error { func (Build) Clean(ctx context.Context) error {
mg.Deps(initVars) mg.Deps(initVars)
if err := exec.Command("go", "clean", "./...").Run(); err != nil { if err := exec.CommandContext(ctx, "go", "clean", "./...").Run(); err != nil {
return err return err
} }
if err := os.Remove(Executable); err != nil && !os.IsNotExist(err) { if err := os.Remove(Executable); err != nil && !os.IsNotExist(err) {
@ -846,7 +850,7 @@ func (Build) Clean() error {
} }
// Build builds a vikunja binary, ready to run // Build builds a vikunja binary, ready to run
func (Build) Build() error { func (Build) Build(ctx context.Context) error {
mg.Deps(initVars) mg.Deps(initVars)
// Check if the frontend dist folder exists // Check if the frontend dist folder exists
distPath := filepath.Join("frontend", "dist") distPath := filepath.Join("frontend", "dist")
@ -866,7 +870,7 @@ func (Build) Build() error {
fmt.Printf("Warning: %s not found, created empty file\n", indexFile) fmt.Printf("Warning: %s not found, created empty file\n", indexFile)
} }
return runAndStreamOutput("go", "build", goDetectVerboseFlag(), "-tags", Tags, "-ldflags", "-s -w "+Ldflags, "-o", Executable) return runAndStreamOutput(ctx, "go", "build", goDetectVerboseFlag(), "-tags", Tags, "-ldflags", "-s -w "+Ldflags, "-o", Executable)
} }
func (Build) SaveVersionToFile() error { func (Build) SaveVersionToFile() error {
@ -898,9 +902,9 @@ func (Release) Release(ctx context.Context) error {
// Run compiling in parallel to speed it up // Run compiling in parallel to speed it up
errs, _ := errgroup.WithContext(ctx) errs, _ := errgroup.WithContext(ctx)
errs.Go((Release{}).Windows) errgroupGoWithContext(ctx, errs, (Release{}).Windows)
errs.Go((Release{}).Linux) errgroupGoWithContext(ctx, errs, (Release{}).Linux)
errs.Go((Release{}).Darwin) errgroupGoWithContext(ctx, errs, (Release{}).Darwin)
if err := errs.Wait(); err != nil { if err := errs.Wait(); err != nil {
return err return err
} }
@ -917,13 +921,19 @@ func (Release) Release(ctx context.Context) error {
if err := (Release{}).OsPackage(); err != nil { if err := (Release{}).OsPackage(); err != nil {
return err return err
} }
if err := (Release{}).Zip(); err != nil { if err := (Release{}).Zip(ctx); err != nil {
return err return err
} }
return nil return nil
} }
func errgroupGoWithContext(ctx context.Context, errs *errgroup.Group, do func(context.Context) error) {
errs.Go(func() error {
return do(ctx)
})
}
// Dirs creates all directories needed to release vikunja // Dirs creates all directories needed to release vikunja
func (Release) Dirs() error { func (Release) Dirs() error {
for _, d := range []string{"binaries", "release", "zip"} { for _, d := range []string{"binaries", "release", "zip"} {
@ -934,19 +944,19 @@ func (Release) Dirs() error {
return nil return nil
} }
func prepareXgo() error { func prepareXgo(ctx context.Context) error {
mg.Deps(initVars) mg.Deps(initVars)
if err := checkAndInstallGoTool("xgo", "src.techknowlogick.com/xgo"); err != nil { if err := checkAndInstallGoTool(ctx, "xgo", "src.techknowlogick.com/xgo"); err != nil {
return err return err
} }
fmt.Println("Pulling latest xgo docker image...") fmt.Println("Pulling latest xgo docker image...")
return runAndStreamOutput("docker", "pull", "ghcr.io/techknowlogick/xgo:latest") return runAndStreamOutput(ctx, "docker", "pull", "ghcr.io/techknowlogick/xgo:latest")
} }
func runXgo(targets string) error { func runXgo(ctx context.Context, targets string) error {
mg.Deps(initVars) mg.Deps(initVars)
if err := checkAndInstallGoTool("xgo", "src.techknowlogick.com/xgo"); err != nil { if err := checkAndInstallGoTool(ctx, "xgo", "src.techknowlogick.com/xgo"); err != nil {
return err return err
} }
@ -961,7 +971,7 @@ func runXgo(targets string) error {
outName = Executable + "-" + Version outName = Executable + "-" + Version
} }
if err := runAndStreamOutput("xgo", if err := runAndStreamOutput(ctx, "xgo",
"-dest", "./"+DIST+"/binaries", "-dest", "./"+DIST+"/binaries",
"-tags", "netgo "+Tags, "-tags", "netgo "+Tags,
"-ldflags", extraLdflags+Ldflags, "-ldflags", extraLdflags+Ldflags,
@ -987,12 +997,12 @@ func runXgo(targets string) error {
} }
// Windows builds binaries for windows // Windows builds binaries for windows
func (Release) Windows() error { func (Release) Windows(ctx context.Context) error {
return runXgo("windows/*") return runXgo(ctx, "windows/*")
} }
// Linux builds binaries for linux // Linux builds binaries for linux
func (Release) Linux() error { func (Release) Linux(ctx context.Context) error {
targets := []string{ targets := []string{
"linux/amd64", "linux/amd64",
"linux/arm-5", "linux/arm-5",
@ -1005,15 +1015,15 @@ func (Release) Linux() error {
"linux/mips64le", "linux/mips64le",
"linux/riscv64", "linux/riscv64",
} }
return runXgo(strings.Join(targets, ",")) return runXgo(ctx, strings.Join(targets, ","))
} }
// Darwin builds binaries for darwin // Darwin builds binaries for darwin
func (Release) Darwin() error { func (Release) Darwin(ctx context.Context) error {
return runXgo("darwin-10.15/*") return runXgo(ctx, "darwin-10.15/*")
} }
func (Release) Xgo(target string) error { func (Release) Xgo(ctx context.Context, target string) error {
parts := strings.Split(target, "/") parts := strings.Split(target, "/")
if len(parts) < 2 { if len(parts) < 2 {
return fmt.Errorf("invalid target") return fmt.Errorf("invalid target")
@ -1024,7 +1034,7 @@ func (Release) Xgo(target string) error {
variant = "-" + strings.ReplaceAll(parts[2], "v", "") variant = "-" + strings.ReplaceAll(parts[2], "v", "")
} }
return runXgo(parts[0] + "/" + parts[1] + variant) return runXgo(ctx, parts[0]+"/"+parts[1]+variant)
} }
// Compress compresses the built binaries in dist/binaries/ to reduce their filesize // Compress compresses the built binaries in dist/binaries/ to reduce their filesize
@ -1052,10 +1062,10 @@ func (Release) Compress(ctx context.Context) error {
// Runs compressing in parallel since upx is single-threaded // Runs compressing in parallel since upx is single-threaded
errs.Go(func() error { errs.Go(func() error {
if err := runAndStreamOutput("chmod", "+x", path); err != nil { // Make sure all binaries are executable. Sometimes the CI does weird things and they're not. if err := runAndStreamOutput(ctx, "chmod", "+x", path); err != nil { // Make sure all binaries are executable. Sometimes the CI does weird things and they're not.
return err return err
} }
return runAndStreamOutput("upx", "-9", path) return runAndStreamOutput(ctx, "upx", "-9", path)
}) })
return nil return nil
@ -1155,7 +1165,7 @@ func (Release) OsPackage() error {
} }
// Zip creates a zip file from all os-package folders in dist/release // Zip creates a zip file from all os-package folders in dist/release
func (Release) Zip() error { func (Release) Zip(ctx context.Context) error {
rootDir, err := os.Getwd() rootDir, err := os.Getwd()
if err != nil { if err != nil {
return fmt.Errorf("could not get working directory: %w", err) return fmt.Errorf("could not get working directory: %w", err)
@ -1173,7 +1183,7 @@ func (Release) Zip() error {
fmt.Printf("Zipping %s...\n", info.Name()) fmt.Printf("Zipping %s...\n", info.Name())
zipFile := filepath.Join(rootDir, DIST, "zip", info.Name()+".zip") zipFile := filepath.Join(rootDir, DIST, "zip", info.Name()+".zip")
c := exec.Command("zip", "-r", zipFile, ".", "-i", "*") //nolint:gosec // This mage task creates zips of every directory recursively, it must use the directory name in the resulting file path to distinguish output files. c := exec.CommandContext(ctx, "zip", "-r", zipFile, ".", "-i", "*") //nolint:gosec // This mage task creates zips of every directory recursively, it must use the directory name in the resulting file path to distinguish output files.
c.Dir = path c.Dir = path
out, err := c.Output() out, err := c.Output()
fmt.Print(string(out)) fmt.Print(string(out))
@ -1186,9 +1196,9 @@ func (Release) Zip() error {
} }
// Reprepro creates a debian repo structure // Reprepro creates a debian repo structure
func (Release) Reprepro() error { func (Release) Reprepro(ctx context.Context) error {
mg.Deps(setVersion, setBinLocation) mg.Deps(setVersion, setBinLocation)
return runAndStreamOutput("reprepro_expect", "debian", "includedeb", "buster", "./"+DIST+"/os-packages/"+Executable+"_"+strings.ReplaceAll(VersionNumber, "v0", "0")+"_amd64.deb") return runAndStreamOutput(ctx, "reprepro_expect", "debian", "includedeb", "buster", "./"+DIST+"/os-packages/"+Executable+"_"+strings.ReplaceAll(VersionNumber, "v0", "0")+"_amd64.deb")
} }
// PrepareNFPMConfig prepares the nfpm config // PrepareNFPMConfig prepares the nfpm config
@ -1215,7 +1225,7 @@ func (Release) PrepareNFPMConfig() error {
} }
// Packages creates deb, rpm and apk packages // Packages creates deb, rpm and apk packages
func (Release) Packages() error { func (Release) Packages(ctx context.Context) error {
mg.Deps(initVars) mg.Deps(initVars)
var err error var err error
@ -1223,10 +1233,10 @@ func (Release) Packages() error {
if binpath == "" { if binpath == "" {
binpath = "nfpm" binpath = "nfpm"
} }
err = exec.Command(binpath).Run() err = exec.CommandContext(ctx, binpath).Run()
if err != nil && strings.Contains(err.Error(), "executable file not found") { if err != nil && strings.Contains(err.Error(), "executable file not found") {
binpath = "/usr/bin/nfpm" binpath = "/usr/bin/nfpm"
err = exec.Command(binpath).Run() err = exec.CommandContext(ctx, binpath).Run()
} }
if err != nil && strings.Contains(err.Error(), "executable file not found") { if err != nil && strings.Contains(err.Error(), "executable file not found") {
return fmt.Errorf("executable %s not found: please manually install nfpm by running the command: curl -sfL https://install.goreleaser.com/github.com/goreleaser/nfpm.sh | sh -s -- -b $(go env GOPATH)/bin", binpath) return fmt.Errorf("executable %s not found: please manually install nfpm by running the command: curl -sfL https://install.goreleaser.com/github.com/goreleaser/nfpm.sh | sh -s -- -b $(go env GOPATH)/bin", binpath)
@ -1242,13 +1252,13 @@ func (Release) Packages() error {
return err return err
} }
if err := runAndStreamOutput(binpath, "pkg", "--packager", "deb", "--target", releasePath); err != nil { if err := runAndStreamOutput(ctx, binpath, "pkg", "--packager", "deb", "--target", releasePath); err != nil {
return err return err
} }
if err := runAndStreamOutput(binpath, "pkg", "--packager", "rpm", "--target", releasePath); err != nil { if err := runAndStreamOutput(ctx, binpath, "pkg", "--packager", "rpm", "--target", releasePath); err != nil {
return err return err
} }
if err := runAndStreamOutput(binpath, "pkg", "--packager", "apk", "--target", releasePath); err != nil { if err := runAndStreamOutput(ctx, binpath, "pkg", "--packager", "apk", "--target", releasePath); err != nil {
return err return err
} }
@ -1493,13 +1503,13 @@ type Generate mg.Namespace
const DefaultConfigYAMLSamplePath = "config.yml.sample" const DefaultConfigYAMLSamplePath = "config.yml.sample"
// SwaggerDocs generates the swagger docs from the code annotations // SwaggerDocs generates the swagger docs from the code annotations
func (Generate) SwaggerDocs() error { func (Generate) SwaggerDocs(ctx context.Context) error {
mg.Deps(initVars) mg.Deps(initVars)
if err := checkAndInstallGoTool("swag", "github.com/swaggo/swag/cmd/swag"); err != nil { if err := checkAndInstallGoTool(ctx, "swag", "github.com/swaggo/swag/cmd/swag"); err != nil {
return err return err
} }
return runAndStreamOutput("swag", "init", "-g", "./pkg/routes/routes.go", "--parseDependency", "-d", ".", "-o", "./pkg/swagger") return runAndStreamOutput(ctx, "swag", "init", "-g", "./pkg/routes/routes.go", "--parseDependency", "-d", ".", "-o", "./pkg/swagger")
} }
type ConfigNode struct { type ConfigNode struct {
@ -1636,7 +1646,7 @@ func (Generate) ConfigYAML(commented bool) {
// The second argument is a path to a plan file that will be copied to the new worktree (pass "" to skip). // The second argument is a path to a plan file that will be copied to the new worktree (pass "" to skip).
// The worktree is created in the parent directory (../). // The worktree is created in the parent directory (../).
// It also copies the current config.yml with an updated rootpath, and initializes the frontend. // It also copies the current config.yml with an updated rootpath, and initializes the frontend.
func (Dev) PrepareWorktree(name string, planPath string) error { func (Dev) PrepareWorktree(ctx context.Context, name string, planPath string) error {
if name == "" { if name == "" {
return fmt.Errorf("name is required: mage dev:prepare-worktree <name> <plan-path>") return fmt.Errorf("name is required: mage dev:prepare-worktree <name> <plan-path>")
} }
@ -1647,7 +1657,7 @@ func (Dev) PrepareWorktree(name string, planPath string) error {
fmt.Printf("Creating worktree at %s with branch %s...\n", worktreePath, name) fmt.Printf("Creating worktree at %s with branch %s...\n", worktreePath, name)
// Create the git worktree // Create the git worktree
cmd := exec.Command("git", "worktree", "add", worktreePath, "-b", name) cmd := exec.CommandContext(ctx, "git", "worktree", "add", worktreePath, "-b", name)
cmd.Stdout = os.Stdout cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr cmd.Stderr = os.Stderr
if err := cmd.Run(); err != nil { if err := cmd.Run(); err != nil {
@ -1728,7 +1738,7 @@ func (Dev) PrepareWorktree(name string, planPath string) error {
frontendDir := filepath.Join(worktreePath, "frontend") frontendDir := filepath.Join(worktreePath, "frontend")
// Run pnpm install // Run pnpm install
pnpmCmd := exec.Command("pnpm", "i") pnpmCmd := exec.CommandContext(ctx, "pnpm", "i")
pnpmCmd.Dir = frontendDir pnpmCmd.Dir = frontendDir
pnpmCmd.Stdout = os.Stdout pnpmCmd.Stdout = os.Stdout
pnpmCmd.Stderr = os.Stderr pnpmCmd.Stderr = os.Stderr
@ -1737,7 +1747,7 @@ func (Dev) PrepareWorktree(name string, planPath string) error {
} }
// Run patch-sass-embedded (shell alias from devenv) // Run patch-sass-embedded (shell alias from devenv)
patchCmd := exec.Command("bash", "-ic", "patch-sass-embedded") patchCmd := exec.CommandContext(ctx, "bash", "-ic", "patch-sass-embedded")
patchCmd.Dir = frontendDir patchCmd.Dir = frontendDir
patchCmd.Stdout = os.Stdout patchCmd.Stdout = os.Stdout
patchCmd.Stderr = os.Stderr patchCmd.Stderr = os.Stderr
@ -1756,8 +1766,8 @@ func (Dev) PrepareWorktree(name string, planPath string) error {
} }
// printReleaseStats prints commit statistics for the range between two refs. // printReleaseStats prints commit statistics for the range between two refs.
func printReleaseStats(fromRef, toRef string) error { func printReleaseStats(ctx context.Context, fromRef, toRef string) error {
output, err := runGitCommandWithOutput("log", fromRef+".."+toRef, "--oneline") output, err := runGitCommandWithOutput(ctx, "log", fromRef+".."+toRef, "--oneline")
if err != nil { if err != nil {
return fmt.Errorf("failed to get commit log: %w", err) return fmt.Errorf("failed to get commit log: %w", err)
} }
@ -1811,7 +1821,7 @@ func printReleaseStats(fromRef, toRef string) error {
// TagRelease creates a new release tag with changelog. // TagRelease creates a new release tag with changelog.
// It updates the version badge in README.md, generates changelog using git-cliff, // It updates the version badge in README.md, generates changelog using git-cliff,
// commits the changes, and creates an annotated tag. // commits the changes, and creates an annotated tag.
func (Dev) TagRelease(version string) error { func (Dev) TagRelease(ctx context.Context, version string) error {
if version == "" { if version == "" {
return fmt.Errorf("version is required: mage dev:tag-release <version>") return fmt.Errorf("version is required: mage dev:tag-release <version>")
} }
@ -1824,7 +1834,7 @@ func (Dev) TagRelease(version string) error {
fmt.Printf("Creating release %s...\n", version) fmt.Printf("Creating release %s...\n", version)
// Get the last tag // Get the last tag
lastTagBytes, err := runGitCommandWithOutput("describe", "--tags", "--abbrev=0") lastTagBytes, err := runGitCommandWithOutput(ctx, "describe", "--tags", "--abbrev=0")
if err != nil { if err != nil {
return fmt.Errorf("failed to get last tag: %w", err) return fmt.Errorf("failed to get last tag: %w", err)
} }
@ -1832,13 +1842,13 @@ func (Dev) TagRelease(version string) error {
fmt.Printf("Last tag: %s\n", lastTag) fmt.Printf("Last tag: %s\n", lastTag)
// Print commit statistics // Print commit statistics
if err := printReleaseStats(lastTag, "HEAD"); err != nil { if err := printReleaseStats(ctx, lastTag, "HEAD"); err != nil {
fmt.Printf("Warning: could not print release stats: %v\n", err) fmt.Printf("Warning: could not print release stats: %v\n", err)
} }
// Generate changelog using git cliff // Generate changelog using git cliff
fmt.Println("Generating changelog...") fmt.Println("Generating changelog...")
changelogBytes, err := runGitCommandWithOutput("cliff", lastTag+"..HEAD", "--tag", version) changelogBytes, err := runGitCommandWithOutput(ctx, "cliff", lastTag+"..HEAD", "--tag", version)
if err != nil { if err != nil {
return fmt.Errorf("failed to generate changelog: %w", err) return fmt.Errorf("failed to generate changelog: %w", err)
} }
@ -1868,12 +1878,12 @@ func (Dev) TagRelease(version string) error {
// Commit the changes // Commit the changes
fmt.Println("Committing changes...") fmt.Println("Committing changes...")
commitMsg := fmt.Sprintf("chore: %s release preparations", version) commitMsg := fmt.Sprintf("chore: %s release preparations", version)
cmd := exec.Command("git", "add", "README.md", "CHANGELOG.md", "frontend/package.json") cmd := exec.CommandContext(ctx, "git", "add", "README.md", "CHANGELOG.md", "frontend/package.json")
if err := cmd.Run(); err != nil { if err := cmd.Run(); err != nil {
return fmt.Errorf("failed to stage files: %w", err) return fmt.Errorf("failed to stage files: %w", err)
} }
cmd = exec.Command("git", "commit", "-m", commitMsg) cmd = exec.CommandContext(ctx, "git", "commit", "-m", commitMsg)
cmd.Stdout = os.Stdout cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr cmd.Stderr = os.Stderr
if err := cmd.Run(); err != nil { if err := cmd.Run(); err != nil {
@ -1885,7 +1895,7 @@ func (Dev) TagRelease(version string) error {
// Create the annotated tag // Create the annotated tag
fmt.Printf("Creating tag %s...\n", version) fmt.Printf("Creating tag %s...\n", version)
cmd = exec.Command("git", "tag", "-a", version, "-m", tagMessage) cmd = exec.CommandContext(ctx, "git", "tag", "-a", version, "-m", tagMessage)
cmd.Stdout = os.Stdout cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr cmd.Stderr = os.Stderr
if err := cmd.Run(); err != nil { if err := cmd.Run(); err != nil {
@ -2070,7 +2080,7 @@ func prepareTagMessage(changelog string) string {
type Plugins mg.Namespace type Plugins mg.Namespace
// Build compiles a Go plugin at the provided path. // Build compiles a Go plugin at the provided path.
func (Plugins) Build(pathToSourceFiles string) error { func (Plugins) Build(ctx context.Context, pathToSourceFiles string) error {
mg.Deps(initVars) mg.Deps(initVars)
if pathToSourceFiles == "" { if pathToSourceFiles == "" {
return fmt.Errorf("please provide a plugin path") return fmt.Errorf("please provide a plugin path")
@ -2086,5 +2096,5 @@ func (Plugins) Build(pathToSourceFiles string) error {
} }
out := filepath.Join("plugins", filepath.Base(pathToSourceFiles)+".so") out := filepath.Join("plugins", filepath.Base(pathToSourceFiles)+".so")
return runAndStreamOutput("go", "build", "-buildmode=plugin", "-tags", Tags, "-o", out, pathToSourceFiles) return runAndStreamOutput(ctx, "go", "build", "-buildmode=plugin", "-tags", Tags, "-o", out, pathToSourceFiles)
} }