A Task (YAML-based task runner) translation of the project’s Makefile.
Quick Start
# Install Task (https://taskfile.dev/installation/)
# e.g.: go install github.com/go-task/task/v3/cmd/task@latest
# Run from the repo root:
task --taskfile buildsys/task/Taskfile.yml build-prod-obverse
# Or add a thin root-level Taskfile.yml:
cat > Taskfile.yml <<'EOF'
version: '3'
includes:
default:
taskfile: buildsys/task/Taskfile.yml
dir: .
EOF
task build-prod-obverse
Available Tasks
Run task --taskfile buildsys/task/Taskfile.yml --list to see all public tasks.
| Task | Description |
|---|---|
build-dev-obverse |
Build dev site (no server) |
build-prod-obverse |
Build production site |
build-prod-reverse |
Build production mirror |
build-test-obverse |
Build test site |
build-test-reverse |
Build test mirror |
build-onion-obverse |
Build onion site |
build-onion-reverse |
Build onion mirror |
build-all |
Build all four production sites (parallel) |
deploy-prod-obverse |
Deploy to Netlify |
deploy-prod-reverse |
Deploy mirror to Netlify |
deploy-test-obverse |
Deploy to AWS (CFN + S3 + hedgerules) |
deploy-test-reverse |
Deploy mirror to AWS |
deploy-onion-obverse |
Deploy via rsync |
deploy-onion-reverse |
Deploy mirror via rsync |
deploy-all |
Deploy all four production sites |
deploy-s3 |
Deploy both test sites |
check-all |
Run all pre-build checks |
check-symlinks |
Check for symlinks in content/ |
check-exifstripped |
Verify/strip EXIF from images |
spellcheck-content |
Spell check content/ files |
dev |
Run Hugo dev servers (obverse + reverse) |
go-mod |
Update go modules |
deploy-clean |
Clean generated files |
install-hedgerules |
Install Hedgerules CLI |
me.micahrl.com-builder |
Build CI Docker container |
Internal tasks (prefixed _) are called by the above but not shown in --list.
Directory Handling
The Taskfile lives in buildsys/task/ but all tasks set dir: '{{.ROOT_DIR}}/../..' to
run with the repo root as their working directory. sources and generates globs are
evaluated relative to this dir, so content/**/* correctly matches repo-root content.
Task caches source checksums in a .task/ directory. When dir is set per-task, Task
places .task/ inside that directory (the repo root), so you’ll see a .task/ appear
at the repo root on first run. Add it to .gitignore or deploy-clean.
How This Compares to Make
What Works Well
1. Readable YAML syntax
Compared to Make’s cryptic $(eval $(shell ...)) and $$(cat ...) patterns, Task’s
YAML is approachable. The _hugobuild internal task replaces the define hugobuild
macro with clearer structure:
# Task: called with vars
- task: _hugobuild
vars: {ENV: prod-obverse}
# Make: macro expansion
$(call hugobuild,prod-obverse)
2. Content-hash tracking (method: checksum)
Task’s default sources tracking uses content checksums stored in .task/checksum/,
not file timestamps. This means:
- Checking out a branch and reverting won’t trigger a rebuild (timestamps change, content doesn’t).
touching a file without changing it won’t cause a rebuild.- Matches the behavior Shake uses, and is strictly better than Make’s timestamp approach.
3. Parallel deps by default
deps: tasks run concurrently. build-all’s four Hugo environments build simultaneously
without needing make -j. No -j flag required; it’s the default.
4. Lazy variable evaluation
Variables with sh: are evaluated only when the task runs, not on every task invocation.
DEVHOST (Tailscale lookup) only runs when dev is actually invoked. Compare to Make’s
DEVHOST = $(shell ...) which executes on every make call including make help.
5. internal: tasks
Tasks marked internal: true are hidden from --list and cannot be invoked directly
by users. This is cleaner than Make’s convention of prefixing with _ or not documenting
internal targets.
6. generates: dependency chain
When _cfn-test-obverse-base generates base.outputs.json, and _cfn-test-obverse-extract
lists it in sources:, Task enforces the ordering and skips the extract step when
base.outputs.json hasn’t changed. The dependency chain is explicit in the YAML, not
implicit in the Makefile’s target path names.
What Doesn’t Work Well / Trade-offs
1. No pattern rules (biggest limitation)
Make’s %.exifstripped: | % elegantly creates one rule per image file, with per-file
incremental tracking. Task has no equivalent. The translation uses a shell loop:
check-exifstripped:
cmds:
- for img in $(scripts/exif all); do scripts/exif smartstrip "$img"; done
This loses per-file tracking — it re-processes all images every time, even if only one changed. For this project (~hundreds of images), it’s tolerable. For projects with many expensive per-file operations, this gap matters.
2. deps: parallelism can be a footgun for deploys
deps: run in parallel, but deploy-test-obverse needs strictly sequential stages:
CFN base → Hugo upload → CFN distribution. Task’s deps: won’t help here; you must
encode the ordering explicitly via task-level deps: chains (each stage depends on the
prior one). The Makefile’s left-to-right dependency ordering is implicit but correct;
Task requires making it explicit.
Compare:
# Task: ordering enforced via deps chain
_cfn-test-obverse-extract:
deps: [_cfn-test-obverse-base] # must finish first
_cfn-test-obverse-distrib:
deps: [_cfn-test-obverse-extract] # must finish second
3. dir: boilerplate
Every task needs dir: '{{.ROOT_DIR}}/../..' because the Taskfile lives in a subdirectory.
This is 26 lines of boilerplate spread across the file. The alternative (a root-level
Taskfile.yml that includes this one) adds a file to the repo root that might feel wrong
for an “investigation” directory.
This would not be an issue if the Taskfile lived at the repo root.
4. deploy-test-obverse ordering caveat
The Makefile’s deploy-test-obverse target has:
deploy-test-obverse: public/test-obverse-cfn/base.outputs.json deploy-test-obverse-hugo public/test-obverse-cfn/distribution.outputs.json
Make processes these left-to-right (CFN base, then Hugo deploy, then CFN distribution).
In Task, deploy-test-obverse’s deps: run in parallel. The ordering is enforced
indirectly: deploy-test-obverse-hugo depends on _cfn-test-obverse-base (bucket must
exist), and _cfn-test-obverse-distrib depends on _cfn-test-obverse-extract (outputs
must be available). But this relies on the internal task DAG, not on the top-level target’s
dep list — which could surprise someone reading only deploy-test-obverse.
5. No make help equivalent without extra setup
Make’s ## comment pattern generates self-documenting help. Task has desc: fields and
task --list, which is actually better — but task --list shows the full task name, which
can be long. There’s no built-in column formatting like Make’s awk trick.
6. Spell check glob syntax
The Makefile’s --glob='public/prod-obverse{,/resume,/work,/contact}/index.html' passes a
brace-expansion glob to Vale. Task’s sources: uses Go’s doublestar glob library, which
doesn’t support brace expansion. This glob appears in a cmds: shell command (not in
sources:), so shell brace expansion handles it correctly. But it’s a subtle distinction —
mixing Task glob syntax with shell glob syntax requires attention.
Targets NOT Translated
macos-dev-*: macOS-specific launchctl/app generation. Not relevant to build system comparison.list-pagerefs,list-shortcodes: One-linerrgcommands. Just run them directly.build-all-docker: Trivial Docker wrapper; not interesting for comparison.
Structural Differences Summary
| Aspect | Make | Task |
|---|---|---|
| Syntax | Makefile DSL | YAML |
| File tracking | Timestamps | Checksums (default) or timestamps |
| Incremental tracking state | None (uses filesystem mtimes) | .task/checksum/ directory |
| Pattern rules | %.exifstripped: | % |
Not supported |
| Macros | define ... endef |
internal: tasks called with vars: |
| Parallel deps | make -j flag |
Default behavior of deps: |
| Variable evaluation | Parse-time (= is recursive, := is immediate) |
Lazy: sh: vars run only when task executes |
| Help | ## comment + awk trick |
desc: fields + task --list |
| Installation | Universal (pre-installed everywhere) | Must install separately |
| Config location | Repo root (Makefile) | Flexible (any dir, specify with --taskfile) |
Verdict
Task is a good fit for this project, and better than Make for day-to-day use. The key wins are:
- Content-hash tracking avoids the “touched file triggers rebuild” problem
- YAML is more readable than Make syntax for complex multi-stage deploys
- Internal tasks with vars cleanly replace Make macros
- Lazy variable evaluation means
task --listis fast (no Tailscale lookup)
The main losses are:
- No pattern rules (EXIF tracking loses per-file granularity)
dir:boilerplate if Taskfile doesn’t live at repo root- Must be installed (
brew install go-task,go install, etc.)
Of all the build systems investigated in this buildsys/ directory, Task and Shake are the
strongest alternatives to Make for this project. Task wins on approachability and
installation simplicity; Shake wins on expressive power and dynamic dependency handling.