Taskfiles for Go Developers
👋 Welcome Gophers! In this tutorial, I’m going to be demonstrating how you can utilize Taskfiles for fame and fortune within your Go application development.
(Disclaimer): This article was prompted by the adoption of Taskfiles within CircleCI to aid them in their adoption of Go. I can’t take any credit for discovering Taskfiles as it was another team that highlighted the advantages.
If you’ve been part of the Go community for a while, you may be familiar with the use of Makefiles or similar build tooling within Go applications, you may even have opted for some form of script to capture some of the longer commands for easier development.
The masochists among you may even have just committed all your regular commands to memory. How you can remember all the various build args and setups is beyond me but serious kudos to you for being able to remember.
But Taskfiles are without a doubt one of my new favourite approaches to capturing some of the more tedious commands I have to run as part of my day-to-day dev activites in one place.
A Sample Taskfile
Let’s take a quick look at the example Taskfile.yml
that is demonstrated on the homepage of
taskfile.dev -
version: '3'
tasks:
hello:
cmds:
- echo 'Hello World from Task!'
silent: true
At first glance, this looks incredibly similar to Makefiles, however, I have found the tool has a far nicer user experience and provides a rich layer of functionality over and above that of what the Makefile system had.
Installing The Task Tool
For mac developers, installation is a cinch using homebrew:
$ brew install go-task/tap/go-task
For Windows and *Nix users, you should be able to find instructions for whatever OS flavour you are running here: Taskfile Installation
Additional Context
With the task
tool installed, you are then able to run tasks defined within your Taskfile.yml
like so:
$ task hello
Hello World from Task!
Super intuitive right?
If you want to add more context to each of the tasks defined, you can add a desc:
block which then allows you to list commands like so:
version: '3'
tasks:
hello:
desc: "A Hello World Task"
cmds:
- echo 'Hello World from Task!'
silent: true
$ task --list
task: Available tasks for this project:
* hello: A Hello World Task
For larger, more complex projects, this can be incredibly handy if you wish to find out how to do things like spinning up the project locally etc.
Running In Subdirectories
In the past, I’ve found myself wanting to run specific commands within a specific sub-directory. With Make this is slightly problematic. You either have to chain commands together like: (cd terraform && make)
which adds complexity, or you have to pass in the directory within the make command with the -C
flag.
The approach Taskfiles take seems soo much simpler:
version: '3'
tasks:
hello:
desc: "A Hello World Task"
cmds:
- echo 'Hello World from Task!'
silent: true
terraform-apply:
desc: "A task demonstrating a subdir"
dir: terraform
cmds:
- pwd
silent: true
Now, whenever we attempt to run the terraform-apply
task, it will execute the command within the terraform
subdirectory which is really quite nice 👌
$ task terraform-apply
/path/to/myproject/terraform
Task Dependencies
Let’s say you have a task that can only be run after other tasks have been executed. This becomes trivial to define within your Taskfile.
Using deps: []
within our task definition, we can list out all of the dependencies we need for our task to run and they will be executed for us:
version: '3'
tasks:
hello:
desc: "A Hello World Task"
cmds:
- echo 'Hello World from Task!'
silent: true
terraform-plan:
desc: "A mock terraform plan"
dir: terraform
cmds:
- echo "Running terraform plan..."
silent: true
terraform-apply:
desc: "A mock terraform apply"
dir: terraform
deps: [terraform-plan]
cmds:
- echo "Running terraform apply..."
silent: true
Now, when we go to run this, we should see terraform-plan
being executed prior to terraform-apply
:
$ task terraform-apply
Running terraform plan...
Running terraform apply...
Once again, the user experience for writing these files compared to Make is far nicer in my opinion.
Dynamic Variables
There are situations where you need to be able to pass dynamic values into your tasks. With the vars
block, we can define specific variables and the scripts we need to fetch them like so:
version: '3'
tasks:
hello:
desc: "A Hello World Task"
cmds:
- echo 'Hello World from Task!'
silent: true
install:
desc: "An example of dynamic variables"
cmds:
- echo 'Installing tool into {{.HOMEDIR}}'
silent: true
vars:
HOMEDIR:
sh: echo $HOME/.mydir
terraform-plan:
desc: "A mock terraform plan"
dir: terraform
cmds:
- echo "Running terraform plan..."
silent: true
terraform-apply:
desc: "A mock terraform apply"
dir: terraform
deps: [terraform-plan]
cmds:
- echo "Running terraform apply..."
silent: true
Here, we’ve defined the install
task which has a dynamic variable HOMEDIR
. For more complex usecases, you could potentially curl an API to fetch some data, or execute some more advanced shell commands.
If we were to execute this command, you’ll see the correct HOMEDIR
value is interpolated into the executed command:
task install
Installing tool into /Users/elliotforbes/.mydir
A General Taskfile for Go Developers
Now that we’ve covered some of the advantages to taskfiles, it’s time to take a look at what a more life-like implementation of a Taskfile.yml
would look like within a Go app:
build:
desc: "build the compiled binary"
cmds:
- go build -o app cmd/server/main.go
test:
desc: "run all unit tests"
cmds:
- go test -v ./...
lint:
desc: "lint the code"
cmds:
- golangci-lint run
run:
desc: "runs our app and any dependencies defined within the docker-compose.yml"
cmds:
- docker-compose up --build
integration-test:
desc: "starts our app and then attempts to run our integration tests"
cmds:
- docker-compose up -d db
- go test -tags=integration -v ./...
env:
DB_USERNAME: postgres
DB_PASSWORD: postgres
DB_TABLE: postgres
DB_HOST: localhost
DB_PORT: 5432
DB_DB: postgres
SSL_MODE: disable
acceptance-tests:
desc: "starts our app and then attempts to run our e2e tests"
cmds:
- docker-compose up -d --build
- go test -tags=e2e -v ./...
This is just a lightweight example of some of the standard commands I include within my Go apps. As it tends to grow in complexity, this list gets longer.
The beauty of this approach also means it is far easier for me to run these same tasks within my CI pipelines. I simply need to ensure that the Taskfile tool is installed within my pipeline and then I can reuse these commands to construct my automation.
Conclusion
Well, hopefully this article has made you consider adopting Taskfiles for your own Go application development. They’ve certainly made my life simpler within my day job and I just cannot emphasize enough how much more I enjoy the user experience of Taskfiles over more traditional tooling.
If you have any questions or comments on this approach, I would love to hear them in the comments section below!