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 testharus jalan setelahmake build, kamu harus tulis manual dan rawan salah. - Windows? Lupakan.
makenggak 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
.envatauenv:section): diakses dengan$VAR_NAMEdi 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:
- Install
taskdengan command di atas. - Buka salah satu project yang punya
Makefileataupackage.jsonscripts yang berantakan. - Buat
Taskfile.ymldan pindahin satu-dua command dulu — misalnyadevdantest. - Coba
task --listdan 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.