Taskfile as Makefile Alternative

Taskfile is a modern task runner that uses YAML syntax to define build and automation tasks, offering a clean alternative to Makefile with better cross-platform support and readability. With features like task dependencies, variables, dotenv support, and CI/CD integration, Taskfile simplifies project automation on Linux without the pitfalls of Makefile tab-sensitive syntax.

Prerequisites

  • Linux (Ubuntu/Debian or CentOS/Rocky)
  • Basic shell knowledge
  • A project that needs task automation

Installing Task

# Install via the official install script (Linux)
sh -c "$(curl --location https://taskfile.dev/install.sh)" -- -d -b /usr/local/bin

# Verify installation
task --version

# Ubuntu/Debian via snap
sudo snap install task --classic

# Using Go (if Go is installed)
go install github.com/go-task/task/v3/cmd/task@latest

# CentOS/Rocky - download binary directly
curl -sL https://github.com/go-task/task/releases/latest/download/task_linux_amd64.tar.gz | \
  tar -xz -C /usr/local/bin task

Basic Taskfile Structure

Create a Taskfile.yml in your project root:

# Taskfile.yml
version: '3'

tasks:
  default:
    desc: Show available tasks
    cmds:
      - task --list

  build:
    desc: Build the application
    cmds:
      - echo "Building..."
      - go build -o bin/app ./cmd/app

  test:
    desc: Run all tests
    cmds:
      - go test ./... -v -cover

  clean:
    desc: Remove build artifacts
    cmds:
      - rm -rf bin/
      - rm -f coverage.out

Run tasks:

# List all available tasks
task --list
task -l

# Run a specific task
task build

# Run default task
task

# Run multiple tasks
task clean build test

# Dry run (show commands without executing)
task --dry build

Task Dependencies and Ordering

Define task dependencies to ensure proper execution order:

version: '3'

tasks:
  build:
    desc: Build the application
    deps: [generate, lint]  # Run these first (in parallel)
    cmds:
      - go build -o bin/app ./cmd/app

  generate:
    desc: Run code generation
    cmds:
      - go generate ./...

  lint:
    desc: Run linter
    cmds:
      - golangci-lint run

  deploy:
    desc: Build and deploy
    deps:
      - task: test
      - task: build
    cmds:
      - ./scripts/deploy.sh

  # Sequential dependencies (not parallel)
  release:
    desc: Full release pipeline
    cmds:
      - task: clean
      - task: test
      - task: build
      - task: push-image

Control parallelism:

tasks:
  test-all:
    desc: Run tests in parallel
    deps:
      - task: test-unit
      - task: test-integration
      - task: test-e2e

  # Force sequential with run: once
  setup:
    deps:
      - task: install-deps
    cmds:
      - task: configure-db
      - task: run-migrations

Variables and Environment

version: '3'

vars:
  APP_NAME: myapp
  VERSION:
    sh: git describe --tags --always  # Dynamic variable from command

env:
  GO_ENV: production
  LOG_LEVEL: info

tasks:
  build:
    desc: Build with version info
    vars:
      BUILD_DATE:
        sh: date -u +"%Y-%m-%dT%H:%M:%SZ"
    cmds:
      - |
        go build \
          -ldflags "-X main.Version={{.VERSION}} -X main.BuildDate={{.BUILD_DATE}}" \
          -o bin/{{.APP_NAME}} ./cmd/app

  tag:
    desc: Create a git tag
    vars:
      TAG: '{{.TAG | default "v0.0.1"}}'
    cmds:
      - git tag -a {{.TAG}} -m "Release {{.TAG}}"
      - git push origin {{.TAG}}

Using .env Files

version: '3'

dotenv: ['.env', '.env.local']

tasks:
  start:
    desc: Start with environment variables
    cmds:
      - echo "Using DB: $DATABASE_URL"
      - ./bin/app serve

Pass variables at runtime:

