Task vs Make - Final Thoughts
Tags: tutorial, automation, tooling
Published: 2023-08-09 22:29:10

So, after spending time moving from _Make_ to _Task_, I decided. I'm going with _Task_ in my future projects. _Make_ will not be removed from my projects immediately, but I will not use it in the future. Let me tell you why.

All this is my personal opinion. I'm not saying that _Task_ is better than _Make_. I think that _Task_ is better for me.

### Global Taskfile

I have a global _Task_ in my home directory for standard stuff. It contains tasks to _update my tools_, _run some diagnostics_, etc. Because it is global, it is accessible from any directory. For example:

```yaml
version: '3'

tasks:
  build:
    dir: '{{.USER_WORKING_DIR}}'
    cmds:
      - go build . -o app
```

In the above example, `task -g build` will build the Golang app in my current working directory. I do not need to add _Task_ for every project. I must add _Taskfile_ to the project if I share it. But, as long it is experimental, there is no need. Anyway, storing everyday tasks as global is a nice feature.

The `{{.USER_WORKING_DIR}}` is a variable set by _Task_. It is the current working directory. And the `dir` slug defines the folder where the task will be executed. In this case, it is the current working directory. But you can set it to any folder you want. So you do not need to switch directories to run a task. Just set the `dir` slug to the folder where you want to run the job. Neat!

### Including/Grouping Task Files

The explicit inclusion of other _Taskfiles_ with namespace is really neat. This way, you get a context of tasks. For example:

```yaml
version: '3'

includes:
  tools: ./Taskfile.tools.yml
  docs: ./docs/
```

Assuming I have the task `install_tools` in the file `Taskfile.tools.yml`, I can run it with `task tools:install_tools`. Also, suppose I have `Taskfile.yml` file in the `docs` directory. I can run it with `task docs:build_docs` from the root directory. This way, I'm groping tasks by context.

In case you have a CI pipeline and need to do builds for different platforms. You can split your tasks into different files with the platform postfix. For example, you have different tasks for `linux` and `windows` platforms. You can split them into separate files. I name them `Taskfile_linux.yml` and `Taskfile_windows.yml`. Then you can include them in the main `Taskfile.yml` file with the following:

```yaml
version: '3'

includes:
  build: ./Taskfile_{{OS}}.yml
```

If you run `task build:build` on the `linux` platform, it will include `Taskfile_linux.yml` and run the `build` task from it. If you run the same task on `windows` platform, it will consist of `Taskfile_windows.yml` and run the `build` job from it. This way, you can have different tasks for different platforms.

### Task Dependencies and Skipping Done Tasks

Task dependencies are really nice. You can define a task that depends on other tasks. For example:

```yaml
version: '3'

tasks:
 manifests:
    desc: Generate manifests e.g. CRD, RBAC etc.
    deps:
      - task: tools:controller-gen
    cmds:
      - controller-gen rbac:roleName=manager-role crd webhook paths="./..." output:crd:artifacts:config=config/crd/bases
    sources:
      - main.go
      - apis/**/*.go
      - pkg/**/*.go
    generates:
      - config/crd/bases/**/*.yaml
```

The `manifests` task in the above example depends on the `tools:controller-gen` task. Dependency tasks are run in parallel. This way, you can define tasks that rely on other tasks.

Also, the above example shows how to define sources and generated files. The `sources` slug defines files used to generate other files. The `generates` slug defines files that are generated by the task. This way, you can tell _Task_ which files are used to generate other files. It is to determine whether the task needs to be run. _Task_ will generate a fingerprint for sources and compare it next time the task
needs to be run. The task is considered done if the fingerprint is not changed, assuming the generated files are present.

Also, there is an alternative with a `status` slug. You can define a command that will return `0` if the task is done. For example:


```yaml
  install-cert-manager:
    desc: Install cert-manager    
    deps:
      - init-cluster
    cmds:
      - kubectl apply -f https://github.com/cert-manager/cert-manager/releases/download/{{.CERT_MANAGER_VERSION}}/cert-manager.yaml
      - echo "Waiting for cert-manager to be ready" && sleep 25
    status:
      - kubectl -n cert-manager get pods | grep Running | wc -l | grep -q 3
```

### Golang Template Engine

_Task_ uses the Golang template engine. As a Golang user, this is a big plus. I can use all the features of the Golang template engine plus one that _Task_ provides, like including functions from `https://go-task.github.io/slim-sprig/`. For example:

```yaml
version: '3'

tasks:
 print-date:
    cmds:
      - echo {{now | date "2006-01-02"}}
      - echo '{{OS}} {{ARCH}}'
```

### Format Is YAML

I like it when a file has a structure and text editors can understand it. I can understand it as well. It is enough said.

## Conclusion

There are other things that I like about _Task_ but I will not go into details. I have covered the most important ones, at least for me.

While evaluating _Makefile_ and _Task_, I discovered another tool called [_Just_](https://just.systems/man/en/chapter_1.html). It is more similar to _Make_ than _Task_. For that reason, I did not want to spend a lot of time on it. But I see it as a good alternative to _Make_. Especially if you do not like YAML.

Let me know what you think. Thanks for reading. Enjoy!

## References

* [Taskfile](https://taskfile.dev/)
* [Taskfile Github](https://github.com/go-task/task)