Taskfile: Ganti Makefile yang Bikin Pusing

by Marcus Chen
Taskfile: Ganti Makefile yang Bikin Pusing

Pernah nggak kamu nulis make deploy terus dapat error kayak gini?

Makefile:12: *** missing separator. Stop.

Kamu scroll ke baris 12, lihat baik-baik — dan ternyata salah satu baris pakai spasi, bukan tab. Satu karakter. Satu error yang makan 20 menit debugging.

Makefile itu powerful, tapi syntaxnya berasal dari tahun 1976. Tab-sensitive, shell-dependent, dan kalau kamu kerja di tim yang campuran Windows-Linux, siap-siap aja nangis. Gue udah terlalu sering ketemu masalah ini di project solo maupun tim kecil.

Nah, Taskfile (atau task) adalah tool yang gue pakai sekarang untuk semua automation script di project. YAML-based, cross-platform, dan punya fitur yang Makefile nggak punya — dependency antar task, dotenv support, bawaan templating variabel.


Kenapa Makefile Nyebelin untuk Project Modern

Makefile dibuat untuk build system C. Kalau kamu pakai buat shortcut command — make dev, make test, make docker-up — kamu basically menyalahgunakan tool yang salah.

Beberapa masalah konkret:

  • Tab vs spasi: satu typo, seluruh Makefile rusak.
  • Shell default berbeda: di macOS defaultnya /bin/sh, bukan bash. Beberapa syntax bash nggak jalan.
  • Nggak ada dependency task yang proper: kalau make test harus jalan setelah make build, kamu harus tulis manual dan rawan salah.
  • Windows? Lupakan. make nggak ada secara native di Windows tanpa install tambahan.
  • Variabel dan environment: passing env variable ke Makefile itu awkward.

Taskfile solve semua ini dengan cara yang lebih eksplisit dan readable.


Install Task

Install task itu satu command:

Linux / macOS (via shell script):

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

macOS via Homebrew:

brew install go-task

Windows via Scoop:

scoop install task

Via Go (kalau kamu udah punya Go):

go install github.com/go-task/task/v3/cmd/task@latest

Verifikasi:

task --version
# Task version: v3.x.x

Nggak ada runtime dependency lain. Satu binary, langsung jalan.


Struktur Dasar Taskfile

Buat file Taskfile.yml di root project kamu:

version: '3'

tasks:
  hello:
    desc: "Sapa dunia"
    cmds:
      - echo "Halo dari Taskfile!"

Jalankan:

task hello
# task: [hello] echo "Halo dari Taskfile!"
# Halo dari Taskfile!

Lihat semua task yang tersedia:

task --list

Output-nya rapi dengan deskripsi dari field desc. Ini udah lebih baik dari Makefile yang kamu harus baca source-nya dulu buat tahu apa yang ada.


Contoh Real: Project Go + Docker + PostgreSQL

Ini Taskfile.yml yang gue pakai di salah satu project API Go:

version: '3'

dotenv: ['.env']

vars:
  APP_NAME: myapi
  BINARY_DIR: ./bin

tasks:
  default:
    desc: "Tampilkan semua task"
    cmds:
      - task --list
    silent: true

  build:
    desc: "Build binary Go"
    cmds:
      - go build -o {{.BINARY_DIR}}/{{.APP_NAME}} ./cmd/api
    sources:
      - '**/*.go'
    generates:
      - '{{.BINARY_DIR}}/{{.APP_NAME}}'

  run:
    desc: "Jalankan server development"
    deps: [build]
    cmds:
      - '{{.BINARY_DIR}}/{{.APP_NAME}}'
    env:
      PORT: '8080'
      ENV: development

  test:
    desc: "Jalankan semua unit test"
    cmds:
      - go test ./... -v -race

  test:coverage:
    desc: "Test dengan coverage report"
    cmds:
      - go test ./... -coverprofile=coverage.out
      - go tool cover -html=coverage.out -o coverage.html
      - echo "Coverage report: coverage.html"

  lint:
    desc: "Jalankan golangci-lint"
    cmds:
      - golangci-lint run ./...

  db:up:
    desc: "Start PostgreSQL via Docker"
    cmds:
      - docker compose up -d postgres
      - task: db:wait

  db:wait:
    desc: "Tunggu PostgreSQL siap"
    internal: true
    cmds:
      - |
        echo "Menunggu PostgreSQL..."
        until docker compose exec postgres pg_isready -U $DB_USER; do
          sleep 1
        done
        echo "PostgreSQL siap!"

  db:migrate:
    desc: "Jalankan database migration"
    deps: [db:up]
    cmds:
      - migrate -path ./migrations -database "postgres://$DB_USER:$DB_PASS@localhost:5432/$DB_NAME?sslmode=disable" up

  db:reset:
    desc: "Reset database (DROP + migrate ulang)"
    prompt: "Ini akan hapus semua data. Lanjut?"
    cmds:
      - migrate -path ./migrations -database "postgres://$DB_USER:$DB_PASS@localhost:5432/$DB_NAME?sslmode=disable" drop -f
      - task: db:migrate

  docker:build:
    desc: "Build Docker image"
    cmds:
      - docker build -t {{.APP_NAME}}:latest .

  ci:
    desc: "Jalankan semua check (lint + test)"
    deps: [lint, test]

Beberapa hal yang perlu diperhatiin:

dotenv: ['.env'] — Taskfile otomatis load .env file. Variabel kayak $DB_USER, $DB_PASS, $DB_NAME langsung tersedia di semua task tanpa kamu harus export manual.