# Override a variable
task build VERSION=v1.2.3

# Set environment variable
task start LOG_LEVEL=debug

# Multiple variables
task deploy APP_NAME=myapp VERSION=v1.0.0 ENV=prod

Conditionals and Guards

Use preconditions to validate before running:

version: '3'

tasks:
  deploy:
    desc: Deploy to production
    preconditions:
      - sh: test -f bin/app
        msg: "Binary not found. Run 'task build' first"
      - sh: git diff --quiet
        msg: "Uncommitted changes detected. Commit before deploying"
      - sh: '[ "{{.ENV}}" = "production" ]'
        msg: "ENV must be set to 'production'"
    cmds:
      - ./scripts/deploy.sh

  # Run only if file doesn't exist (caching)
  generate-proto:
    desc: Generate protobuf files
    sources:
      - proto/**/*.proto
    generates:
      - gen/**/*.go
    cmds:
      - protoc --go_out=gen ./proto/*.proto

Use status to skip tasks when already done:

tasks:
  install-tools:
    desc: Install required tools
    status:
      - which golangci-lint
      - which protoc
    cmds:
      - go install github.com/golangci/golangci-lint/cmd/golangci-lint@latest
      - apt-get install -y protobuf-compiler

Including External Taskfiles

Split large Taskfiles into modular includes:

# Taskfile.yml
version: '3'

includes:
  docker:
    taskfile: ./taskfiles/docker.yml
    dir: .
  db:
    taskfile: ./taskfiles/db.yml
  deploy:
    taskfile: ./taskfiles/deploy.yml
    optional: true  # Don't fail if file doesn't exist

tasks:
  setup:
    desc: Full project setup
    cmds:
      - task: docker:build
      - task: db:migrate
# taskfiles/docker.yml
version: '3'

vars:
  IMAGE: myapp

tasks:
  build:
    desc: Build Docker image
    cmds:
      - docker build -t {{.IMAGE}}:latest .

  push:
    desc: Push to registry
    cmds:
      - docker push {{.IMAGE}}:latest

Run namespaced tasks:

# Run docker:build task
task docker:build

# Run db:migrate task
task db:migrate

CI/CD Integration

GitHub Actions

# .github/workflows/ci.yml
name: CI

on: [push, pull_request]

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Install Task
        uses: arduino/setup-task@v2
        with:
          version: 3.x

      - name: Run CI pipeline
        run: task ci

  # Taskfile.yml ci task:
  # ci:
  #   cmds:
  #     - task: lint
  #     - task: test
  #     - task: build

GitLab CI

# .gitlab-ci.yml
before_script:
  - sh -c "$(curl --location https://taskfile.dev/install.sh)" -- -d -b /usr/local/bin

test:
  script:
    - task test

build:
  script:
    - task build

Troubleshooting

Task not found:

# Check Taskfile is in current directory or specify path
task --taskfile /path/to/Taskfile.yml list

# Check YAML syntax
python3 -c "import yaml; yaml.safe_load(open('Taskfile.yml'))" && echo "Valid YAML"

Variable not expanding:

# Variables use {{.VAR}} syntax, not $VAR in task definitions
# For shell variables in cmds, use $VAR as normal
# Debug variable values
task --verbose build

Dependency loop detected:

# Task detects circular dependencies automatically
# Review your deps: sections for circular references
# Use task --dry to trace execution order
task --dry deploy

Command runs every time despite generates/sources:

# Ensure sources and generates paths are correct
# Task uses file modification times for caching
# Force run ignoring status
task --force generate-proto

Conclusion

Taskfile provides a readable, YAML-based alternative to Makefiles that handles cross-platform compatibility, parallel task execution, and variable management elegantly. Its support for includes, dotenv files, and preconditions makes it well-suited for complex projects. For teams already comfortable with YAML-based CI/CD pipelines, Taskfile integrates naturally and reduces the friction of shell-based project automation.