Library / SDK
goyek/goyek avatar
goyek/goyek

goyek: a Go library for build tasks instead of a Makefile

Task automation Go library

701 stars39 forksGoNOASSERTION

At a glance

What is it?
goyek is a Go library for expressing build automation as ordinary Go code, so tasks are defined with Define at startup, run through a Flow with dependencies, and are debugged with the Go toolchain rather than a separate DSL. It suits Go projects that want typed build logic and no extra binary to install, and the trade is that you give up the declarative readability of a Makefile and accept that your build toolchain becomes a Go module dependency.
Who is it for?
Adopt goyek if your build logic is already Go, or is going to be, since you get type checking, IDE navigation and the standard debugger on your build steps rather than a separate DSL. Do not adopt it if you want a build description that a colleague can read without knowing Go, and check first how your build directory becomes a module, because the pattern depends on the build code living in its own directory with its own go.mod, which changes how the repository is laid out.
Can I use it commercially?
Check first. The repository uses a licence we do not classify automatically, so read its LICENSE file before any commercial use.
Is it still maintained?
Yes. The repository last received commits 2 days ago.
What is it written in?
Mainly Go, according to GitHub's language statistics.

Answers come from the project's GitHub data, last synced on October 23, 2026, and from our analysis. They are not legal advice.

Editorial analysis

Why a library and not a binary

goyek is a task automation library intended to be an alternative to Make, Mage and Task, and the design decision that defines everything else is that it is a library first, not a standalone binary.

The rationale section explains where that came from. goyek was originally built as a small library for expressing Go build pipelines in Go itself instead of relying on an external DSL or tool. Keeping build logic as regular Go code makes it easy to reuse existing packages, refactor with IDE support, and debug tasks with the standard Go toolchain.

The stated properties follow from that. A small, library-first design, not a standalone binary, with an API inspired by testing, cobra, flag and http. Cross-platform and shell independent. No binary installation required. Easy to debug, like regular Go code. Tasks are defined similarly to cobra commands. And task actions look like Go tests.

That last pair of comparisons is the one to hold on to, because it explains what a goyek task looks like in practice. It is not a target in a Makefile with a shell command under it, it is a function, and the task definition carries a name, a usage string and an action closure. The `A` type that the action receives is the equivalent of a test's `t`, and the methods on it are the equivalent of `t.Log`, `t.Error` and `t.Fatal`. You write `a.Log` and `a.Fatal` and you get logging and a non-zero exit with the right semantics, because the library borrowed them from the testing package.

The project lives at github.com/goyek/goyek with the v3 module path, the last push was on 2026-10-15, and the recent releases are v3.0.1 from 2025-12-09, v3.0.0 from 2025-11-25 and v2.3.0 from 2025-02-25.

A hello task in two files

The usage example puts the build code in a `build` directory, and there are two files: one defining the task and one running it.

The task definition is a Go file that registers itself at package init:

go
package main

import (
	"flag"

	"github.com/goyek/goyek/v3"
	"github.com/goyek/x/cmd"
)

var msg = flag.String("msg", "greeting message", "Hello world!")

var hello = goyek.Define(goyek.Task{
	Name:  "hello",
	Usage: "demonstration",
	Action: func(a *goyek.A) {
		a.Log(*msg)
		cmd.Exec(a, "go version")
	},
})

Note what `goyek.Define` does and where it is called. It is at package level, so it runs when the package is initialised, and the returned value is a package-level variable. That is what lets a task exist before the runner starts, and it is why the best practices section says to define tasks at startup, registering with `goyek.Define` during package init before calling `Main` or `Execute`, and to avoid mutating task definitions afterwards.

The `flag.String` for the message is the other half of the design. Because the API is inspired by the flag package, a task can take flags, and the help output shows them. The expected output confirms it, with `-msg string` listed under Flags with its default value.

The entry point is a second file:

go
package main

import (
	"os"

	"github.com/goyek/goyek/v3"
	"github.com/goyek/x/boot"
)

func main() {
	if err := os.Chdir(".."); err != nil {
		panic(err)
	}
	goyek.SetDefault(hello)
	boot.Main()
}

