Taskfile

Β· 917 words Β· 5 minute read

Introduction πŸ”—

According to the official documentation for Task

Task is a task runner / build tool that aims to be simpler and easier to use than, for example, GNU Make.

Having used both to build projects over the years, I strongly agree!

Task Quickstart πŸ”—

Installation πŸ”—

Task can be installed with the package manager of your choice.

See the Task Installation page for detailed instructions.

Overview πŸ”—

Create a new file at the root of your repository called taskfile.yaml:

version: '3'

# Define some environment variables to use in the tasks
env:
  # Env vars can be static strings
  VERSION_FILE: version.yaml
  # or defined by running a shell command
  TIMESTAMP:
    sh: date +%s
  SHORT_COMMIT_HASH:
    sh: git rev-parse --short HEAD
  # or multiple shell commands
  SEMVER:
    sh: |
      major="$(grep -i major {{.VERSION_FILE}} | awk '{print $2}')"
      minor="$(grep -i minor {{.VERSION_FILE}} | awk '{print $2}')"
      patch="$(grep -i patch {{.VERSION_FILE}} | awk '{print $2}')"
      # The value of the env var is set by writing to stdout
      echo "${major}.${minor}.${patch}"      

# Define some tasks that can be run by name
tasks:
  # If no task name is specified when running Task, the default task will be run
  default:
    # Env vars can be accessed as expected
    cmds:
      - echo "SEMVER=${SEMVER}"

  # You can run multiple commands like so
  hello1:
    cmds:
      - echo "hello"
      - echo "world"

  # or like so
  hello2:
    cmd: |
      echo "Hello"
      echo "World"      

  # Use 'set -x' to show which command was run
  hello3:
    cmd: |
      set -x
      echo "Hello"
      echo "World"      

  # Calling one task from another is easy
  build:
    cmds:
      - echo "build"
      - task: test
        # as is passing variables to a task
        vars:
          FILE: "my-test-file"

  test:
    # you can require that variables be set
    requires:
      vars: [FILE]
    # and access the value of a variable with using golang templating syntax
    cmds:
      - echo "test file={{.FILE}}"

Add a version.yaml file just for this example:

major: 0
minor: 1
patch: 0

Now you can run Task:

$ task
task: [default] echo "SEMVER=${SEMVER}"
SEMVER=0.1.0

$ task hello1
task: [hello1] echo "hello"
hello
task: [hello1] echo "world"
world

$ task hello2
task: [hello2] echo "Hello"
echo "World"

Hello
World

$ task hello3
task: [hello3] set -x
echo "Hello"
echo "World"

+ echo Hello
Hello
+ echo World
World

$ task build
task: [build] echo "build"
build
task: [test] echo "test file=my-test-file"
test file=my-test-file

The Task usage docs are excellent, so this post will not rehash what is already covered there. Instead, I want to talk about how to use Task to standardize builds across a number of repositories.

Build library πŸ”—

Suppose you have a large number of repositories, each of which contains the code and configuration files for a component that you want to build. You may be able to get away with a go build ... or docker build ... for a simple case, but what if your build process becomes more involved.

Perhaps you have a version.yaml file that contains the Major, Minor, and Patch numbers for your component and you want to name your binary <NAME>-<SEMVER> before uploading it to artifactory. Or maybe you want to add the short commit hash of your git repo as a build arg to your docker image for attestation purposes.

Whatever the case may be, you probably want to standardize that across all of your repositories. Now you are suddenly in need of a “build library” that all repositories can make use of. Fortunately Task has a mechanism to include other taskfiles.

Create a common-tasks repository where you keep all of your common taskfiles. If you check out all of your repositories in the same directory, like so:

git/
β”œβ”€ common-tasks/
β”‚  β”œβ”€ tasks/
β”‚  β”‚  β”œβ”€ common.yaml
β”‚  β”‚  β”œβ”€ docker.yaml
β”œβ”€ foo/
β”‚  β”œβ”€ taskfile.yaml
β”œβ”€ bar/
β”‚  β”œβ”€ taskfile.yaml

then the taskfile in each repository could include the taskfiles in the common-tasks repository, like so:

version: '3'

includes:
  common: ../common-tasks/tasks/common.yaml
  docker: ../common-tasks/tasks/docker.yaml

...

While this approach is doable, it’s not the easiest to manage. The biggest problem, is that you cannot pin the version of the common-tasks repository in each of the “client” repositories. That is, you cannot use common-tasks@v1 in foo/ and common-tasks@v2 in bar/, at least not without some serious workarounds. Not an ideal situation for a build system.

The solution to the above problem is to use git submodules to include the common-tasks repository in all other repositories.

Git submodules πŸ”—

Git submodules allow you to include code from one repository into another and to specify which version of the repository to include.

If the foo repository includes common-tasks@v1 and the bar repository includes common-tasks@v2, then the directory structure would look as follows:

git/
β”œβ”€ common-tasks/
β”‚  β”œβ”€ tasks/
β”‚  β”‚  β”œβ”€ common.yaml
β”‚  β”‚  β”œβ”€ docker.yaml
β”‚  β”‚  β”œβ”€ go.yaml
β”œβ”€ foo/
β”‚  β”œβ”€ taskfile.yaml
β”‚  β”œβ”€ common-tasks
β”‚  β”‚  β”œβ”€ tasks/
β”‚  β”‚  β”‚  β”œβ”€ common.yaml
β”‚  β”‚  β”‚  β”œβ”€ docker.yaml
β”œβ”€ bar/
β”‚  β”œβ”€ taskfile.yaml
β”‚  β”œβ”€ common-tasks
β”‚  β”‚  β”œβ”€ tasks/
β”‚  β”‚  β”‚  β”œβ”€ common.yaml
β”‚  β”‚  β”‚  β”œβ”€ docker.yaml
β”‚  β”‚  β”‚  β”œβ”€ go.yaml

To git the common-tasks sub-directories are of course special, but to the filesystem they are just another directory. This allows each taskfile to include and use the common tasks like so:

version: '3'

includes:
  common: common-tasks/tasks/common.yaml
  docker: common-tasks/tasks/docker.yaml

env:
  PROJECT_NAME: <project-name-goes-here>

tasks:
  default:
    # Run the local build and test tasks
    cmds:
      - task: build
      - task: test

  build:
    # Run the build task in the docker taskfile
    cmds:
      - task: docker:build

  test:
    cmds:
      ...