Ran into a build.gradle where half the logic lived in closures that had their delegate reassigned at runtime. Took a while to work out what anything meant.
Groovy makes this easy. A closure resolves unqualified names against its owner first by default, and a DSL flips that by setting resolveStrategy to Closure.DELEGATE_FIRST. Fine for a documented configuration block. Miserable when a helper in another file swaps the delegate and a bare version quietly starts meaning something else.
Build scripts get read far more than they get written, mostly by someone who came to bump one dependency. So my rule is boring. Declarative blocks in the script. Anything with a loop or a condition goes into a plugin class with a name and a test. No metaClass tricks in the build, ever.
The usual pushback is that it's more files. Yes. It's also the only way the next reader finds out what version refers to without stepping through the build in a debugger.