The `os.Chdir("..")` is the detail that makes the layout work. The build program lives in the `build` directory, and when it runs it moves up one level so that the tasks execute with the repository root as the working directory. The build code stays in its own place, but the commands it runs happen where a build would normally happen.

`goyek.SetDefault(hello)` registers the task, and `boot.Main()` from the `github.com/goyek/x` package starts the runner. The packages from that module are used for convenience, and `boot` is one of them.

The flags you get without writing them

The help output is the best evidence for what the runner provides, because those flags are the runner's behaviour and not the task's.

The expected usage is `Usage of build: [tasks] [flags] [--] [args]`, and the flags are `-dry-run` to print all tasks without executing actions, `-long-run duration` to print when a task takes longer with a default of 1m0s, `-msg string` which is the task's own flag, `-no-color` to disable colorizing output, `-no-deps` to do not process dependencies, `-skip` for comma-separated tasks to skip processing, and `-v` to print all tasks as they are run.

Several of those are decisions about what a build tool should assume. A dry run that prints without executing is a CI safety feature. A long-run threshold with a one minute default is a performance warning that fires without you configuring anything. A skip list is a way to run everything except the two tasks you know are broken. And no-color exists because a build tool's output is often piped into a log file where escape sequences are noise.

The verbose output shows the task framing:

out
===== TASK  hello
      hello.go:16: greeting message
      hello.go:17: Exec: go version
go version go1.24.0 linux/amd64
----- PASS: hello (0.12s)
ok      0.123s

The `hello.go:16` and `hello.go:17` prefixes are the `A` type doing what `t.Log` does, giving you the file and line of the call site, and the `PASS` line mirrors the testing package's output format, which is where the API inspiration becomes visible in the terminal.

Running it is a normal Go run:

sh
cd build
go mod tidy
go run . -h

The `go mod tidy` step matters, because the build directory is its own Go module with its own dependencies, including the goyek module and the convenience packages. That is the cost of the library-first design: your build code is a module like any other, and it has to resolve its own dependencies.

Middlewares, dependencies and the concurrency rules

The best practices section is longer than the usage section, and it is where the library's real design shows.

The list of middlewares is short and covers the cross-cutting concerns a build tool needs: `middleware.ReportStatus`, `middleware.ReportLongRun`, `middleware.DryRun`, `middleware.BufferParallel`, plugged in via `goyek.Use` and `goyek.UseExecutor`. ReportStatus is what produces the PASS and duration lines, ReportLongRun is the slow task warning, DryRun is the no-execute mode, and BufferParallel exists to keep output readable when tasks run at the same time.

The output ownership rule is the subtle one and it is stated at length. `Flow.Execute` and `Flow.Main` serialize writes made through the output they provide. Middleware that replaces the output owns its replacement. If the destination is not independently safe for concurrent use, you need `goyek.SyncWriter` with the low-level `Input` or `ExecuteInput` values or a replacement writer. The same applies to a destination shared concurrently with another flow or with external code, and the guidance is to create one wrapper and share it so every participant uses the same lock.

That is a real operational requirement, and it is the kind of detail that separates a library that has been used in anger from one that has not. A build tool that writes to a shared log from parallel tasks and does not serialise those writes produces interleaved output that is hard to read at best and a corrupted log at worst.

The parallelism guidance is to set `Task.Parallel` only when actions are safe to run concurrently, and to rely on `middleware.BufferParallel` to keep the output readable. And the pipeline guidance is to keep actions small and compose them with `Deps` rather than writing one large, monolithic task, which is the same advice as for a test suite.

For control flow, the advice is to prefer `*goyek.A` with `a.Log`, `a.Error`, `a.Fatal`, `a.Skip`, `a.Cleanup` and `a.Context` over manual log or os.Exit calls. For external commands, prefer `github.com/goyek/x/cmd.Exec` or a small helper rather than calling `os/exec` directly in actions. And for long-running resources, start them with `a.Context()` and release them in `a.Cleanup` callbacks, keeping in mind that the task context is canceled before cleanups run.

Reuse across repositories, and what the library gives up

The examples section shows how goyek is meant to grow beyond one repository, and the most interesting pattern there is the shared module.

