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:
...