- A task that overwrites its own sources is now called out. When a task's
commands write files its
sources:glob also matches, the checksum changes mid-run, so the task can never be cached. Task now prints a warning naming those files and skips publishing the task's fingerprint and cache entry; previously this was logged only under--verbose. - Cache save and push failures are now reported. A cache entry that cannot
be archived, written, or pushed prints a warning instead of being logged only
under
--verbose. An unreachable registry warns once per host; other failures warn per task.
- Included tasks share saved fingerprints when their build inputs match, even across separate runs without a build cache. Variables that change commands or declared environment values keep separate entries; namespaced task references still distinguish copies. Checksum values keep the Go fork's format, but filenames differ, so upgrading rebuilds each task once.
- A cache transfer no longer fails on one dropped connection to the registry. A pull or push request whose connection could not be made, timed out, or was cut short under the response is now made again a few times, seconds apart, before the cache entry is given up on. A registry answer — a miss, a refusal, a digest that does not match — is still final at once.
- Cache archives carry the whole generated tree. Collecting a
generatesglob for the cache now includes hidden entries —node_modules/.binand every other dot file or directory under**/*— and stores a symlink to a directory as the link instead of following it into its target, so a restorednode_modulesruns and a Yarn workspace keeps its links. Fingerprint checksums still see what the shell lists, so.taskstate is unaffected. - Failed cache imports invalidate saved task status. After an import fails, tasks rebuild instead of trusting partially restored outputs. If saved status cannot be removed, Task reports that cleanup failure too.
run: oncetasks run once across direct and nested includes. When one Taskfile is included both directly and through another include — the root includeslib, and alsoapp, which itself includeslib— arun: oncetask in it now runs once for both paths instead of once per path. Go Task v3 treats the nested copy as a separate task.- A task included under several namespaces shares one cache entry.
vk-registry entries, the build-once lock and the archive's task annotation
are keyed by the task's name in the Taskfile that defines it, so the copy
reached through a nested include restores what the directly included copy
built instead of rebuilding it. Tasks that differ only in their
vars, or in thevarstheir include passes, keep separate entries. Every vk-registry tag changes — the include namespace andlabelno longer appear in it — and an archive saved by a namespaced or labelled task is rebuilt once, so the first build on this version repopulates the cache..taskfingerprints are unaffected.
- A Linux release stays rebuildable for good. The pinned build environment
is now one locked package set whose sources stay fetchable indefinitely,
replacing hand-pinned Alpine packages that eventually disappear. Rebuilding a
release still needs only Docker and
./build.sh; thebuild-info.txtshipped with each Linux release now records aflake lock:digest in place ofapk pins:.
- vk-registry caches need only an address and an API key. A cache model
can name a vk-registry repository —
vk: registry.example/task-cache— and Task derives the cache entries and the build-once lock from it; an optionalnamespacekeeps entries built by different toolchains apart, and avkthat renders empty turns the cache off. The key comes fromapi_keyorTASK_VK_API_KEY;api_keyalso works on an explicitoci://cache orvk://lock, where it outranks any other credential. - One vk-registry API key authenticates an explicit
oci://cache andvk://lock.TASK_VK_API_KEYis sent as a bearer token tooci://caches.vk://locks without URL credentials useTASK_VK_LOCK_TOKEN, then fall back to that key or theTASK_CACHE_OCI_USER/TASK_CACHE_OCI_PASSWORDpair. - Authenticate transparent-zstd capability probes. The
oci://cache now sends registry credentials when negotiating transparent-zstd uploads, so credential-gated vk-registry servers use that mode instead of compressed-digest uploads. - A
vk://lock is released when its task finishes. It used to stay held until its lease expired, making a peer waiting on the same build wait up to 30 s longer than needed. - An
oci://cache miss is quiet. A tag the registry does not have was reported as a registry error in verbose output on every miss. - A
vk://lock names its holder's machine on the registry even when the shell did not exportHOSTNAME, as under a CI job.
- Match files with trailing
**globs —target/**now matches the same files astarget/**/*, restoring Go Task and bashglobstarcompatibility forsources:,generates:, andexclude:patterns. These patterns previously matched nothing. - Exclude build directories without scanning them — an
exclude:such astarget/**/*skips matching trees, keeping large excludes fast without changing the matched file set.
task --helplists the flags — it now prints the usage line, every option with its description, and the hint to runtask --list-all, instead of a short blurb that pointed back at--helpfor the flag list.
- Linux release binaries are reproducible — each release includes a
task-linux-<arch>.build-info.txtmanifest for verifying a local rebuild. See reproducible builds.
defaultis a filter, not a backwards-looking call —--migratenow converts both Go spellings,{{ .X | default "y" }}and{{ default "y" .X }}, to{{ X | default("y", true) }}. That second argument makes Jinja's own filter mean what sprig'sdefaultmeans — substituting for any empty value, not only an unset one — so a migrated Taskfile renders what it always did while reading in the natural order. A Go{{ default "y" }}with no value at all now rendersy, as sprig does, instead of failing.- Breaking: the sprig-ordered
default(fallback, value)function is gone. A Taskfile written or migrated against 4.1.0 / 4.1.1 that calls it now fails withunknown function; write the value first and add, true—X | default("y", true). truncandregexReplaceAllwork as filters —s | trunc(n)ands | regexReplaceAll(pattern, repl), so a Jinja Taskfile no longer has to spell either as a sprig-ordered call. The functions keep working as before, and a Go Taskfile's{{ .P | trunc 3 }}, which used to fail to render, now works and migrates to{{ P | trunc(3) }}.- A file's template dialect now covers the
vars:it passes to anincludes:entry and thecaches:models it defines. A tree partway through--migrateworks: the migrated file no longer fails on its own include vars, and a not-yet-migrated task no longer fails on the cache URL it inherits. A file that declares one dialect but writes those in the other used to be read as Go regardless, and now errors — runtask --migrateon it.
task --updatereplaces the binary with a published release — the latest, or--update=<version>for a specific one. It asks before touching anything (--yesskips that), checks the download against thesha256published beside it, and runs the new binary to confirm it works here before putting it in place.task --update --checkonly reports what is available, exiting1when a newer release exists. A task namedupdatestill runs as it always did.install-task.shinstalls again — every run used to fail before it downloaded the archive.- A failing cache registry or cache lock names the reason — a rejected certificate, a refused connection, a name that does not resolve — instead of only reporting that the registry could not be reached.
- A
vks://lock trusts the same certificate as the cache, so one registry behind a private CA serves both the cache and the build-once lock. Such a lock could not connect before, and every run took a local lock instead. - Go-syntax Taskfiles can call
printfandprint, so a Taskfile that builds a path out of several variables renders and migrates instead of being rejected as an unsupported construct.printfcovers the string-composing verbs (%s,%v,%q,%d,%%, with flags and a width); another verb, a precision, an argument its verb cannot render, or a mismatched argument count is an error. - An unsupported Go construct is reported as the
{{ … }}that used it — with the line it is on when migrating a whole Taskfile — instead of quoting the string it came from, which under--migratewas the whole file.
- Quoted strings in Go-syntax templates keep their dots, so
{{ .FILE | replace ".tar.gz" "" }}strips what it was given. Affected tasks re-run once, and a Taskfile already converted bytask --migrate --writeneeds checking by hand. - An escaped quote works inside a Go-syntax template string. A Taskfile
already converted by
task --migrate --writeneeds checking by hand. - Go-syntax templates can call the string helpers directly, not only after
a pipe, so
{{trimSuffix ".po" .ITEM}}works.splitListwas splitting the wrong argument. Affected tasks re-run once. default,title,join,firstandlastmean the same on either side of a pipe in Go-syntax Taskfiles — most visibly,defaultsubstitutes for any empty value, not only an unset one. Jinja Taskfiles keep Jinja's meaning. Affected tasks re-run once, and a Taskfile already converted needs checking by hand.- The Go-template deprecation warning no longer runs into the next line.
- A dependency cycle is reported as one, naming the path (
a -> b -> a), instead of running until the process ran out of stack. A task that reaches itself is rejected: calling itself with differentvars:still works, but recursion driven by state outside the Taskfile no longer does. - Deep dependency trees no longer crash the runner. Thousands of levels run; a runaway recursion stops with an error instead.
- Tasks start in a different order, so interleaved output can differ.
--output groupandprefixedstill keep each task's output together. - A failing dependency no longer stops its siblings starting. Under
--failfastthey all start and are cancelled when one fails, so a cancelled sibling may leave partial work behind. - A failing run stops the commands it leaves behind instead of orphaning
them. It cannot tell those from a job a task backgrounded on purpose, so both
are stopped;
TASK_NO_REAP=1turns that off. - Ctrl-C reaches the commands, not just the runner. The second press is passed on to them, and the third stops them before exiting.
- A confirmation prompt no longer pauses the rest of the run, and Ctrl-C works while one is waiting.
- A
SIGTERMstops the run at once, exiting1— it comes from a supervisor, so there is no second signal to wait for. Ctrl-C keeps its three-press escalation, and watch sessions are unchanged.
Task is now a Rust program. v4.0.0 is a full rewrite that aims to be drop-in compatible.
- Local deduplicated cache. A content-defined-chunking, zstd-compressed,
content-addressed cache (the
ocicascrate) backs--export-cache/--import-cacheand the OCI cache backend, so shared build outputs are stored and transferred once. - Native Jinja templating (opt-in), with migration. Alongside the existing
Go
text/templatesyntax, a Taskfile can opt into native minijinja withtemplater: jinja, unlocking{% for %},{% if %}, filters, and function-call syntax. The dialect is auto-detected per file;task --migrateconverts a Go-syntax Taskfile to Jinja (preview by default,--writeto apply). - Go template syntax is deprecated. Files that still use it get a one-time
warning pointing at
task --migrate. Go rendering will be removed in a future release; migration will remain. Suppress the warning withTASK_NO_GO_DEPRECATION=1. - vk-registry distributed lock.
cache.lock: vk://host/<prefix>takes the build-once lock over the registry's own/lockAPI, so one vk-registry serves both theoci://cache and the lock with no separate Redis. Credentials come from the URL (Basic) or$TASK_VK_LOCK_TOKEN(bearer); the lease is renewed by a heartbeat and expires 30 s after a holder goes away. - Single static binary. The release build links no system C libraries (musl-static, rustls+ring) and ships as one file.
- A duplicate task key in a Taskfile is now an error instead of silently taking the last definition.
- Task env/vars take precedence over the process environment by default. A
task-defined
env/varsvalue overrides one already present in the inherited environment; setTASK_X_ENV_PRECEDENCE=0to restore the old order where the process environment wins. - Remote (HTTP) Taskfiles are not supported.
- Storing cache contents in Redis (
cache.url: redis://) is not supported. (The file and OCI cache backends, and the Redis and vk-registry distributed locks —cache.lock: redis://andvk://— all work.) - Syntax highlighting in error snippets.