Masalah Klasik: Makefile yang Bikin Emosi
Kamu pernah nggak, nulis make build terus dapat error kayak gini:
Makefile:5: *** missing separator. Stop.
Terus kamu scroll ke baris 5, lihat baik-baik — dan nggak ada yang salah. Sampai kamu sadar: oh, kamu pakai spasi, bukan tab. Lagi. Untuk kesekian kalinya.
Makefile itu powerful, tapi didesain di era 1976. Syntax-nya tab-sensitive, variabel-nya punya scoping yang nggak intuitif, dan kalau kamu mau nulis logic conditional yang agak kompleks, kamu harus belajar bahasa shell yang embedded di dalam Makefile — yang kadang beda behavior-nya antara macOS dan Linux.
Buat solo engineer yang kerja di self-hosted dev environment, ini nyebelin banget. Kamu mau fokus ke kode, bukan debugging indentasi.
Masuk Taskfile — task runner modern yang pakai YAML, cross-platform, dan punya fitur yang Makefile nggak punya out of the box.
Kenapa Taskfile Lebih Enak Dipakai
Taskfile (dari project Task) bukan sekadar "Makefile tapi YAML". Ada beberapa hal konkret yang bikin gue personally switch:
1. Nggak ada tab hell. YAML pakai spasi, dan editor modern handle ini dengan baik.
2. Cross-platform tanpa drama. Task ditulis dalam Go dan punya built-in support buat Windows, macOS, Linux. Kalau kamu kolaborasi sama developer yang pakai Windows (atau kamu sendiri dual-boot), ini penting banget.
3. Dependency antar task jelas. Kamu bisa define task A harus jalan sebelum task B dengan syntax yang manusiawi.
4. .env file support bawaan. Nggak perlu export $(cat .env | xargs) yang sering error kalau ada komentar di .env-mu.
5. Watch mode. Taskfile bisa watch file changes dan re-run task otomatis — berguna banget waktu develop.
Instalasi Task
Install Task itu cepat. Pilih salah satu cara:
# macOS / Linux via Homebrew
brew install go-task
# Linux via script installer
sh -c "$(curl --location https://taskfile.dev/install.sh)" -- -d -b /usr/local/bin
# Atau kalau kamu pakai Go
go install github.com/go-task/task/v3/cmd/task@latest
Verifikasi instalasi:
task --version
# Task version: v3.x.x
Struktur Dasar Taskfile.yml
Buat file Taskfile.yml di root project kamu:
version: '3'
tasks:
default:
desc: "Tampilkan semua task yang tersedia"
cmds:
- task --list
build:
desc: "Build aplikasi"
cmds:
- go build -o bin/app ./cmd/main.go
run:
desc: "Jalankan aplikasi"
deps: [build]
cmds:
- ./bin/app
test:
desc: "Jalankan semua test"
cmds:
- go test ./... -v
clean:
desc: "Hapus build artifacts"
cmds:
- rm -rf bin/
Jalankan dengan:
task build
task run
task test
# Lihat semua task yang tersedia
task --list
Output task --list langsung rapi:
task: Available tasks for this project:
* build: Build aplikasi
* clean: Hapus build artifacts
* default: Tampilkan semua task yang tersedia
* run: Jalankan aplikasi
* test: Jalankan semua test
Contoh Real: Workflow Full-Stack Project
Ini Taskfile yang gue pakai buat project Next.js + Go API yang di-self-host di VPS:
version: '3'
dotenv: ['.env', '.env.local']
vars:
APP_NAME: myapp
GO_CMD: ./cmd/api/main.go
BINARY: bin/api
tasks:
default:
cmds:
- task --list
silent: true
# === Backend Tasks ===
api:build:
desc: "Build Go API binary"
cmds:
- mkdir -p bin
- go build -ldflags="-s -w" -o {{.BINARY}} {{.GO_CMD}}
sources:
- '**/*.go'
generates:
- '{{.BINARY}}'
api:run:
desc: "Jalankan API server (development)"
deps: [api:build]
cmds:
- '{{.BINARY}}'
env:
PORT: '{{.API_PORT | default "8080"}}'
DB_URL: '{{.DATABASE_URL}}'
api:watch:
desc: "Watch dan rebuild API saat ada perubahan"
watch: true
sources:
- '**/*.go'
cmds:
- task: api:run
api:test:
desc: "Jalankan test dengan coverage"
cmds:
- go test ./... -coverprofile=coverage.out
- go tool cover -html=coverage.out -o coverage.html
- echo "Coverage report: coverage.html"
# === Frontend Tasks ===
web:install:
desc: "Install npm dependencies"
dir: frontend
cmds:
- npm install
sources:
- frontend/package.json
generates:
- frontend/node_modules/.package-lock.json
web:dev:
desc: "Jalankan Next.js dev server"
dir: frontend
deps: [web:install]
cmds:
- npm run dev
web:build:
desc: "Build Next.js untuk production"
dir: frontend
deps: [web:install]
cmds:
- npm run build
# === Database Tasks ===
db:migrate:
desc: "Jalankan database migrations"
cmds:
- goose -dir migrations postgres "{{.DATABASE_URL}}" up
db:rollback:
desc: "Rollback migration terakhir"
cmds:
- goose -dir migrations postgres "{{.DATABASE_URL}}" down
db:seed:
desc: "Seed database dengan data awal"
cmds:
- go run ./cmd/seed/main.go
# === Deploy Tasks ===
deploy:staging:
desc: "Deploy ke staging server"
cmds:
- task: api:build
- task: web:build
- rsync -avz --delete bin/ {{.STAGING_USER}}@{{.STAGING_HOST}}:/opt/{{.APP_NAME}}/bin/
- rsync -avz --delete frontend/.next/ {{.STAGING_USER}}@{{.STAGING_HOST}}:/opt/{{.APP_NAME}}/web/
- ssh {{.STAGING_USER}}@{{.STAGING_HOST}} "sudo systemctl restart {{.APP_NAME}}"
# === Utility ===
lint:
desc: "Jalankan linter"
cmds:
- golangci-lint run ./...
- cd frontend && npm run lint
fmt:
desc: "Format semua kode"
cmds:
- gofmt -w .
- cd frontend && npm run format
Beberapa hal yang perlu diperhatiin di sini:
dotenvdi baris atas otomatis load.envdan.env.local— variabel langsung tersedia di semua task tanpa setup tambahan.sourcesdangeneratesbikin task jadi incremental: kalau source file nggak berubah sejak terakhir kali task jalan, task di-skip. Ini mirip behavior Makefile tapi lebih explicit.dirkey bikin task jalan di direktori tertentu — nggak perlucd frontend &&di setiap command.- Namespace dengan titik dua (
api:build,web:dev) bikin task terorganisir. Kamu bisa jalankan semua task dalam namespace dengantask api:*(fitur experimental).
Gotcha yang Gue Kena
1. sources pakai glob pattern, bukan regex.
Gue sempat nulis sources: ['*.go'] dan heran kenapa file di subdirektori nggak ke-detect. Yang bener adalah '**/*.go'.
2. Environment variable dari dotenv nggak di-inherit ke subprocess secara otomatis di beberapa kasus.
Kalau kamu punya script external yang di-call dari task, pastikan variable-nya di-pass explicitly:
my-task:
cmds:
- ./scripts/deploy.sh
env:
DATABASE_URL: '{{.DATABASE_URL}}'
3. deps jalan secara parallel by default.
Ini sebenarnya fitur, tapi bisa jadi masalah kalau task-mu ada dependency order yang implisit. Kalau mau sequential, pakai:
setup:
cmds:
- task: db:migrate
- task: db:seed
Bukan deps, tapi cmds dengan task: prefix.
4. Watch mode (watch: true) belum support semua OS dengan sempurna.
Di Linux, pastikan inotify-tools terinstall kalau ada issue. Di macOS biasanya langsung jalan.
5. Taskfile nggak automatically di-detect kalau namanya beda.
File harus bernama Taskfile.yml atau Taskfile.yaml (case-sensitive di Linux). Kalau kamu mau pakai nama lain, gunakan flag -t:
task -t MyCustomTaskfile.yml build
Integrasi dengan Self-Hosted Dev Environment
Kalau kamu pakai Gitea, Forgejo, atau self-hosted CI kayak Woodpecker CI atau Drone, Taskfile integrate dengan mulus karena cukup install binary task di runner.
Contoh pipeline Woodpecker CI:
# .woodpecker.yml
steps:
- name: test
image: golang:1.22
commands:
- curl -sL https://taskfile.dev/install.sh | sh -s -- -d -b /usr/local/bin
- task test
- name: build
image: golang:1.22
commands:
- task api:build
Satu binary, nggak ada dependency aneh. Cocok banget buat self-hosted setup yang kamu mau keep simple.
Migrasi dari Makefile
Kalau kamu punya Makefile yang udah ada dan mau migrate, prosesnya straightforward:
- Tiap
target:di Makefile jadi satutask:di Taskfile - Dependency di Makefile (
target: dep1 dep2) jadideps: [dep1, dep2] - Variable Makefile (
VAR := value) jadivars: VAR: value .PHONYtargets nggak perlu — semua task di Taskfile by default phony
Kamu bahkan bisa jalankan keduanya bersamaan selama transisi — Makefile dan Taskfile bisa coexist di satu repo.
Yang Gue Lakuin Sekarang
Semua project gue — dari side project kecil sampai yang di-deploy ke VPS — sekarang pakai Taskfile sebagai entry point standar. Workflow-nya simpel:
- Clone repo →
task setup(install deps, copy.env.exampleke.env) task devbuat developmenttask testsebelum committask deploy:stagingkalau mau push ke server
Kalau kamu baru mau coba, mulai dari project yang paling sering kamu sentuh. Buat Taskfile.yml dengan 3-4 task yang paling sering kamu jalankan manual, dan rasain sendiri bedanya.
Dokumentasi resminya ada di taskfile.dev — well-written dan ada banyak contoh. Bukan marketing fluff, literally reference yang berguna.
Satu hal lagi: commit Taskfile.yml ke repo. Ini jadi dokumentasi living buat project kamu — siapapun yang clone repo langsung tahu cara build, test, dan deploy tanpa baca README panjang.