feat(templater): report template deprecations through the verbose logger

sprout logs its deprecation notices to stdout by default, which would
corrupt the JSON and group output styles. Route them to the Executor's
logger at verbose level instead, deduplicated so that a single deprecated
call is not reported once per compilation pass.
This commit is contained in:
Valentin Maerten
2026-08-10 21:11:49 +02:00
parent 3d2fb43227
commit b0c792f09e
3 changed files with 62 additions and 1 deletions

View File

@@ -63,7 +63,7 @@ var legacySprigAliases = sprout.FunctionAliasMap{
func init() {
handler := sprout.New(
sprout.WithLogger(slog.New(slog.DiscardHandler)),
sprout.WithLogger(slog.New(noticeHandler{})),
sprout.WithRegistries(
taskfuncs.NewRegistry(),
backward.NewRegistry(),

View File

@@ -0,0 +1,57 @@
package templater
import (
"context"
"log/slog"
"sync"
"sync/atomic"
)
// NoticeFunc receives the deprecation notices that sprout emits when a
// template calls a deprecated function name or uses a deprecated argument
// order.
type NoticeFunc func(format string, args ...any)
// noticeSink is set once the Executor has built its logger. Until then — and
// the function map is built in an init(), long before that — notices are
// dropped rather than written to sprout's default stdout handler, which would
// corrupt the JSON and group output styles.
var noticeSink atomic.Pointer[NoticeFunc]
// SetNoticeSink installs the destination for template deprecation notices.
// Passing nil silences them again.
func SetNoticeSink(fn NoticeFunc) {
noticeSeen.Clear()
if fn == nil {
noticeSink.Store(nil)
return
}
noticeSink.Store(&fn)
}
type noticeHandler struct{}
func (noticeHandler) Enabled(_ context.Context, level slog.Level) bool {
return level >= slog.LevelWarn && noticeSink.Load() != nil
}
// noticeSeen keeps each distinct notice to a single line. Task renders the
// same templates several times per run — once per compilation pass — so
// without this a lone deprecated call would be reported over and over.
var noticeSeen sync.Map
func (noticeHandler) Handle(_ context.Context, record slog.Record) error {
fn := noticeSink.Load()
if fn == nil {
return nil
}
if _, dup := noticeSeen.LoadOrStore(record.Message, struct{}{}); dup {
return nil
}
(*fn)("task: %s\n", record.Message)
return nil
}
func (h noticeHandler) WithAttrs([]slog.Attr) slog.Handler { return h }
func (h noticeHandler) WithGroup(string) slog.Handler { return h }

View File

@@ -18,6 +18,7 @@ import (
"github.com/go-task/task/v3/internal/filepathext"
"github.com/go-task/task/v3/internal/logger"
"github.com/go-task/task/v3/internal/output"
"github.com/go-task/task/v3/internal/templater"
"github.com/go-task/task/v3/internal/version"
"github.com/go-task/task/v3/taskfile"
"github.com/go-task/task/v3/taskfile/ast"
@@ -192,6 +193,9 @@ func (e *Executor) setupLogger() {
AssumeYes: e.AssumeYes,
AssumeTerm: e.AssumeTerm,
}
templater.SetNoticeSink(func(format string, args ...any) {
e.Logger.VerboseErrf(logger.Yellow, format, args...)
})
}
func (e *Executor) setupOutput() error {