Direct answer
Grunt does not provide a documented architectural pattern for atomic file writes or transactional retries across plugin tasks. The runner executes tasks synchronously in a configured sequence, but it offers no rollback, no per-task retry, and no guarantee that a failed file operation leaves the filesystem unchanged. The closest practical equivalent is to enforce idempotency at the pipeline level: clean outputs first, regenerate from source, and stage multi-file writes so a failure never leaves a partially swapped directory.
Confirmed facts vs. likely explanation
Confirmed: task failure aborts the queue unless --force is used, and any partial state written by the failed task remains on disk. There is no core API that wraps multiple tasks in a transaction.
Likely explanation: Grunt's plugin contract expects tasks to mutate files directly. The runner passes file descriptors to tasks and waits for the done() callback; it does not mediate the writes themselves. Because the runner never owns the write operation, it cannot make it atomic.
Enforcing idempotency without custom logic in every task
You cannot make arbitrary plugins atomic from the outside, but you can make the overall build deterministic so that a retry converges to the same state:
- Clean before regenerate. Run
grunt-contrib-clean (or equivalent) as the first step of every build, including retries. This resets the environment so a failed run does not poison the next one. Cleaning is not transactional, but it removes the duplicate-write problem by ensuring outputs are always regenerated from source. - Stage writes you control. For custom tasks, write to a temporary file on the same filesystem and then
fs.renameSync() over the target. On POSIX and Windows same-volume filesystems, rename is atomic: readers see either the old file or the new one, never a partial write.
const fs = require('fs');
function atomicWrite(target, data) {
const tmp = target + '.tmp-' + process.pid;
fs.writeFileSync(tmp, data);
fs.renameSync(tmp, target); // atomic on same filesystem
}
- Wrap opaque plugins. If a plugin writes in place and offers no temp-output option, call it from a custom task that redirects its destination to a staging directory, then perform the atomic swap after the plugin completes. Do not assume a plugin is atomic unless its source or documentation confirms it uses an atomic rename.
- Avoid
--force during recovery. Forcing past a failure hides the error and leaves the build in an inconsistent state. Retry only after a clean reset.
Verification
To verify the pattern, run a custom atomic-write task twice and confirm the target file is either fully written or unchanged. Then inject a failure in the middle of a multi-task pipeline, rerun with a preceding clean task, and verify that no duplicate or partial files remain.
One diagnostic that changes the advice
Do the specific file-writing plugins in your pipeline expose a temporary or staging output option? If they do, you can configure them directly and skip the wrapper tasks. If they do not, the wrapper-and-swap pattern above is the only reliable way to prevent partial or duplicate data.