The list is the goyek/template repository, a Go application repository template using goyek for build automation, the `build/` directory of this repository itself, which dogfoods goyek for its own build pipeline, splunk-otel-go, a multi-module repository using goyek, and curioswitch/go-build with projects such as curioswitch/tasuke, described as an example of extracting reusable build tasks into a shared module consumed by multiple repositories.

That last pattern is the strongest argument for the library approach. In a Makefile, a shared build rule means a shared include file, a Makefile fragment, or a copy. In goyek, a shared build rule is a shared Go package, and it can be imported like any other module. The best practices section says the same thing: reuse tasks across repositories by extracting common tasks into a shared module, as in curioswitch/go-build, and importing them from other projects to keep build logic consistent.

The wrapper scripts are worth a mention too, since they solve an ergonomics problem. Instead of running `go run.` inside `build`, you can use wrapper scripts, with `goyek.sh` for Bash and `goyek.ps1` for PowerShell. So the daily workflow is a familiar command, and the implementation is Go.

The comparison with the alternatives is now clear. Against Make, you gain type checking, refactoring and debugging, and you lose the declarative one-line-per-target readability that makes a Makefile easy to skim. Against Mage, which is also Go-based, the difference is that Mage is a framework you run as a binary in a magefile, while goyek is a library your own program imports, and the library version has no binary to install. Against Task, the YAML DSL, you lose the ability to have someone who does not write Go maintain the build definition, and you gain the ability to use any Go library inside a build step.

There is a cost in the module layout. The build code has to live in its own directory with its own go.mod, it has to change directory at startup, and the `go mod tidy` step becomes part of setting up. That is a structural change to the repository rather than a file you drop in, and it is the main reason the template repository exists.

The LICENSE file uses a custom licence that GitHub cannot classify, follows a Keep a Changelog format, and the module targets Go 1.16 in go.mod.

Editorial conclusion

Adopt goyek if your build logic is already Go, or is going to be, since you get type checking, IDE navigation and the standard debugger on your build steps rather than a separate DSL. Do not adopt it if you want a build description that a colleague can read without knowing Go, and check first how your build directory becomes a module, because the pattern depends on the build code living in its own directory with its own go.mod, which changes how the repository is laid out.

Frequently asked questions

What is goyek?

It is a Go library for task automation, intended as an alternative to Make, Mage and Task. It is library first rather than a standalone binary, so build logic is ordinary Go code you import, and the API is inspired by testing, cobra, flag and http.

How do I define and run a task with goyek?

Call goyek.Define with a Task carrying a name, usage and an action at package level so it registers during init, then call goyek.SetDefault in main and boot.Main() to run it. The build code lives in a build directory and changes to the repository root at startup with os.Chdir.

Which flags does the goyek runner provide?

-dry-run prints all tasks without executing actions, -long-run prints when a task takes longer than a duration with a one minute default, -no-color disables colorized output, -no-deps skips dependency processing, -skip takes comma-separated tasks to skip, and -v prints all tasks as they run.

How do I share build tasks between repositories with goyek?

Extract them into a shared Go module and import that module from the other repositories, as the curioswitch/go-build example does. The documentation also recommends using the goyek/template repository when starting a new project, and copying the relevant files for existing ones.

Is goyek safe to run tasks in parallel?

It is if you set Task.Parallel only for actions that are safe to run concurrently, use middleware.BufferParallel to keep the output readable, and serialise writes to a shared destination with goyek.SyncWriter. The documentation is explicit that Flow.Execute and Flow.Main serialise writes through the output they provide, and a destination shared with another flow or with external code needs one shared wrapper so every participant uses the same lock.

Is goyek still being maintained?

The repository is not archived and the last push was on 2026-10-15. Recent releases are v3.0.1 from 2025-12-09, v3.0.0 from 2025-11-25 and v2.3.0 from 2025-02-25, and the module is at github.com/goyek/goyek/v3 with go 1.16 in go.mod.

Official sources

  1. goyek/goyek on GitHub
  2. Issues
  3. Project website
  4. README
  5. Releases
Add this badge to your README

If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.

Add this badge to your README

markdown
[![Hysen Labs](https://hysenlabs.com/badge/goyek-goyek.svg)](https://hysenlabs.com/projects/goyek-goyek)

Was this useful?