deps: [build] — Task run akan otomatis jalankan build dulu sebelum eksekusi. Dependency-nya explicit dan nggak perlu kamu manage manual.

sources dan generates — Ini fitur incremental build. Task build hanya akan re-run kalau ada file .go yang berubah. Kalau binary sudah up-to-date, task di-skip. Persis kayak behavior Makefile yang beneran, tapi tanpa syntax horror-nya.

internal: true — Task db:wait nggak akan muncul di task --list. Ini untuk helper task yang nggak perlu di-expose ke user.

prompt: — Task db:reset akan minta konfirmasi sebelum jalan. Built-in, nggak perlu read -p manual di shell script.


Gotcha yang Gue Temuin

1. Colon di nama task itu namespace, bukan literal

Kalau kamu nulis nama task db:migrate, itu bukan masalah — tapi di beberapa shell, kamu perlu quote kalau passing sebagai argument:

task db:migrate   # ini jalan
task "db:migrate" # ini juga jalan

Tapi kalau kamu include task ini sebagai deps, tulis sebagai string biasa:

deps:
  - db:migrate  # benar

2. Variabel di cmds pakai {{.VAR}}, bukan $VAR

Ini yang sering bikin bingung. Ada dua jenis variabel:

  • Taskfile vars (vars: section): diakses dengan {{.VAR_NAME}}
  • Environment vars (dari .env atau env: section): diakses dengan $VAR_NAME di shell

Contoh:

vars:
  APP_NAME: myapi  # akses dengan {{.APP_NAME}}

tasks:
  example:
    env:
      DB_HOST: localhost  # akses dengan $DB_HOST di shell
    cmds:
      - echo "App: {{.APP_NAME}}"
      - echo "DB: $DB_HOST"

3. Multi-line command pakai |

Kalau kamu mau jalankan beberapa baris shell dalam satu cmds entry, pakai YAML block scalar:

cmds:
  - |
    if [ ! -f .env ]; then
      cp .env.example .env
      echo ".env dibuat dari .env.example"
    fi

Tanpa |, setiap item di cmds dijalankan sebagai shell command terpisah — termasuk subshell yang berbeda. Jadi kalau kamu cd di satu command, directory-nya nggak persist ke command berikutnya.

4. Default task untuk onboarding

Gue selalu set task default yang jalankan task --list. Jadi kalau ada developer baru clone repo dan cuma ketik task, mereka langsung lihat semua yang tersedia. Kecil tapi sangat membantu.


Taskfile untuk Project Non-Go

Taskfile nggak terikat ke bahasa apapun. Ini contoh untuk project Node.js + Prisma:

version: '3'

dotenv: ['.env']

tasks:
  install:
    desc: "Install dependencies"
    cmds:
      - npm ci
    sources:
      - package-lock.json
    generates:
      - node_modules/.package-lock.json

  dev:
    desc: "Jalankan development server"
    cmds:
      - npm run dev

  build:
    desc: "Build untuk production"
    cmds:
      - npm run build

  db:generate:
    desc: "Generate Prisma client"
    cmds:
      - npx prisma generate
    sources:
      - prisma/schema.prisma

  db:push:
    desc: "Push schema ke database (dev)"
    cmds:
      - npx prisma db push

  db:studio:
    desc: "Buka Prisma Studio"
    cmds:
      - npx prisma studio

  format:
    desc: "Format kode dengan Prettier"
    cmds:
      - npx prettier --write .

  check:
    desc: "Typecheck + lint"
    cmds:
      - npx tsc --noEmit
      - npx eslint . --ext .ts,.tsx

Sama persis konsepnya, beda ecosystem.


Taskfile vs Makefile: Kapan Pakai Yang Mana?

Kalau project kamu sudah punya Makefile yang jalan dan tim kamu nyaman, nggak ada urgensi untuk migrasi. Tapi kalau kamu:

  • Start project baru
  • Kerja di environment campuran (Linux + macOS + Windows)
  • Butuh dotenv support yang proper
  • Sering frustrated dengan tab-vs-spasi
  • Mau dependency antar task yang eksplisit

...Taskfile adalah pilihan yang lebih masuk akal.

Satu hal yang Makefile masih lebih baik: integrasi dengan C/C++ build system. Kalau kamu compile C dan butuh proper file dependency tracking yang granular, Makefile memang dibuat untuk itu. Tapi untuk workflow automation sehari-hari di project web/backend/API, Taskfile menang telak.


Yang Gue Lakuin Sekarang

Semua project baru gue — baik itu Go API, Next.js app, atau script Python — sekarang mulai dengan Taskfile.yml di root. Gue commit file ini ke repo supaya siapapun yang clone langsung bisa jalankan task dan tahu harus ngapain.

Langkah konkret yang bisa kamu coba sekarang:

  1. Install task dengan command di atas.
  2. Buka salah satu project yang punya Makefile atau package.json scripts yang berantakan.
  3. Buat Taskfile.yml dan pindahin satu-dua command dulu — misalnya dev dan test.
  4. Coba task --list dan rasain bedanya.

Migrasi nggak harus sekaligus. Kamu bisa punya Taskfile dan Makefile dalam satu repo selama transisi — task nggak akan konflik dengan make.

Dokumentasi lengkapnya ada di taskfile.dev — well-written, ada contoh per use case, dan nggak butuh waktu lama buat dibaca.