Taskfiles for Go Developers

Elliot Forbes Elliot Forbes ⏰ 6 Minutes 📅 Jun 12, 2022

👋 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!