mirror of
https://gitea.com/gitea/runner.git
synced 2026-11-13 19:33:41 +00:00
refactor(logging): add a logging abstraction and move the job logger onto it (#1295)
Add `act/common/logging`: a printf-style `Logger` on a slog-shaped `Handler`, with a logrus-backed handler as the only backend, plus a `logtest` handler for tests. Move the job logger onto it: `common.Logger` returns `*logging.Logger`, the job log formatters and the `Reporter` hook become handlers, and composite step IDs travel in the context. Logging is tightly coupled to logrus types (hooks, formatters, `Entry.Data`). Going through our own interface first lets the remaining call sites move over with unchanged behavior, and lets the backend be swapped for `log/slog` in one place afterwards. The global process logger and its masking formatter stay on logrus for now. Assisted-by: Codet Co-authored-by: bircni <[email protected]> Reviewed-on: https://gitea.com/gitea/runner/pulls/1295 Reviewed-by: bircni <[email protected]>
This commit is contained in:
1 parent
ad9eeb668a
commit
de961b91eb
50 files changed
+1241
-438
No files matched your search
@@ -15,7 +15,6 @@ import (
|
|||||||
"encoding/json/v2"
|
"encoding/json/v2"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
|
||||||
"net"
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/http/httputil"
|
"net/http/httputil"
|
||||||
@@ -30,10 +29,10 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"gitea.com/gitea/runner/act/common"
|
"gitea.com/gitea/runner/act/common"
|
||||||
|
"gitea.com/gitea/runner/act/common/logging"
|
||||||
"gitea.com/gitea/runner/internal/pkg/disk"
|
"gitea.com/gitea/runner/internal/pkg/disk"
|
||||||
|
|
||||||
"github.com/julienschmidt/httprouter"
|
"github.com/julienschmidt/httprouter"
|
||||||
"github.com/sirupsen/logrus"
|
|
||||||
"github.com/timshannon/bolthold"
|
"github.com/timshannon/bolthold"
|
||||||
"go.etcd.io/bbolt"
|
"go.etcd.io/bbolt"
|
||||||
)
|
)
|
||||||
@@ -85,7 +84,7 @@ type Handler struct {
|
|||||||
listener net.Listener
|
listener net.Listener
|
||||||
port int
|
port int
|
||||||
server *http.Server
|
server *http.Server
|
||||||
logger logrus.FieldLogger
|
logger *logging.Logger
|
||||||
|
|
||||||
gcing atomic.Bool
|
gcing atomic.Bool
|
||||||
gcAt time.Time
|
gcAt time.Time
|
||||||
@@ -133,7 +132,7 @@ type Options struct {
|
|||||||
InternalSecret string
|
InternalSecret string
|
||||||
|
|
||||||
Policy Policy
|
Policy Policy
|
||||||
Logger logrus.FieldLogger
|
Logger *logging.Logger
|
||||||
}
|
}
|
||||||
|
|
||||||
// StartHandler starts an embedded cache or forwards cache requests to Upstream.
|
// StartHandler starts an embedded cache or forwards cache requests to Upstream.
|
||||||
@@ -147,11 +146,9 @@ func StartHandler(opts Options) (*Handler, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if logger == nil {
|
if logger == nil {
|
||||||
discard := logrus.New()
|
logger = logging.New(logging.DiscardHandler)
|
||||||
discard.Out = io.Discard
|
|
||||||
logger = discard
|
|
||||||
}
|
}
|
||||||
logger = logger.WithField("module", "artifactcache")
|
logger = logger.With("module", "artifactcache")
|
||||||
h.logger = logger
|
h.logger = logger
|
||||||
|
|
||||||
if opts.OutboundIP != "" {
|
if opts.OutboundIP != "" {
|
||||||
|
|||||||
@@ -230,7 +230,8 @@ func Serve(ctx context.Context, artifactPath, addr, port string) context.CancelF
|
|||||||
go func() {
|
go func() {
|
||||||
logger.Infof("Start server on http://%s", server.Addr)
|
logger.Infof("Start server on http://%s", server.Addr)
|
||||||
if err := server.ListenAndServe(); err != nil && err != http.ErrServerClosed {
|
if err := server.ListenAndServe(); err != nil && err != http.ErrServerClosed {
|
||||||
logger.Fatal(err)
|
logger.Error(err)
|
||||||
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|
||||||
|
|||||||
@@ -8,7 +8,8 @@ import (
|
|||||||
"errors"
|
"errors"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/sirupsen/logrus"
|
"gitea.com/gitea/runner/act/common/logging"
|
||||||
|
"gitea.com/gitea/runner/act/common/logging/logtest"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestDryrunContext(t *testing.T) {
|
func TestDryrunContext(t *testing.T) {
|
||||||
@@ -40,34 +41,27 @@ func TestJobErrorContainer(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestLoggerAndHookContext(t *testing.T) {
|
func TestLoggerAndHandlerContext(t *testing.T) {
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
if Logger(ctx) != logrus.StandardLogger() {
|
if Logger(ctx) != logging.Default() {
|
||||||
t.Fatal("plain context should use standard logger")
|
t.Fatal("plain context should use the default logger")
|
||||||
}
|
}
|
||||||
if LoggerHook(ctx) != nil {
|
if LoggerHandler(ctx) != nil {
|
||||||
t.Fatal("plain context should not have a logger hook")
|
t.Fatal("plain context should not have a logger handler")
|
||||||
|
}
|
||||||
|
if StepIDs(ctx) != nil {
|
||||||
|
t.Fatal("plain context should not have step IDs")
|
||||||
}
|
}
|
||||||
|
|
||||||
logger := logrus.New()
|
logger := logging.New(logtest.New())
|
||||||
ctx = WithLogger(ctx, logger)
|
ctx = WithLogger(ctx, logger)
|
||||||
if Logger(ctx) != logger {
|
if Logger(ctx) != logger {
|
||||||
t.Fatal("WithLogger should set logger")
|
t.Fatal("WithLogger should set logger")
|
||||||
}
|
}
|
||||||
|
|
||||||
hook := testHook{}
|
handler := logtest.New()
|
||||||
ctx = WithLoggerHook(ctx, hook)
|
ctx = WithLoggerHandler(ctx, handler)
|
||||||
if LoggerHook(ctx) != hook {
|
if LoggerHandler(ctx) != handler {
|
||||||
t.Fatal("WithLoggerHook should set hook")
|
t.Fatal("WithLoggerHandler should set handler")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
type testHook struct{}
|
|
||||||
|
|
||||||
func (testHook) Levels() []logrus.Level {
|
|
||||||
return logrus.AllLevels
|
|
||||||
}
|
|
||||||
|
|
||||||
func (testHook) Fire(*logrus.Entry) error {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
@@ -23,9 +23,8 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"gitea.com/gitea/runner/act/common"
|
"gitea.com/gitea/runner/act/common"
|
||||||
|
"gitea.com/gitea/runner/act/common/logging"
|
||||||
"gitea.com/gitea/runner/internal/pkg/lock"
|
"gitea.com/gitea/runner/internal/pkg/lock"
|
||||||
|
|
||||||
log "github.com/sirupsen/logrus"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
@@ -189,7 +188,7 @@ type NewGitCloneExecutorInput struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// CloneIfRequired reports whether an existing local clone was reused.
|
// CloneIfRequired reports whether an existing local clone was reused.
|
||||||
func CloneIfRequired(ctx context.Context, input NewGitCloneExecutorInput, logger log.FieldLogger) (bool, error) {
|
func CloneIfRequired(ctx context.Context, input NewGitCloneExecutorInput, logger *logging.Logger) (bool, error) {
|
||||||
var origin string
|
var origin string
|
||||||
_, err := os.Stat(filepath.Join(input.Dir, ".git")) // without it git would discover a repository around Dir
|
_, err := os.Stat(filepath.Join(input.Dir, ".git")) // without it git would discover a repository around Dir
|
||||||
if err == nil {
|
if err == nil {
|
||||||
|
|||||||
@@ -22,10 +22,10 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"gitea.com/gitea/runner/act/common"
|
"gitea.com/gitea/runner/act/common"
|
||||||
|
"gitea.com/gitea/runner/act/common/logging"
|
||||||
|
"gitea.com/gitea/runner/act/common/logging/logtest"
|
||||||
"gitea.com/gitea/runner/internal/pkg/lock"
|
"gitea.com/gitea/runner/internal/pkg/lock"
|
||||||
|
|
||||||
log "github.com/sirupsen/logrus"
|
|
||||||
logrustest "github.com/sirupsen/logrus/hooks/test"
|
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
)
|
)
|
||||||
@@ -318,7 +318,7 @@ func TestGitCloneExecutorReclonesWhenOriginURLChanges(t *testing.T) {
|
|||||||
|
|
||||||
cancelled, cancel := context.WithCancel(t.Context())
|
cancelled, cancel := context.WithCancel(t.Context())
|
||||||
cancel()
|
cancel()
|
||||||
_, err := CloneIfRequired(cancelled, NewGitCloneExecutorInput{URL: oldRemoteDir, Ref: "main", Dir: cacheDir}, log.New())
|
_, err := CloneIfRequired(cancelled, NewGitCloneExecutorInput{URL: oldRemoteDir, Ref: "main", Dir: cacheDir}, logging.New(logtest.New()))
|
||||||
require.ErrorIs(t, err, context.Canceled)
|
require.ErrorIs(t, err, context.Canceled)
|
||||||
require.FileExists(t, markerPath)
|
require.FileExists(t, markerPath)
|
||||||
|
|
||||||
@@ -428,9 +428,9 @@ func TestGitCloneExecutorQuietDemotesCloneLine(t *testing.T) {
|
|||||||
// Quiet callers report the download themselves, so the clone line must not reach the job log.
|
// Quiet callers report the download themselves, so the clone line must not reach the job log.
|
||||||
for name, quiet := range map[string]bool{"quiet": true, "not quiet": false} {
|
for name, quiet := range map[string]bool{"quiet": true, "not quiet": false} {
|
||||||
t.Run(name, func(t *testing.T) {
|
t.Run(name, func(t *testing.T) {
|
||||||
logger, hook := logrustest.NewNullLogger()
|
hook := logtest.New()
|
||||||
logger.SetLevel(log.InfoLevel)
|
logger := logging.New(logging.NewLevelHandler(hook, logging.LevelInfo))
|
||||||
ctx := common.WithLogger(context.Background(), logger.WithField("job", "j1"))
|
ctx := common.WithLogger(context.Background(), logger.With("job", "j1"))
|
||||||
|
|
||||||
require.NoError(t, NewGitCloneExecutor(NewGitCloneExecutorInput{
|
require.NoError(t, NewGitCloneExecutor(NewGitCloneExecutorInput{
|
||||||
URL: remoteDir,
|
URL: remoteDir,
|
||||||
@@ -440,7 +440,7 @@ func TestGitCloneExecutorQuietDemotesCloneLine(t *testing.T) {
|
|||||||
})(ctx))
|
})(ctx))
|
||||||
|
|
||||||
var cloneLines int
|
var cloneLines int
|
||||||
for _, entry := range hook.AllEntries() {
|
for _, entry := range hook.AllRecords() {
|
||||||
if strings.HasPrefix(entry.Message, "git clone ") {
|
if strings.HasPrefix(entry.Message, "git clone ") {
|
||||||
cloneLines++
|
cloneLines++
|
||||||
}
|
}
|
||||||
|
|||||||
+30
-23
@@ -7,46 +7,53 @@ package common
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
|
||||||
"github.com/sirupsen/logrus"
|
"gitea.com/gitea/runner/act/common/logging"
|
||||||
)
|
)
|
||||||
|
|
||||||
type loggerContextKey string
|
type loggerContextKey string
|
||||||
|
|
||||||
const loggerContextKeyVal = loggerContextKey("logrus.FieldLogger")
|
const loggerContextKeyVal = loggerContextKey("logging.Logger")
|
||||||
|
|
||||||
// Logger returns the appropriate logger for current context
|
// Logger returns the appropriate logger for current context
|
||||||
func Logger(ctx context.Context) logrus.FieldLogger {
|
func Logger(ctx context.Context) *logging.Logger {
|
||||||
val := ctx.Value(loggerContextKeyVal)
|
if logger, ok := ctx.Value(loggerContextKeyVal).(*logging.Logger); ok {
|
||||||
if val != nil {
|
return logger
|
||||||
if logger, ok := val.(logrus.FieldLogger); ok {
|
|
||||||
return logger
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return logrus.StandardLogger()
|
return logging.Default()
|
||||||
}
|
}
|
||||||
|
|
||||||
// WithLogger adds a value to the context for the logger
|
// WithLogger adds a value to the context for the logger
|
||||||
func WithLogger(ctx context.Context, logger logrus.FieldLogger) context.Context {
|
func WithLogger(ctx context.Context, logger *logging.Logger) context.Context {
|
||||||
return context.WithValue(ctx, loggerContextKeyVal, logger)
|
return context.WithValue(ctx, loggerContextKeyVal, logger)
|
||||||
}
|
}
|
||||||
|
|
||||||
type loggerHookKey string
|
type loggerHandlerKey string
|
||||||
|
|
||||||
const loggerHookKeyVal = loggerHookKey("logrus.Hook")
|
const loggerHandlerKeyVal = loggerHandlerKey("logging.Handler")
|
||||||
|
|
||||||
// LoggerHook returns the appropriate logger hook for current context
|
// LoggerHandler returns the extra handler the job logger sends its records to, or nil.
|
||||||
// the hook affects job logger, not global logger
|
// It affects the job logger, not the global logger.
|
||||||
func LoggerHook(ctx context.Context) logrus.Hook {
|
func LoggerHandler(ctx context.Context) logging.Handler {
|
||||||
val := ctx.Value(loggerHookKeyVal)
|
if h, ok := ctx.Value(loggerHandlerKeyVal).(logging.Handler); ok {
|
||||||
if val != nil {
|
return h
|
||||||
if hook, ok := val.(logrus.Hook); ok {
|
|
||||||
return hook
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// WithLoggerHook adds a value to the context for the logger hook
|
// WithLoggerHandler adds the extra handler for the job logger to the context
|
||||||
func WithLoggerHook(ctx context.Context, hook logrus.Hook) context.Context {
|
func WithLoggerHandler(ctx context.Context, h logging.Handler) context.Context {
|
||||||
return context.WithValue(ctx, loggerHookKeyVal, hook)
|
return context.WithValue(ctx, loggerHandlerKeyVal, h)
|
||||||
|
}
|
||||||
|
|
||||||
|
type stepIDsContextKey struct{}
|
||||||
|
|
||||||
|
// StepIDs returns the IDs of the step and the composite steps enclosing the current one, outermost first.
|
||||||
|
func StepIDs(ctx context.Context) []string {
|
||||||
|
ids, _ := ctx.Value(stepIDsContextKey{}).([]string)
|
||||||
|
return ids
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithStepIDs replaces the step IDs in the context
|
||||||
|
func WithStepIDs(ctx context.Context, ids []string) context.Context {
|
||||||
|
return context.WithValue(ctx, stepIDsContextKey{}, ids)
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
// Copyright 2026 The Gitea Authors. All rights reserved.
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
package logging
|
||||||
|
|
||||||
|
import "github.com/sirupsen/logrus"
|
||||||
|
|
||||||
|
var std = New(NewLogrusHandler(logrus.StandardLogger()))
|
||||||
|
|
||||||
|
// Default returns the process-wide Logger.
|
||||||
|
func Default() *Logger { return std }
|
||||||
|
|
||||||
|
// SetLevel sets the minimum level of the process-wide Logger.
|
||||||
|
func SetLevel(level Level) { logrus.SetLevel(toLogrusLevel(level)) }
|
||||||
|
|
||||||
|
// GetLevel returns the minimum level of the process-wide Logger.
|
||||||
|
func GetLevel() Level { return fromLogrusLevel(logrus.GetLevel()) }
|
||||||
|
|
||||||
|
func Enabled(level Level) bool { return std.Enabled(level) }
|
||||||
|
|
||||||
|
func With(args ...any) *Logger { return std.With(args...) }
|
||||||
|
func WithError(err error) *Logger { return std.WithError(err) }
|
||||||
|
func Tracef(format string, args ...any) { std.logf(LevelTrace, format, args) }
|
||||||
|
func Debugf(format string, args ...any) { std.logf(LevelDebug, format, args) }
|
||||||
|
func Infof(format string, args ...any) { std.logf(LevelInfo, format, args) }
|
||||||
|
func Warnf(format string, args ...any) { std.logf(LevelWarn, format, args) }
|
||||||
|
func Errorf(format string, args ...any) { std.logf(LevelError, format, args) }
|
||||||
|
func Trace(args ...any) { std.log(LevelTrace, args) }
|
||||||
|
func Debug(args ...any) { std.log(LevelDebug, args) }
|
||||||
|
func Info(args ...any) { std.log(LevelInfo, args) }
|
||||||
|
func Warn(args ...any) { std.log(LevelWarn, args) }
|
||||||
|
func Error(args ...any) { std.log(LevelError, args) }
|
||||||
@@ -0,0 +1,114 @@
|
|||||||
|
// Copyright 2026 The Gitea Authors. All rights reserved.
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
package logging
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"slices"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Attr is a structured key/value pair attached to a log record.
|
||||||
|
type Attr struct {
|
||||||
|
Key string
|
||||||
|
Value any
|
||||||
|
}
|
||||||
|
|
||||||
|
// Record is one log event. Attrs holds the call-site attributes only, the ones added with
|
||||||
|
// Logger.With are accumulated by the Handler.
|
||||||
|
type Record struct {
|
||||||
|
Time time.Time
|
||||||
|
Level Level
|
||||||
|
Message string
|
||||||
|
PC uintptr // program counter of the logging call, 0 if unknown
|
||||||
|
Attrs []Attr
|
||||||
|
}
|
||||||
|
|
||||||
|
// Attr returns the value of the last attribute named key.
|
||||||
|
func (r Record) Attr(key string) (any, bool) {
|
||||||
|
for _, a := range slices.Backward(r.Attrs) {
|
||||||
|
if a.Key == key {
|
||||||
|
return a.Value, true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil, false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handler consumes records, and is shaped like slog.Handler so that a slog backend can replace
|
||||||
|
// the current one without touching callers. Handle must not modify the Attrs slice it is given.
|
||||||
|
type Handler interface {
|
||||||
|
Enabled(ctx context.Context, level Level) bool
|
||||||
|
Handle(ctx context.Context, r Record) error
|
||||||
|
// WithAttrs returns a Handler that adds attrs to every record it handles.
|
||||||
|
WithAttrs(attrs []Attr) Handler
|
||||||
|
}
|
||||||
|
|
||||||
|
type multiHandler []Handler
|
||||||
|
|
||||||
|
// NewMultiHandler fans every record out to all handlers that have its level enabled.
|
||||||
|
func NewMultiHandler(handlers ...Handler) Handler {
|
||||||
|
return multiHandler(handlers)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m multiHandler) Enabled(ctx context.Context, level Level) bool {
|
||||||
|
for _, h := range m {
|
||||||
|
if h.Enabled(ctx, level) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m multiHandler) Handle(ctx context.Context, r Record) error {
|
||||||
|
var errs []error
|
||||||
|
for _, h := range m {
|
||||||
|
if h.Enabled(ctx, r.Level) {
|
||||||
|
errs = append(errs, h.Handle(ctx, r))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return errors.Join(errs...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m multiHandler) WithAttrs(attrs []Attr) Handler {
|
||||||
|
out := make(multiHandler, len(m))
|
||||||
|
for i, h := range m {
|
||||||
|
out[i] = h.WithAttrs(attrs)
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
type discardHandler struct{}
|
||||||
|
|
||||||
|
// DiscardHandler drops every record.
|
||||||
|
var DiscardHandler Handler = discardHandler{}
|
||||||
|
|
||||||
|
func (discardHandler) Enabled(context.Context, Level) bool { return false }
|
||||||
|
func (discardHandler) Handle(context.Context, Record) error { return nil }
|
||||||
|
func (d discardHandler) WithAttrs([]Attr) Handler { return d }
|
||||||
|
|
||||||
|
type levelHandler struct {
|
||||||
|
h Handler
|
||||||
|
minLevel Level
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewLevelHandler passes only records at or above minLevel on to h.
|
||||||
|
func NewLevelHandler(h Handler, minLevel Level) Handler {
|
||||||
|
return levelHandler{h: h, minLevel: minLevel}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l levelHandler) Enabled(ctx context.Context, level Level) bool {
|
||||||
|
return level >= l.minLevel && l.h.Enabled(ctx, level)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l levelHandler) Handle(ctx context.Context, r Record) error {
|
||||||
|
if r.Level < l.minLevel {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return l.h.Handle(ctx, r)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l levelHandler) WithAttrs(attrs []Attr) Handler {
|
||||||
|
return levelHandler{h: l.h.WithAttrs(attrs), minLevel: l.minLevel}
|
||||||
|
}
|
||||||
@@ -0,0 +1,53 @@
|
|||||||
|
// Copyright 2026 The Gitea Authors. All rights reserved.
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
package logging
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Level is a log severity. The values match log/slog, with Trace added below Debug.
|
||||||
|
type Level int
|
||||||
|
|
||||||
|
const (
|
||||||
|
LevelTrace Level = -8
|
||||||
|
LevelDebug Level = -4
|
||||||
|
LevelInfo Level = 0
|
||||||
|
LevelWarn Level = 4
|
||||||
|
LevelError Level = 8
|
||||||
|
)
|
||||||
|
|
||||||
|
func (l Level) String() string {
|
||||||
|
switch {
|
||||||
|
case l <= LevelTrace:
|
||||||
|
return "trace"
|
||||||
|
case l <= LevelDebug:
|
||||||
|
return "debug"
|
||||||
|
case l <= LevelInfo:
|
||||||
|
return "info"
|
||||||
|
case l <= LevelWarn:
|
||||||
|
return "warn"
|
||||||
|
default:
|
||||||
|
return "error"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ParseLevel accepts every name logrus does. "fatal" and "panic" map to LevelError because
|
||||||
|
// the Logger has no level above it.
|
||||||
|
func ParseLevel(s string) (Level, error) {
|
||||||
|
switch strings.ToLower(s) {
|
||||||
|
case "trace":
|
||||||
|
return LevelTrace, nil
|
||||||
|
case "debug":
|
||||||
|
return LevelDebug, nil
|
||||||
|
case "info":
|
||||||
|
return LevelInfo, nil
|
||||||
|
case "warn", "warning":
|
||||||
|
return LevelWarn, nil
|
||||||
|
case "error", "fatal", "panic":
|
||||||
|
return LevelError, nil
|
||||||
|
}
|
||||||
|
return LevelInfo, fmt.Errorf("not a valid log level: %q", s)
|
||||||
|
}
|
||||||
@@ -0,0 +1,106 @@
|
|||||||
|
// Copyright 2026 The Gitea Authors. All rights reserved.
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
package logging
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"runtime"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
const badKey = "!BADKEY"
|
||||||
|
|
||||||
|
// Logger writes printf-style messages with structured attributes to a Handler.
|
||||||
|
type Logger struct {
|
||||||
|
h Handler
|
||||||
|
ctx context.Context
|
||||||
|
}
|
||||||
|
|
||||||
|
func New(h Handler) *Logger {
|
||||||
|
return &Logger{h: h, ctx: context.Background()}
|
||||||
|
}
|
||||||
|
|
||||||
|
// With returns a Logger that adds the key/value pairs to every record, like slog.Logger.With.
|
||||||
|
func (l *Logger) With(args ...any) *Logger {
|
||||||
|
if len(args) == 0 {
|
||||||
|
return l
|
||||||
|
}
|
||||||
|
return &Logger{h: l.h.WithAttrs(argsToAttrs(args)), ctx: l.ctx}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *Logger) WithError(err error) *Logger {
|
||||||
|
return l.With("error", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithContext returns a Logger that passes ctx to its Handler.
|
||||||
|
func (l *Logger) WithContext(ctx context.Context) *Logger {
|
||||||
|
return &Logger{h: l.h, ctx: ctx}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *Logger) Enabled(level Level) bool {
|
||||||
|
return l.h.Enabled(l.ctx, level)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *Logger) Tracef(format string, args ...any) { l.logf(LevelTrace, format, args) }
|
||||||
|
func (l *Logger) Debugf(format string, args ...any) { l.logf(LevelDebug, format, args) }
|
||||||
|
func (l *Logger) Infof(format string, args ...any) { l.logf(LevelInfo, format, args) }
|
||||||
|
func (l *Logger) Warnf(format string, args ...any) { l.logf(LevelWarn, format, args) }
|
||||||
|
func (l *Logger) Errorf(format string, args ...any) { l.logf(LevelError, format, args) }
|
||||||
|
|
||||||
|
// Trace and the methods below format their arguments like fmt.Sprint.
|
||||||
|
func (l *Logger) Trace(args ...any) { l.log(LevelTrace, args) }
|
||||||
|
func (l *Logger) Debug(args ...any) { l.log(LevelDebug, args) }
|
||||||
|
func (l *Logger) Info(args ...any) { l.log(LevelInfo, args) }
|
||||||
|
func (l *Logger) Warn(args ...any) { l.log(LevelWarn, args) }
|
||||||
|
func (l *Logger) Error(args ...any) { l.log(LevelError, args) }
|
||||||
|
|
||||||
|
func (l *Logger) logf(level Level, format string, args []any) {
|
||||||
|
if !l.Enabled(level) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
l.emit(callerPC(4), level, fmt.Sprintf(format, args...))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *Logger) log(level Level, args []any) {
|
||||||
|
if !l.Enabled(level) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
l.emit(callerPC(4), level, fmt.Sprint(args...))
|
||||||
|
}
|
||||||
|
|
||||||
|
// emit drops the Handler error, as a logger has nowhere to report it.
|
||||||
|
func (l *Logger) emit(pc uintptr, level Level, msg string) {
|
||||||
|
_ = l.h.Handle(l.ctx, Record{Time: time.Now(), Level: level, Message: msg, PC: pc})
|
||||||
|
}
|
||||||
|
|
||||||
|
// callerPC returns the program counter skip frames up, 4 being the caller of an exported method.
|
||||||
|
func callerPC(skip int) uintptr {
|
||||||
|
var pcs [1]uintptr
|
||||||
|
runtime.Callers(skip, pcs[:])
|
||||||
|
return pcs[0]
|
||||||
|
}
|
||||||
|
|
||||||
|
func argsToAttrs(args []any) []Attr {
|
||||||
|
attrs := make([]Attr, 0, len(args)/2+1)
|
||||||
|
for len(args) > 0 {
|
||||||
|
switch k := args[0].(type) {
|
||||||
|
case Attr:
|
||||||
|
attrs = append(attrs, k)
|
||||||
|
args = args[1:]
|
||||||
|
case string:
|
||||||
|
if len(args) == 1 {
|
||||||
|
attrs = append(attrs, Attr{Key: badKey, Value: k})
|
||||||
|
args = nil
|
||||||
|
} else {
|
||||||
|
attrs = append(attrs, Attr{Key: k, Value: args[1]})
|
||||||
|
args = args[2:]
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
attrs = append(attrs, Attr{Key: badKey, Value: k})
|
||||||
|
args = args[1:]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return attrs
|
||||||
|
}
|
||||||
@@ -0,0 +1,163 @@
|
|||||||
|
// Copyright 2026 The Gitea Authors. All rights reserved.
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
package logging_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"io"
|
||||||
|
"runtime"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"gitea.com/gitea/runner/act/common/logging"
|
||||||
|
"gitea.com/gitea/runner/act/common/logging/logtest"
|
||||||
|
|
||||||
|
"github.com/sirupsen/logrus"
|
||||||
|
logrustest "github.com/sirupsen/logrus/hooks/test"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestParseLevel(t *testing.T) {
|
||||||
|
for in, want := range map[string]logging.Level{
|
||||||
|
"trace": logging.LevelTrace, "DEBUG": logging.LevelDebug, "info": logging.LevelInfo,
|
||||||
|
"warn": logging.LevelWarn, "warning": logging.LevelWarn,
|
||||||
|
"error": logging.LevelError, "fatal": logging.LevelError, "panic": logging.LevelError,
|
||||||
|
} {
|
||||||
|
got, err := logging.ParseLevel(in)
|
||||||
|
require.NoError(t, err, in)
|
||||||
|
assert.Equal(t, want, got, in)
|
||||||
|
}
|
||||||
|
_, err := logging.ParseLevel("loud")
|
||||||
|
require.Error(t, err)
|
||||||
|
assert.Equal(t, "warn", logging.LevelWarn.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
type countingStringer struct{ n *int }
|
||||||
|
|
||||||
|
func (c countingStringer) String() string { *c.n++; return "x" }
|
||||||
|
|
||||||
|
func TestLogger(t *testing.T) {
|
||||||
|
h := logtest.NewWithLevel(logging.LevelTrace)
|
||||||
|
l := logging.New(h)
|
||||||
|
|
||||||
|
l.With("job", "j1").WithError(errors.New("boom")).Warnf("step %d", 2)
|
||||||
|
l.Info("a", 1, 2, "b")
|
||||||
|
l.With("k").With(7).Debug("x")
|
||||||
|
|
||||||
|
recs := h.AllRecords()
|
||||||
|
require.Len(t, recs, 3)
|
||||||
|
assert.Equal(t, "step 2", recs[0].Message)
|
||||||
|
assert.Equal(t, logging.LevelWarn, recs[0].Level)
|
||||||
|
assert.False(t, recs[0].Time.IsZero())
|
||||||
|
job, _ := recs[0].Attr("job")
|
||||||
|
assert.Equal(t, "j1", job)
|
||||||
|
errv, _ := recs[0].Attr("error")
|
||||||
|
require.EqualError(t, errv.(error), "boom")
|
||||||
|
assert.Equal(t, "a1 2b", recs[1].Message) // fmt.Sprint spacing
|
||||||
|
bad, _ := recs[2].Attr("!BADKEY")
|
||||||
|
assert.Equal(t, 7, bad)
|
||||||
|
|
||||||
|
h.Reset()
|
||||||
|
assert.Nil(t, h.LastRecord())
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestLoggerSkipsFormattingWhenDisabled(t *testing.T) {
|
||||||
|
n := 0
|
||||||
|
l := logging.New(logging.NewLevelHandler(logtest.NewWithLevel(logging.LevelTrace), logging.LevelWarn))
|
||||||
|
l.Debugf("%v", countingStringer{&n})
|
||||||
|
assert.Zero(t, n)
|
||||||
|
assert.False(t, l.Enabled(logging.LevelInfo))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMultiHandler(t *testing.T) {
|
||||||
|
assert.False(t, logging.DiscardHandler.Enabled(t.Context(), logging.LevelError))
|
||||||
|
all, warnOnly := logtest.New(), logtest.NewWithLevel(logging.LevelTrace)
|
||||||
|
l := logging.New(logging.NewMultiHandler(all, logging.NewLevelHandler(warnOnly, logging.LevelWarn), logging.DiscardHandler)).With("a", 1)
|
||||||
|
l.Info("i")
|
||||||
|
l.Error("e")
|
||||||
|
assert.Len(t, all.AllRecords(), 2)
|
||||||
|
require.Len(t, warnOnly.AllRecords(), 1)
|
||||||
|
v, _ := warnOnly.LastRecord().Attr("a")
|
||||||
|
assert.Equal(t, 1, v)
|
||||||
|
}
|
||||||
|
|
||||||
|
func frameOf(t *testing.T, pc uintptr) runtime.Frame {
|
||||||
|
t.Helper()
|
||||||
|
require.NotZero(t, pc)
|
||||||
|
f, _ := runtime.CallersFrames([]uintptr{pc}).Next()
|
||||||
|
return f
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCallerPC(t *testing.T) {
|
||||||
|
h := logtest.New()
|
||||||
|
l := logging.New(h)
|
||||||
|
l.Infof("method")
|
||||||
|
l.Info("method")
|
||||||
|
for _, r := range h.AllRecords() {
|
||||||
|
assert.True(t, strings.HasSuffix(frameOf(t, r.PC).Function, "TestCallerPC"), r.Message)
|
||||||
|
}
|
||||||
|
|
||||||
|
std := logrus.StandardLogger()
|
||||||
|
prevOut, prevCaller, prevHooks := std.Out, std.ReportCaller, std.ReplaceHooks(logrus.LevelHooks{})
|
||||||
|
t.Cleanup(func() {
|
||||||
|
std.SetOutput(prevOut)
|
||||||
|
std.SetReportCaller(prevCaller)
|
||||||
|
std.ReplaceHooks(prevHooks)
|
||||||
|
})
|
||||||
|
std.SetOutput(io.Discard)
|
||||||
|
std.SetReportCaller(true)
|
||||||
|
hook := logrustest.NewGlobal()
|
||||||
|
logging.Infof("global")
|
||||||
|
logging.Error("global")
|
||||||
|
|
||||||
|
require.Len(t, hook.AllEntries(), 2)
|
||||||
|
for _, e := range hook.AllEntries() {
|
||||||
|
require.NotNil(t, e.Caller, e.Message)
|
||||||
|
assert.True(t, strings.HasSuffix(e.Caller.Function, "TestCallerPC"), e.Caller.Function)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestLogrusHandler(t *testing.T) {
|
||||||
|
logger, hook := logrustest.NewNullLogger()
|
||||||
|
logger.SetLevel(logrus.DebugLevel)
|
||||||
|
logger.SetReportCaller(true)
|
||||||
|
l := logging.New(logging.NewLogrusHandler(logger)).With("job", "j1")
|
||||||
|
|
||||||
|
when := time.Date(2026, 1, 2, 3, 4, 5, 0, time.UTC)
|
||||||
|
h := logging.NewLogrusHandler(logger).WithAttrs([]logging.Attr{{Key: "base", Value: true}})
|
||||||
|
require.NoError(t, h.Handle(t.Context(), logging.Record{Time: when, Level: logging.LevelWarn, Message: "timed"}))
|
||||||
|
|
||||||
|
l.Tracef("dropped")
|
||||||
|
assert.False(t, l.Enabled(logging.LevelTrace))
|
||||||
|
l.WithError(errors.New("boom")).Errorf("fail %s", "x")
|
||||||
|
l.Debugf("dbg")
|
||||||
|
|
||||||
|
entries := hook.AllEntries()
|
||||||
|
require.Len(t, entries, 3)
|
||||||
|
assert.Equal(t, when, entries[0].Time)
|
||||||
|
assert.Equal(t, logrus.WarnLevel, entries[0].Level)
|
||||||
|
assert.Equal(t, true, entries[0].Data["base"])
|
||||||
|
|
||||||
|
assert.Equal(t, logrus.ErrorLevel, entries[1].Level)
|
||||||
|
assert.Equal(t, "fail x", entries[1].Message)
|
||||||
|
assert.Equal(t, "j1", entries[1].Data["job"])
|
||||||
|
require.EqualError(t, entries[1].Data[logrus.ErrorKey].(error), "boom")
|
||||||
|
assert.Equal(t, logrus.DebugLevel, entries[2].Level)
|
||||||
|
|
||||||
|
require.NotNil(t, entries[1].Caller)
|
||||||
|
assert.True(t, strings.HasSuffix(entries[1].Caller.Function, "TestLogrusHandler"), entries[1].Caller.Function)
|
||||||
|
assert.Contains(t, entries[1].Caller.File, "logging_test.go")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDefaultLevel(t *testing.T) {
|
||||||
|
prev := logging.GetLevel()
|
||||||
|
t.Cleanup(func() { logging.SetLevel(prev) })
|
||||||
|
|
||||||
|
logging.SetLevel(logging.LevelWarn)
|
||||||
|
assert.Equal(t, logging.LevelWarn, logging.GetLevel())
|
||||||
|
assert.True(t, logging.Enabled(logging.LevelError))
|
||||||
|
assert.False(t, logging.Enabled(logging.LevelInfo))
|
||||||
|
}
|
||||||
@@ -0,0 +1,87 @@
|
|||||||
|
// Copyright 2026 The Gitea Authors. All rights reserved.
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
package logging
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"runtime"
|
||||||
|
|
||||||
|
"github.com/sirupsen/logrus"
|
||||||
|
)
|
||||||
|
|
||||||
|
type logrusHandler struct {
|
||||||
|
entry *logrus.Entry
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewLogrusHandler writes to l, so its level, hooks, formatter and output all apply.
|
||||||
|
func NewLogrusHandler(l *logrus.Logger) Handler {
|
||||||
|
return &logrusHandler{entry: logrus.NewEntry(l)}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *logrusHandler) Enabled(_ context.Context, level Level) bool {
|
||||||
|
return h.entry.Logger.IsLevelEnabled(toLogrusLevel(level))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *logrusHandler) Handle(ctx context.Context, r Record) error {
|
||||||
|
if ctx == nil {
|
||||||
|
ctx = context.Background()
|
||||||
|
}
|
||||||
|
e := h.entry.WithContext(ctx) // always a copy, so the fields below never reach h.entry
|
||||||
|
if !r.Time.IsZero() {
|
||||||
|
e = e.WithTime(r.Time)
|
||||||
|
}
|
||||||
|
if len(r.Attrs) > 0 {
|
||||||
|
e = e.WithFields(attrsToFields(r.Attrs))
|
||||||
|
}
|
||||||
|
if r.PC != 0 && e.Logger.ReportCaller {
|
||||||
|
// logrus only skips its own frames, so without this it would report the logging package
|
||||||
|
frame, _ := runtime.CallersFrames([]uintptr{r.PC}).Next()
|
||||||
|
e.Caller = &frame
|
||||||
|
}
|
||||||
|
e.Log(toLogrusLevel(r.Level), r.Message)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *logrusHandler) WithAttrs(attrs []Attr) Handler {
|
||||||
|
return &logrusHandler{entry: h.entry.WithFields(attrsToFields(attrs))}
|
||||||
|
}
|
||||||
|
|
||||||
|
func attrsToFields(attrs []Attr) logrus.Fields {
|
||||||
|
fields := make(logrus.Fields, len(attrs))
|
||||||
|
for _, a := range attrs {
|
||||||
|
fields[a.Key] = a.Value
|
||||||
|
}
|
||||||
|
return fields
|
||||||
|
}
|
||||||
|
|
||||||
|
func toLogrusLevel(l Level) logrus.Level {
|
||||||
|
switch {
|
||||||
|
case l <= LevelTrace:
|
||||||
|
return logrus.TraceLevel
|
||||||
|
case l <= LevelDebug:
|
||||||
|
return logrus.DebugLevel
|
||||||
|
case l <= LevelInfo:
|
||||||
|
return logrus.InfoLevel
|
||||||
|
case l <= LevelWarn:
|
||||||
|
return logrus.WarnLevel
|
||||||
|
default:
|
||||||
|
return logrus.ErrorLevel
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// fromLogrusLevel maps logrus's Fatal and Panic to LevelError.
|
||||||
|
func fromLogrusLevel(l logrus.Level) Level {
|
||||||
|
switch l {
|
||||||
|
case logrus.TraceLevel:
|
||||||
|
return LevelTrace
|
||||||
|
case logrus.DebugLevel:
|
||||||
|
return LevelDebug
|
||||||
|
case logrus.InfoLevel:
|
||||||
|
return LevelInfo
|
||||||
|
case logrus.WarnLevel:
|
||||||
|
return LevelWarn
|
||||||
|
default:
|
||||||
|
return LevelError
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,72 @@
|
|||||||
|
// Copyright 2026 The Gitea Authors. All rights reserved.
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
// Package logtest provides a Handler that keeps records in memory for assertions.
|
||||||
|
package logtest
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"slices"
|
||||||
|
"sync"
|
||||||
|
|
||||||
|
"gitea.com/gitea/runner/act/common/logging"
|
||||||
|
)
|
||||||
|
|
||||||
|
type store struct {
|
||||||
|
mu sync.Mutex
|
||||||
|
records []logging.Record
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handler records what it receives at or above its level. Loggers derived with WithAttrs share one store.
|
||||||
|
type Handler struct {
|
||||||
|
store *store
|
||||||
|
attrs []logging.Attr
|
||||||
|
minLevel logging.Level
|
||||||
|
}
|
||||||
|
|
||||||
|
// New records from LevelInfo up, like the logger of logrus's hooks/test.
|
||||||
|
func New() *Handler {
|
||||||
|
return NewWithLevel(logging.LevelInfo)
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewWithLevel(minLevel logging.Level) *Handler {
|
||||||
|
return &Handler{store: &store{}, minLevel: minLevel}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *Handler) Enabled(_ context.Context, level logging.Level) bool { return level >= h.minLevel }
|
||||||
|
|
||||||
|
// Handle stores the record with the attributes accumulated by WithAttrs placed first.
|
||||||
|
func (h *Handler) Handle(_ context.Context, r logging.Record) error {
|
||||||
|
r.Attrs = append(slices.Clone(h.attrs), r.Attrs...)
|
||||||
|
h.store.mu.Lock()
|
||||||
|
defer h.store.mu.Unlock()
|
||||||
|
h.store.records = append(h.store.records, r)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *Handler) WithAttrs(attrs []logging.Attr) logging.Handler {
|
||||||
|
return &Handler{store: h.store, attrs: append(slices.Clone(h.attrs), attrs...), minLevel: h.minLevel}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *Handler) AllRecords() []logging.Record {
|
||||||
|
h.store.mu.Lock()
|
||||||
|
defer h.store.mu.Unlock()
|
||||||
|
return slices.Clone(h.store.records)
|
||||||
|
}
|
||||||
|
|
||||||
|
// LastRecord returns nil when nothing was logged.
|
||||||
|
func (h *Handler) LastRecord() *logging.Record {
|
||||||
|
h.store.mu.Lock()
|
||||||
|
defer h.store.mu.Unlock()
|
||||||
|
if len(h.store.records) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
r := h.store.records[len(h.store.records)-1]
|
||||||
|
return &r
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *Handler) Reset() {
|
||||||
|
h.store.mu.Lock()
|
||||||
|
defer h.store.mu.Unlock()
|
||||||
|
h.store.records = nil
|
||||||
|
}
|
||||||
@@ -12,7 +12,7 @@ import (
|
|||||||
"errors"
|
"errors"
|
||||||
"io"
|
"io"
|
||||||
|
|
||||||
"github.com/sirupsen/logrus"
|
"gitea.com/gitea/runner/act/common/logging"
|
||||||
)
|
)
|
||||||
|
|
||||||
type dockerMessage struct {
|
type dockerMessage struct {
|
||||||
@@ -26,7 +26,7 @@ type dockerMessage struct {
|
|||||||
Progress string `json:"progress"`
|
Progress string `json:"progress"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func logDockerResponse(logger logrus.FieldLogger, dockerResponse io.ReadCloser, isError bool) error {
|
func logDockerResponse(logger *logging.Logger, dockerResponse io.ReadCloser, isError bool) error {
|
||||||
if dockerResponse == nil {
|
if dockerResponse == nil {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@@ -77,7 +77,7 @@ func logDockerResponse(logger logrus.FieldLogger, dockerResponse io.ReadCloser,
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func writeLog(logger logrus.FieldLogger, isError bool, format string, args ...any) {
|
func writeLog(logger *logging.Logger, isError bool, format string, args ...any) {
|
||||||
if isError {
|
if isError {
|
||||||
logger.Errorf(format, args...)
|
logger.Errorf(format, args...)
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -10,15 +10,16 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
"gitea.com/gitea/runner/act/common/logging"
|
||||||
|
"gitea.com/gitea/runner/act/common/logging/logtest"
|
||||||
|
|
||||||
"github.com/docker/cli/cli/config"
|
"github.com/docker/cli/cli/config"
|
||||||
log "github.com/sirupsen/logrus"
|
|
||||||
"github.com/sirupsen/logrus/hooks/test"
|
|
||||||
assert "github.com/stretchr/testify/assert"
|
assert "github.com/stretchr/testify/assert"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
)
|
)
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
log.SetLevel(log.DebugLevel)
|
logging.SetLevel(logging.LevelDebug)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestCleanImage(t *testing.T) {
|
func TestCleanImage(t *testing.T) {
|
||||||
@@ -79,7 +80,7 @@ func TestIsPinnedImage(t *testing.T) {
|
|||||||
// The pull path reports a failure the daemon sent mid-stream, so it must carry the reason
|
// The pull path reports a failure the daemon sent mid-stream, so it must carry the reason
|
||||||
// whichever of the two shapes the daemon used.
|
// whichever of the two shapes the daemon used.
|
||||||
func TestLogDockerResponseError(t *testing.T) {
|
func TestLogDockerResponseError(t *testing.T) {
|
||||||
logger, _ := test.NewNullLogger()
|
logger := logging.New(logtest.New())
|
||||||
streamErr := func(line string) error {
|
streamErr := func(line string) error {
|
||||||
return logDockerResponse(logger, io.NopCloser(strings.NewReader(line)), false)
|
return logDockerResponse(logger, io.NopCloser(strings.NewReader(line)), false)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"gitea.com/gitea/runner/act/common"
|
"gitea.com/gitea/runner/act/common"
|
||||||
|
"gitea.com/gitea/runner/act/common/logging"
|
||||||
|
|
||||||
"dario.cat/mergo"
|
"dario.cat/mergo"
|
||||||
"github.com/bmatcuk/doublestar/v4"
|
"github.com/bmatcuk/doublestar/v4"
|
||||||
@@ -40,7 +41,6 @@ import (
|
|||||||
"github.com/moby/moby/client"
|
"github.com/moby/moby/client"
|
||||||
"github.com/moby/moby/client/pkg/versions"
|
"github.com/moby/moby/client/pkg/versions"
|
||||||
specs "github.com/opencontainers/image-spec/specs-go/v1"
|
specs "github.com/opencontainers/image-spec/specs-go/v1"
|
||||||
"github.com/sirupsen/logrus"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// drainGracePeriod bounds how long we wait for an output-copy goroutine to
|
// drainGracePeriod bounds how long we wait for an output-copy goroutine to
|
||||||
@@ -1046,7 +1046,7 @@ func (cr *containerReference) wait() common.Executor {
|
|||||||
// sanitizeOptionsHostConfig takes back everything a workflow could escape the container with,
|
// sanitizeOptionsHostConfig takes back everything a workflow could escape the container with,
|
||||||
// setting each field to trusted, which is what the runner's own options parse to on their own.
|
// setting each field to trusted, which is what the runner's own options parse to on their own.
|
||||||
// Only for unprivileged mode, since privileged mode grants host access anyway.
|
// Only for unprivileged mode, since privileged mode grants host access anyway.
|
||||||
func sanitizeOptionsHostConfig(logger logrus.FieldLogger, hostConfig, trusted *container.HostConfig) {
|
func sanitizeOptionsHostConfig(logger *logging.Logger, hostConfig, trusted *container.HostConfig) {
|
||||||
resetOption(logger, "--pid", &hostConfig.PidMode, trusted.PidMode)
|
resetOption(logger, "--pid", &hostConfig.PidMode, trusted.PidMode)
|
||||||
resetOption(logger, "--ipc", &hostConfig.IpcMode, trusted.IpcMode)
|
resetOption(logger, "--ipc", &hostConfig.IpcMode, trusted.IpcMode)
|
||||||
resetOption(logger, "--uts", &hostConfig.UTSMode, trusted.UTSMode)
|
resetOption(logger, "--uts", &hostConfig.UTSMode, trusted.UTSMode)
|
||||||
@@ -1085,7 +1085,7 @@ func sanitizeOptionsHostConfig(logger logrus.FieldLogger, hostConfig, trusted *c
|
|||||||
|
|
||||||
// resetOption puts a field back to the runner's own value. It compares the values rather than
|
// resetOption puts a field back to the runner's own value. It compares the values rather than
|
||||||
// the flags, so a field that more than one option feeds cannot slip through.
|
// the flags, so a field that more than one option feeds cannot slip through.
|
||||||
func resetOption[T any](logger logrus.FieldLogger, option string, field *T, trusted T) {
|
func resetOption[T any](logger *logging.Logger, option string, field *T, trusted T) {
|
||||||
if reflect.DeepEqual(*field, trusted) {
|
if reflect.DeepEqual(*field, trusted) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,6 +19,8 @@ import (
|
|||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"gitea.com/gitea/runner/act/common"
|
"gitea.com/gitea/runner/act/common"
|
||||||
|
"gitea.com/gitea/runner/act/common/logging"
|
||||||
|
"gitea.com/gitea/runner/act/common/logging/logtest"
|
||||||
|
|
||||||
cerrdefs "github.com/containerd/errdefs"
|
cerrdefs "github.com/containerd/errdefs"
|
||||||
"github.com/moby/moby/api/pkg/stdcopy"
|
"github.com/moby/moby/api/pkg/stdcopy"
|
||||||
@@ -26,7 +28,6 @@ import (
|
|||||||
"github.com/moby/moby/api/types/mount"
|
"github.com/moby/moby/api/types/mount"
|
||||||
"github.com/moby/moby/api/types/network"
|
"github.com/moby/moby/api/types/network"
|
||||||
mobyclient "github.com/moby/moby/client"
|
mobyclient "github.com/moby/moby/client"
|
||||||
"github.com/sirupsen/logrus/hooks/test"
|
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
"github.com/stretchr/testify/mock"
|
"github.com/stretchr/testify/mock"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
@@ -353,7 +354,8 @@ func TestRemoveIgnoresAutoRemoveRace(t *testing.T) {
|
|||||||
{name: "real failure", err: removeFailure, wantErr: removeFailure},
|
{name: "real failure", err: removeFailure, wantErr: removeFailure},
|
||||||
} {
|
} {
|
||||||
t.Run(tc.name, func(t *testing.T) {
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
logger, hook := test.NewNullLogger()
|
hook := logtest.New()
|
||||||
|
logger := logging.New(hook)
|
||||||
ctx := common.WithLogger(context.Background(), logger)
|
ctx := common.WithLogger(context.Background(), logger)
|
||||||
client := &mockDockerClient{}
|
client := &mockDockerClient{}
|
||||||
client.On("ContainerKill", ctx, "abc", killOpts).Return(mobyclient.ContainerKillResult{}, nil)
|
client.On("ContainerKill", ctx, "abc", killOpts).Return(mobyclient.ContainerKillResult{}, nil)
|
||||||
@@ -377,7 +379,7 @@ func TestRemoveIgnoresAutoRemoveRace(t *testing.T) {
|
|||||||
} else {
|
} else {
|
||||||
assert.Empty(t, cr.id)
|
assert.Empty(t, cr.id)
|
||||||
}
|
}
|
||||||
assert.Empty(t, hook.AllEntries())
|
assert.Empty(t, hook.AllRecords())
|
||||||
client.AssertExpectations(t)
|
client.AssertExpectations(t)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -614,7 +616,8 @@ func TestCheckVolumes(t *testing.T) {
|
|||||||
}
|
}
|
||||||
for _, tc := range testCases {
|
for _, tc := range testCases {
|
||||||
t.Run(tc.desc, func(t *testing.T) {
|
t.Run(tc.desc, func(t *testing.T) {
|
||||||
logger, hook := test.NewNullLogger()
|
hook := logtest.New()
|
||||||
|
logger := logging.New(hook)
|
||||||
ctx := common.WithLogger(context.Background(), logger)
|
ctx := common.WithLogger(context.Background(), logger)
|
||||||
cr := &containerReference{
|
cr := &containerReference{
|
||||||
input: &NewContainerInput{
|
input: &NewContainerInput{
|
||||||
@@ -624,7 +627,7 @@ func TestCheckVolumes(t *testing.T) {
|
|||||||
_, hostConf := cr.sanitizeConfig(ctx, &container.Config{}, &container.HostConfig{Binds: tc.binds, Mounts: tc.mounts})
|
_, hostConf := cr.sanitizeConfig(ctx, &container.Config{}, &container.HostConfig{Binds: tc.binds, Mounts: tc.mounts})
|
||||||
assert.Equal(t, tc.expectedBinds, hostConf.Binds)
|
assert.Equal(t, tc.expectedBinds, hostConf.Binds)
|
||||||
assert.ElementsMatch(t, tc.expectedMounts, hostConf.Mounts)
|
assert.ElementsMatch(t, tc.expectedMounts, hostConf.Mounts)
|
||||||
assert.Len(t, hook.AllEntries(), len(tc.binds)+len(tc.mounts)-len(tc.expectedBinds)-len(tc.expectedMounts)) // every drop is warned about
|
assert.Len(t, hook.AllRecords(), len(tc.binds)+len(tc.mounts)-len(tc.expectedBinds)-len(tc.expectedMounts)) // every drop is warned about
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -652,7 +655,7 @@ func TestMergeContainerConfigsKeepsTheRunnersFilesAndEnvToItself(t *testing.T) {
|
|||||||
t.Setenv("RUNNER_SECRET", "s3cr3t")
|
t.Setenv("RUNNER_SECRET", "s3cr3t")
|
||||||
|
|
||||||
for _, option := range []string{"--env-file " + hostFile, "--label-file " + hostFile} {
|
for _, option := range []string{"--env-file " + hostFile, "--label-file " + hostFile} {
|
||||||
logger, _ := test.NewNullLogger()
|
logger := logging.New(logtest.New())
|
||||||
cr := &containerReference{input: &NewContainerInput{NetworkMode: "bridge", WorkflowOptions: option}}
|
cr := &containerReference{input: &NewContainerInput{NetworkMode: "bridge", WorkflowOptions: option}}
|
||||||
|
|
||||||
_, _, err := cr.mergeContainerConfigs(common.WithLogger(context.Background(), logger), &container.Config{}, &container.HostConfig{})
|
_, _, err := cr.mergeContainerConfigs(common.WithLogger(context.Background(), logger), &container.Config{}, &container.HostConfig{})
|
||||||
@@ -665,7 +668,7 @@ func TestMergeContainerConfigsKeepsTheRunnersFilesAndEnvToItself(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// a bare name is no longer resolved from the runner's environment, for either source
|
// a bare name is no longer resolved from the runner's environment, for either source
|
||||||
logger, _ := test.NewNullLogger()
|
logger := logging.New(logtest.New())
|
||||||
cr := &containerReference{input: &NewContainerInput{
|
cr := &containerReference{input: &NewContainerInput{
|
||||||
NetworkMode: "bridge",
|
NetworkMode: "bridge",
|
||||||
RunnerOptions: "--env RUNNER_SECRET",
|
RunnerOptions: "--env RUNNER_SECRET",
|
||||||
@@ -678,7 +681,7 @@ func TestMergeContainerConfigsKeepsTheRunnersFilesAndEnvToItself(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestSanitizeOptionsHostConfig(t *testing.T) {
|
func TestSanitizeOptionsHostConfig(t *testing.T) {
|
||||||
logger, _ := test.NewNullLogger()
|
logger := logging.New(logtest.New())
|
||||||
|
|
||||||
// every field the sanitizer resets, so a reset dropped in a refactor fails here
|
// every field the sanitizer resets, so a reset dropped in a refactor fails here
|
||||||
hostConfig := &container.HostConfig{
|
hostConfig := &container.HostConfig{
|
||||||
@@ -711,9 +714,10 @@ func TestSanitizeOptionsHostConfig(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// mergeOptions merges both option sources into a bare container, returning the result and its log.
|
// mergeOptions merges both option sources into a bare container, returning the result and its log.
|
||||||
func mergeOptions(t *testing.T, runnerOptions, workflowOptions string, privileged bool) (*container.HostConfig, *test.Hook) {
|
func mergeOptions(t *testing.T, runnerOptions, workflowOptions string, privileged bool) (*container.HostConfig, *logtest.Handler) {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
logger, hook := test.NewNullLogger()
|
hook := logtest.New()
|
||||||
|
logger := logging.New(hook)
|
||||||
cr := &containerReference{input: &NewContainerInput{
|
cr := &containerReference{input: &NewContainerInput{
|
||||||
RunnerOptions: runnerOptions,
|
RunnerOptions: runnerOptions,
|
||||||
WorkflowOptions: workflowOptions,
|
WorkflowOptions: workflowOptions,
|
||||||
@@ -765,7 +769,7 @@ func TestMergeContainerConfigsStripsDangerousOptionsWhenUnprivileged(t *testing.
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestCheckVolumesRejectsEscapingHostPaths(t *testing.T) {
|
func TestCheckVolumesRejectsEscapingHostPaths(t *testing.T) {
|
||||||
logger, _ := test.NewNullLogger()
|
logger := logging.New(logtest.New())
|
||||||
ctx := common.WithLogger(context.Background(), logger)
|
ctx := common.WithLogger(context.Background(), logger)
|
||||||
|
|
||||||
base := t.TempDir()
|
base := t.TempDir()
|
||||||
@@ -806,14 +810,15 @@ func TestSanitizeConfigKeepsTmpfsMounts(t *testing.T) {
|
|||||||
require.Len(t, hostConfig.Mounts, 1)
|
require.Len(t, hostConfig.Mounts, 1)
|
||||||
|
|
||||||
for _, validVolumes := range [][]string{nil, {"data"}, {"**:ro"}} {
|
for _, validVolumes := range [][]string{nil, {"data"}, {"**:ro"}} {
|
||||||
logger, hook := test.NewNullLogger()
|
hook := logtest.New()
|
||||||
|
logger := logging.New(hook)
|
||||||
_, sanitized := (&containerReference{input: &NewContainerInput{ValidVolumes: validVolumes}}).sanitizeConfig(common.WithLogger(context.Background(), logger), &container.Config{}, &container.HostConfig{Mounts: []mount.Mount{
|
_, sanitized := (&containerReference{input: &NewContainerInput{ValidVolumes: validVolumes}}).sanitizeConfig(common.WithLogger(context.Background(), logger), &container.Config{}, &container.HostConfig{Mounts: []mount.Mount{
|
||||||
hostConfig.Mounts[0],
|
hostConfig.Mounts[0],
|
||||||
{Type: mount.TypeVolume, Target: "/anonymous"},
|
{Type: mount.TypeVolume, Target: "/anonymous"},
|
||||||
{Type: mount.TypeTmpfs, Source: "/etc", Target: "/etc-copy"},
|
{Type: mount.TypeTmpfs, Source: "/etc", Target: "/etc-copy"},
|
||||||
}})
|
}})
|
||||||
assert.Equal(t, hostConfig.Mounts, sanitized.Mounts)
|
assert.Equal(t, hostConfig.Mounts, sanitized.Mounts)
|
||||||
assert.Len(t, hook.AllEntries(), 2)
|
assert.Len(t, hook.AllRecords(), 2)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -896,7 +901,7 @@ func TestContainerInfoFromInspect(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestMergeContainerConfigsVolumesReplaceRunnerMounts(t *testing.T) {
|
func TestMergeContainerConfigsVolumesReplaceRunnerMounts(t *testing.T) {
|
||||||
logger, _ := test.NewNullLogger()
|
logger := logging.New(logtest.New())
|
||||||
ctx := common.WithLogger(context.Background(), logger)
|
ctx := common.WithLogger(context.Background(), logger)
|
||||||
cr := &containerReference{
|
cr := &containerReference{
|
||||||
input: &NewContainerInput{
|
input: &NewContainerInput{
|
||||||
@@ -919,7 +924,7 @@ func TestMergeContainerConfigsVolumesReplaceRunnerMounts(t *testing.T) {
|
|||||||
func TestMergeContainerConfigsWarnsOnlyAboutOptionsThatWereGiven(t *testing.T) {
|
func TestMergeContainerConfigsWarnsOnlyAboutOptionsThatWereGiven(t *testing.T) {
|
||||||
warnings := func(runnerOptions, workflowOptions string) int {
|
warnings := func(runnerOptions, workflowOptions string) int {
|
||||||
_, hook := mergeOptions(t, runnerOptions, workflowOptions, false)
|
_, hook := mergeOptions(t, runnerOptions, workflowOptions, false)
|
||||||
return len(hook.AllEntries())
|
return len(hook.AllRecords())
|
||||||
}
|
}
|
||||||
|
|
||||||
assert.Zero(t, warnings("--volume /host/tools:/opt/hostedtoolcache", ""))
|
assert.Zero(t, warnings("--volume /host/tools:/opt/hostedtoolcache", ""))
|
||||||
@@ -931,7 +936,8 @@ func TestMergeContainerConfigsWarnsOnlyAboutOptionsThatWereGiven(t *testing.T) {
|
|||||||
func TestDockerCreateKeepsInputEntrypoint(t *testing.T) {
|
func TestDockerCreateKeepsInputEntrypoint(t *testing.T) {
|
||||||
for _, entrypoint := range [][]string{nil, {"sleep", "3600"}} {
|
for _, entrypoint := range [][]string{nil, {"sleep", "3600"}} {
|
||||||
for _, optionEntrypoint := range []string{"/sbin/dumb-init", ""} {
|
for _, optionEntrypoint := range []string{"/sbin/dumb-init", ""} {
|
||||||
logger, hook := test.NewNullLogger()
|
hook := logtest.New()
|
||||||
|
logger := logging.New(hook)
|
||||||
cr := &containerReference{
|
cr := &containerReference{
|
||||||
input: &NewContainerInput{
|
input: &NewContainerInput{
|
||||||
NetworkMode: "bridge",
|
NetworkMode: "bridge",
|
||||||
@@ -952,13 +958,13 @@ func TestDockerCreateKeepsInputEntrypoint(t *testing.T) {
|
|||||||
}
|
}
|
||||||
require.NoError(t, cr.create(nil, nil)(common.WithLogger(t.Context(), logger)))
|
require.NoError(t, cr.create(nil, nil)(common.WithLogger(t.Context(), logger)))
|
||||||
assert.Equal(t, "created", cr.id)
|
assert.Equal(t, "created", cr.id)
|
||||||
assert.Equal(t, len(entrypoint) > 0, len(hook.AllEntries()) == 1)
|
assert.Equal(t, len(entrypoint) > 0, len(hook.AllRecords()) == 1)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestMergeContainerConfigsKeepsNetworkAliasesFromOptions(t *testing.T) {
|
func TestMergeContainerConfigsKeepsNetworkAliasesFromOptions(t *testing.T) {
|
||||||
logger, _ := test.NewNullLogger()
|
logger := logging.New(logtest.New())
|
||||||
cr := &containerReference{input: &NewContainerInput{
|
cr := &containerReference{input: &NewContainerInput{
|
||||||
NetworkMode: "job-network", NetworkAliases: []string{"redis"}, WorkflowOptions: "--network-alias redis-primary",
|
NetworkMode: "job-network", NetworkAliases: []string{"redis"}, WorkflowOptions: "--network-alias redis-primary",
|
||||||
}}
|
}}
|
||||||
|
|||||||
@@ -18,8 +18,8 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"gitea.com/gitea/runner/act/common"
|
"gitea.com/gitea/runner/act/common"
|
||||||
|
"gitea.com/gitea/runner/act/common/logging"
|
||||||
|
|
||||||
"github.com/sirupsen/logrus"
|
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
)
|
)
|
||||||
@@ -170,8 +170,7 @@ func TestHostEnvironmentAllocatePTY(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestHostEnvironmentRemovePreservesWorkdirByDefault(t *testing.T) {
|
func TestHostEnvironmentRemovePreservesWorkdirByDefault(t *testing.T) {
|
||||||
logger := logrus.New()
|
ctx := common.WithLogger(context.Background(), logging.Default())
|
||||||
ctx := common.WithLogger(context.Background(), logrus.NewEntry(logger))
|
|
||||||
base := t.TempDir()
|
base := t.TempDir()
|
||||||
miscRoot := filepath.Join(base, "misc")
|
miscRoot := filepath.Join(base, "misc")
|
||||||
path := filepath.Join(miscRoot, "hostexecutor")
|
path := filepath.Join(miscRoot, "hostexecutor")
|
||||||
@@ -193,8 +192,7 @@ func TestHostEnvironmentRemovePreservesWorkdirByDefault(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestHostEnvironmentRemoveCleansWorkdirWhenOwned(t *testing.T) {
|
func TestHostEnvironmentRemoveCleansWorkdirWhenOwned(t *testing.T) {
|
||||||
logger := logrus.New()
|
ctx := common.WithLogger(context.Background(), logging.Default())
|
||||||
ctx := common.WithLogger(context.Background(), logrus.NewEntry(logger))
|
|
||||||
base := t.TempDir()
|
base := t.TempDir()
|
||||||
miscRoot := filepath.Join(base, "misc")
|
miscRoot := filepath.Join(base, "misc")
|
||||||
path := filepath.Join(miscRoot, "hostexecutor")
|
path := filepath.Join(miscRoot, "hostexecutor")
|
||||||
@@ -252,8 +250,7 @@ func TestHostEnvironmentRemoveDoesNotHangOnStuckCleanUp(t *testing.T) {
|
|||||||
hostCleanupTimeout = 100 * time.Millisecond
|
hostCleanupTimeout = 100 * time.Millisecond
|
||||||
t.Cleanup(func() { hostCleanupTimeout = orig })
|
t.Cleanup(func() { hostCleanupTimeout = orig })
|
||||||
|
|
||||||
logger := logrus.New()
|
ctx := common.WithLogger(context.Background(), logging.Default())
|
||||||
ctx := common.WithLogger(context.Background(), logrus.NewEntry(logger))
|
|
||||||
base := t.TempDir()
|
base := t.TempDir()
|
||||||
path := filepath.Join(base, "misc", "hostexecutor")
|
path := filepath.Join(base, "misc", "hostexecutor")
|
||||||
require.NoError(t, os.MkdirAll(path, 0o700))
|
require.NoError(t, os.MkdirAll(path, 0o700))
|
||||||
@@ -306,8 +303,7 @@ func TestHostEnvironmentRemoveDoesNotHangOnStuckPathRemoval(t *testing.T) {
|
|||||||
removeAll = origRemoveAll
|
removeAll = origRemoveAll
|
||||||
})
|
})
|
||||||
|
|
||||||
logger := logrus.New()
|
ctx := common.WithLogger(context.Background(), logging.Default())
|
||||||
ctx := common.WithLogger(context.Background(), logrus.NewEntry(logger))
|
|
||||||
base := t.TempDir()
|
base := t.TempDir()
|
||||||
path := filepath.Join(base, "misc", "hostexecutor")
|
path := filepath.Join(base, "misc", "hostexecutor")
|
||||||
require.NoError(t, os.MkdirAll(path, 0o700))
|
require.NoError(t, os.MkdirAll(path, 0o700))
|
||||||
|
|||||||
@@ -74,7 +74,7 @@ func SetRef(ctx context.Context, ghc *model.GithubContext, repoPath string) {
|
|||||||
if ghc.Ref == "" {
|
if ghc.Ref == "" {
|
||||||
ref, err := pinned(ctx, "ref:"+repoPath, func() (string, error) { return findGitRef(ctx, repoPath) })
|
ref, err := pinned(ctx, "ref:"+repoPath, func() (string, error) { return findGitRef(ctx, repoPath) })
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Warningf("unable to get git ref: %v", err)
|
logger.Warnf("unable to get git ref: %v", err)
|
||||||
} else {
|
} else {
|
||||||
logger.Debugf("using github ref: %s", ref)
|
logger.Debugf("using github ref: %s", ref)
|
||||||
ghc.Ref = ref
|
ghc.Ref = ref
|
||||||
@@ -121,7 +121,7 @@ func SetSha(ctx context.Context, ghc *model.GithubContext, repoPath string) {
|
|||||||
return revision, err
|
return revision, err
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Warningf("unable to get git revision: %v", err)
|
logger.Warnf("unable to get git revision: %v", err)
|
||||||
} else {
|
} else {
|
||||||
ghc.Sha = sha
|
ghc.Sha = sha
|
||||||
}
|
}
|
||||||
@@ -134,7 +134,7 @@ func SetRepositoryAndOwner(ctx context.Context, ghc *model.GithubContext, repoPa
|
|||||||
if ghc.Repository == "" {
|
if ghc.Repository == "" {
|
||||||
repo, err := findGithubRepo(ctx, repoPath)
|
repo, err := findGithubRepo(ctx, repoPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
common.Logger(ctx).Warningf("unable to get git repo (repoPath: %v): %v", repoPath, err)
|
common.Logger(ctx).Warnf("unable to get git repo (repoPath: %v): %v", repoPath, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
ghc.Repository = repo
|
ghc.Repository = repo
|
||||||
|
|||||||
@@ -689,17 +689,17 @@ func shouldRunPostStep(step actionStep) common.Conditional {
|
|||||||
stepResult := stepResults[step.getStepModel().ID]
|
stepResult := stepResults[step.getStepModel().ID]
|
||||||
|
|
||||||
if stepResult == nil {
|
if stepResult == nil {
|
||||||
log.WithField("stepResult", model.StepStatusSkipped).Debugf("skipping post step for '%s'; step was not executed", step.getStepModel())
|
log.With("stepResult", model.StepStatusSkipped).Debugf("skipping post step for '%s'; step was not executed", step.getStepModel())
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
if stepResult.Conclusion == model.StepStatusSkipped {
|
if stepResult.Conclusion == model.StepStatusSkipped {
|
||||||
log.WithField("stepResult", model.StepStatusSkipped).Debugf("skipping post step for '%s'; main step was skipped", step.getStepModel())
|
log.With("stepResult", model.StepStatusSkipped).Debugf("skipping post step for '%s'; main step was skipped", step.getStepModel())
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
if step.getActionModel() == nil {
|
if step.getActionModel() == nil {
|
||||||
log.WithField("stepResult", model.StepStatusSkipped).Debugf("skipping post step for '%s': no action model available", step.getStepModel())
|
log.With("stepResult", model.StepStatusSkipped).Debugf("skipping post step for '%s': no action model available", step.getStepModel())
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -117,7 +117,7 @@ func (rc *RunContext) refuseUnsecureCommand(ctx context.Context, command string)
|
|||||||
}
|
}
|
||||||
|
|
||||||
// The step executor logs the failure itself, so keep this line's wording distinct.
|
// The step executor logs the failure itself, so keep this line's wording distinct.
|
||||||
common.Logger(ctx).WithField(rawOutputField, true).Errorf("##[error]%s", EscapeCommandData(fmt.Sprintf(
|
common.Logger(ctx).With(rawOutputField, true).Errorf("##[error]%s", EscapeCommandData(fmt.Sprintf(
|
||||||
"The `%s` command is disabled: it can set the environment of every later step from untrusted output. "+
|
"The `%s` command is disabled: it can set the environment of every later step from untrusted output. "+
|
||||||
"Write to $GITHUB_ENV or $GITHUB_PATH instead, or set ACTIONS_ALLOW_UNSECURE_COMMANDS to allow it",
|
"Write to $GITHUB_ENV or $GITHUB_PATH instead, or set ACTIONS_ALLOW_UNSECURE_COMMANDS to allow it",
|
||||||
command)))
|
command)))
|
||||||
@@ -159,7 +159,7 @@ func (rc *RunContext) setEnv(ctx context.Context, kvPairs map[string]string, arg
|
|||||||
if fromCommand {
|
if fromCommand {
|
||||||
message = "Can't update NODE_OPTIONS environment variable using ::set-env:: command."
|
message = "Can't update NODE_OPTIONS environment variable using ::set-env:: command."
|
||||||
}
|
}
|
||||||
common.Logger(ctx).WithField(rawOutputField, true).Errorf("##[error]%s", EscapeCommandData(message))
|
common.Logger(ctx).With(rawOutputField, true).Errorf("##[error]%s", EscapeCommandData(message))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
common.Logger(ctx).Infof("::set-env:: %s=%s", name, arg)
|
common.Logger(ctx).Infof("::set-env:: %s=%s", name, arg)
|
||||||
|
|||||||
@@ -12,9 +12,10 @@ import (
|
|||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"gitea.com/gitea/runner/act/common"
|
"gitea.com/gitea/runner/act/common"
|
||||||
|
"gitea.com/gitea/runner/act/common/logging"
|
||||||
|
"gitea.com/gitea/runner/act/common/logging/logtest"
|
||||||
|
|
||||||
"gitea.dev/actionslib/pkg/model"
|
"gitea.dev/actionslib/pkg/model"
|
||||||
"github.com/sirupsen/logrus/hooks/test"
|
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
)
|
)
|
||||||
@@ -26,7 +27,8 @@ func unsecureRC() *RunContext {
|
|||||||
|
|
||||||
func TestSetEnv(t *testing.T) {
|
func TestSetEnv(t *testing.T) {
|
||||||
a := assert.New(t)
|
a := assert.New(t)
|
||||||
logger, hook := test.NewNullLogger()
|
hook := logtest.New()
|
||||||
|
logger := logging.New(hook)
|
||||||
ctx := common.WithLogger(context.Background(), logger)
|
ctx := common.WithLogger(context.Background(), logger)
|
||||||
rc := unsecureRC()
|
rc := unsecureRC()
|
||||||
handler := rc.commandHandler(ctx)
|
handler := rc.commandHandler(ctx)
|
||||||
@@ -37,7 +39,7 @@ func TestSetEnv(t *testing.T) {
|
|||||||
rc.setEnvFile(ctx, map[string]string{"name": "node_options"}, "--require env.js")
|
rc.setEnvFile(ctx, map[string]string{"name": "node_options"}, "--require env.js")
|
||||||
a.NotContains(rc.Env, "NODE_OPTIONS")
|
a.NotContains(rc.Env, "NODE_OPTIONS")
|
||||||
a.NotContains(rc.Env, "node_options")
|
a.NotContains(rc.Env, "node_options")
|
||||||
entries := hook.AllEntries()
|
entries := hook.AllRecords()
|
||||||
require.Len(t, entries, 3)
|
require.Len(t, entries, 3)
|
||||||
a.Equal("##[error]Can't update NODE_OPTIONS environment variable using ::set-env:: command.", entries[1].Message)
|
a.Equal("##[error]Can't update NODE_OPTIONS environment variable using ::set-env:: command.", entries[1].Message)
|
||||||
a.Equal("##[error]Can't store NODE_OPTIONS output parameter using '$GITHUB_ENV' command.", entries[2].Message)
|
a.Equal("##[error]Can't store NODE_OPTIONS output parameter using '$GITHUB_ENV' command.", entries[2].Message)
|
||||||
@@ -120,7 +122,8 @@ func TestAddpath(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestStopCommands(t *testing.T) {
|
func TestStopCommands(t *testing.T) {
|
||||||
logger, hook := test.NewNullLogger()
|
hook := logtest.New()
|
||||||
|
logger := logging.New(hook)
|
||||||
|
|
||||||
a := assert.New(t)
|
a := assert.New(t)
|
||||||
ctx := common.WithLogger(context.Background(), logger)
|
ctx := common.WithLogger(context.Background(), logger)
|
||||||
@@ -137,7 +140,7 @@ func TestStopCommands(t *testing.T) {
|
|||||||
a.Equal("abcd", rc.Env["x"])
|
a.Equal("abcd", rc.Env["x"])
|
||||||
|
|
||||||
messages := make([]string, 0)
|
messages := make([]string, 0)
|
||||||
for _, entry := range hook.AllEntries() {
|
for _, entry := range hook.AllRecords() {
|
||||||
messages = append(messages, entry.Message)
|
messages = append(messages, entry.Message)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -174,7 +177,8 @@ func TestAddpathADO(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestAddmask(t *testing.T) {
|
func TestAddmask(t *testing.T) {
|
||||||
logger, hook := test.NewNullLogger()
|
hook := logtest.New()
|
||||||
|
logger := logging.New(hook)
|
||||||
|
|
||||||
a := assert.New(t)
|
a := assert.New(t)
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
@@ -184,7 +188,7 @@ func TestAddmask(t *testing.T) {
|
|||||||
handler := rc.commandHandler(loggerCtx)
|
handler := rc.commandHandler(loggerCtx)
|
||||||
handler("::ADD-MASK::my::secret")
|
handler("::ADD-MASK::my::secret")
|
||||||
|
|
||||||
a.Equal("***", hook.LastEntry().Message)
|
a.Equal("***", hook.LastRecord().Message)
|
||||||
a.Equal([]string{"my::secret"}, rc.Masks)
|
a.Equal([]string{"my::secret"}, rc.Masks)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -15,12 +15,12 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"gitea.com/gitea/runner/act/common"
|
"gitea.com/gitea/runner/act/common"
|
||||||
|
"gitea.com/gitea/runner/act/common/logging/logtest"
|
||||||
"gitea.com/gitea/runner/act/container"
|
"gitea.com/gitea/runner/act/container"
|
||||||
|
|
||||||
"gitea.dev/actionslib/pkg/model"
|
"gitea.dev/actionslib/pkg/model"
|
||||||
cerrdefs "github.com/containerd/errdefs"
|
cerrdefs "github.com/containerd/errdefs"
|
||||||
"github.com/moby/moby/client"
|
"github.com/moby/moby/client"
|
||||||
"github.com/sirupsen/logrus/hooks/test"
|
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
)
|
)
|
||||||
@@ -83,10 +83,10 @@ func TestDockerProxyMountedJob(t *testing.T) {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
hook := &test.Hook{}
|
hook := logtest.New()
|
||||||
require.NoError(t, runner.NewPlanExecutor(plan)(common.WithLoggerHook(ctx, hook)))
|
require.NoError(t, runner.NewPlanExecutor(plan)(common.WithLoggerHandler(ctx, hook)))
|
||||||
var messages []string
|
var messages []string
|
||||||
for _, entry := range hook.AllEntries() {
|
for _, entry := range hook.AllRecords() {
|
||||||
messages = append(messages, strings.TrimSpace(entry.Message))
|
messages = append(messages, strings.TrimSpace(entry.Message))
|
||||||
}
|
}
|
||||||
require.Contains(t, messages, "docker proxy post verified")
|
require.Contains(t, messages, "docker proxy post verified")
|
||||||
|
|||||||
@@ -92,7 +92,7 @@ func printPrepareActions(rc *RunContext, preparers []actionPreparer) common.Exec
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
rawLogger := common.Logger(ctx).WithField(rawOutputField, true)
|
rawLogger := common.Logger(ctx).With(rawOutputField, true)
|
||||||
rawLogger.Infof("Prepare all required actions")
|
rawLogger.Infof("Prepare all required actions")
|
||||||
|
|
||||||
for _, preparer := range preparers {
|
for _, preparer := range preparers {
|
||||||
@@ -126,7 +126,7 @@ func printCompleteJobName(rc *RunContext) common.Executor {
|
|||||||
if name == "" && rc.Run != nil {
|
if name == "" && rc.Run != nil {
|
||||||
name = rc.Run.JobID
|
name = rc.Run.JobID
|
||||||
}
|
}
|
||||||
common.Logger(ctx).WithField(rawOutputField, true).Infof("Complete job name: %s", name)
|
common.Logger(ctx).With(rawOutputField, true).Infof("Complete job name: %s", name)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -412,7 +412,7 @@ func setJobResult(ctx context.Context, info jobInfo, rc *RunContext, success boo
|
|||||||
jobResultMessage = "cancelled"
|
jobResultMessage = "cancelled"
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.WithField("jobResult", jobResult).Infof("Job %s", jobResultMessage)
|
logger.With("jobResult", jobResult).Infof("Job %s", jobResultMessage)
|
||||||
}
|
}
|
||||||
|
|
||||||
// evaluateJobEnv returns the workflow and job env evaluated, as far as it got on error, leaving rc.Env raw.
|
// evaluateJobEnv returns the workflow and job env evaluated, as far as it got on error, leaving rc.Env raw.
|
||||||
|
|||||||
@@ -21,11 +21,11 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"gitea.com/gitea/runner/act/common"
|
"gitea.com/gitea/runner/act/common"
|
||||||
|
"gitea.com/gitea/runner/act/common/logging"
|
||||||
|
"gitea.com/gitea/runner/act/common/logging/logtest"
|
||||||
"gitea.com/gitea/runner/act/container"
|
"gitea.com/gitea/runner/act/container"
|
||||||
|
|
||||||
"gitea.dev/actionslib/pkg/model"
|
"gitea.dev/actionslib/pkg/model"
|
||||||
log "github.com/sirupsen/logrus"
|
|
||||||
logrustest "github.com/sirupsen/logrus/hooks/test"
|
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
"github.com/stretchr/testify/mock"
|
"github.com/stretchr/testify/mock"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
@@ -136,11 +136,8 @@ func (apm *actionPreparerMock) actionDownloadInfo() (string, string, bool) {
|
|||||||
|
|
||||||
func TestPrintPrepareActionsGolden(t *testing.T) {
|
func TestPrintPrepareActionsGolden(t *testing.T) {
|
||||||
buf := &bytes.Buffer{}
|
buf := &bytes.Buffer{}
|
||||||
logger := log.New()
|
logger := logging.New(logging.NewLevelHandler(newJobLogHandler(buf, cyan), logging.LevelInfo))
|
||||||
logger.SetOutput(buf)
|
ctx := common.WithLogger(context.Background(), logger.With("job", "j1"))
|
||||||
logger.SetLevel(log.InfoLevel)
|
|
||||||
logger.SetFormatter(&jobLogFormatter{color: cyan})
|
|
||||||
ctx := common.WithLogger(context.Background(), logger.WithFields(log.Fields{"job": "j1"}))
|
|
||||||
|
|
||||||
preparers := []actionPreparer{
|
preparers := []actionPreparer{
|
||||||
&actionPreparerMock{reference: "actions/checkout@v7", sha: "9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0", ok: true},
|
&actionPreparerMock{reference: "actions/checkout@v7", sha: "9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0", ok: true},
|
||||||
@@ -162,10 +159,8 @@ func TestPrintPrepareActionsGolden(t *testing.T) {
|
|||||||
|
|
||||||
func TestPrintPrepareActionsSkipsWithoutActions(t *testing.T) {
|
func TestPrintPrepareActionsSkipsWithoutActions(t *testing.T) {
|
||||||
buf := &bytes.Buffer{}
|
buf := &bytes.Buffer{}
|
||||||
logger := log.New()
|
logger := logging.New(logging.NewLevelHandler(newJobLogHandler(buf, cyan), logging.LevelInfo))
|
||||||
logger.SetOutput(buf)
|
ctx := common.WithLogger(context.Background(), logger.With("job", "j1"))
|
||||||
logger.SetFormatter(&jobLogFormatter{color: cyan})
|
|
||||||
ctx := common.WithLogger(context.Background(), logger.WithFields(log.Fields{"job": "j1"}))
|
|
||||||
|
|
||||||
require.NoError(t, printPrepareActions(&RunContext{}, nil)(ctx))
|
require.NoError(t, printPrepareActions(&RunContext{}, nil)(ctx))
|
||||||
|
|
||||||
@@ -173,8 +168,8 @@ func TestPrintPrepareActionsSkipsWithoutActions(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestPrintPrepareActionsFailsJobOnDownloadError(t *testing.T) {
|
func TestPrintPrepareActionsFailsJobOnDownloadError(t *testing.T) {
|
||||||
logger, _ := logrustest.NewNullLogger()
|
logger := logging.New(logtest.New())
|
||||||
ctx := common.WithJobErrorContainer(common.WithLogger(context.Background(), logger.WithField("job", "j1")))
|
ctx := common.WithJobErrorContainer(common.WithLogger(context.Background(), logger.With("job", "j1")))
|
||||||
|
|
||||||
downloadErr := errors.New("failed to fetch \"actions/checkout\"")
|
downloadErr := errors.New("failed to fetch \"actions/checkout\"")
|
||||||
rc := &RunContext{}
|
rc := &RunContext{}
|
||||||
@@ -203,10 +198,8 @@ func TestPrintCompleteJobName(t *testing.T) {
|
|||||||
} {
|
} {
|
||||||
t.Run(name, func(t *testing.T) {
|
t.Run(name, func(t *testing.T) {
|
||||||
buf := &bytes.Buffer{}
|
buf := &bytes.Buffer{}
|
||||||
logger := log.New()
|
logger := logging.New(logging.NewLevelHandler(newJobLogHandler(buf, cyan), logging.LevelInfo))
|
||||||
logger.SetOutput(buf)
|
ctx := common.WithLogger(context.Background(), logger.With("job", "j1"))
|
||||||
logger.SetFormatter(&jobLogFormatter{color: cyan})
|
|
||||||
ctx := common.WithLogger(context.Background(), logger.WithFields(log.Fields{"job": "j1"}))
|
|
||||||
|
|
||||||
require.NoError(t, printCompleteJobName(tt.rc)(ctx))
|
require.NoError(t, printCompleteJobName(tt.rc)(ctx))
|
||||||
|
|
||||||
@@ -269,14 +262,16 @@ func TestNewJobExecutorDownloadsAllActionsBeforeTheFirstStep(t *testing.T) {
|
|||||||
defer sm.AssertExpectations(t)
|
defer sm.AssertExpectations(t)
|
||||||
}
|
}
|
||||||
|
|
||||||
logger, hook := logrustest.NewNullLogger()
|
hook := logtest.New()
|
||||||
err := newJobExecutor(jim, sfm, rc)(common.WithLogger(ctx, logger.WithField("job", "test")))
|
|
||||||
|
logger := logging.New(hook)
|
||||||
|
err := newJobExecutor(jim, sfm, rc)(common.WithLogger(ctx, logger.With("job", "test")))
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
assert.Equal(t, []string{"pre1", "pre2", "step1", "step2"}, executorOrder)
|
assert.Equal(t, []string{"pre1", "pre2", "step1", "step2"}, executorOrder)
|
||||||
|
|
||||||
setup := make([]string, 0)
|
setup := make([]string, 0)
|
||||||
for _, entry := range hook.AllEntries() {
|
for _, entry := range hook.AllRecords() {
|
||||||
if strings.HasPrefix(entry.Message, "Prepare all required actions") || strings.HasPrefix(entry.Message, "Download action") ||
|
if strings.HasPrefix(entry.Message, "Prepare all required actions") || strings.HasPrefix(entry.Message, "Download action") ||
|
||||||
strings.HasPrefix(entry.Message, "Complete job name") {
|
strings.HasPrefix(entry.Message, "Complete job name") {
|
||||||
setup = append(setup, entry.Message)
|
setup = append(setup, entry.Message)
|
||||||
@@ -974,7 +969,8 @@ func TestReadSingleFileFromContainerArchiveFindsMatchingRegularFile(t *testing.T
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestReadSingleFileFromContainerArchiveTruncatesWhenTooLarge(t *testing.T) {
|
func TestReadSingleFileFromContainerArchiveTruncatesWhenTooLarge(t *testing.T) {
|
||||||
logger, hook := logrustest.NewNullLogger()
|
hook := logtest.New()
|
||||||
|
logger := logging.New(hook)
|
||||||
ctx := common.WithLogger(context.Background(), logger)
|
ctx := common.WithLogger(context.Background(), logger)
|
||||||
cm := &containerMock{}
|
cm := &containerMock{}
|
||||||
content := strings.Repeat("a", 300)
|
content := strings.Repeat("a", 300)
|
||||||
@@ -992,14 +988,15 @@ func TestReadSingleFileFromContainerArchiveTruncatesWhenTooLarge(t *testing.T) {
|
|||||||
assert.LessOrEqual(t, len(body), maxBytes)
|
assert.LessOrEqual(t, len(body), maxBytes)
|
||||||
keep := maxBytes - len(jobSummaryTruncationMarker)
|
keep := maxBytes - len(jobSummaryTruncationMarker)
|
||||||
assert.Equal(t, []byte(content[:keep]+jobSummaryTruncationMarker), body)
|
assert.Equal(t, []byte(content[:keep]+jobSummaryTruncationMarker), body)
|
||||||
if assert.Len(t, hook.Entries, 1) {
|
if assert.Len(t, hook.AllRecords(), 1) {
|
||||||
assert.Contains(t, hook.Entries[0].Message, "job summary truncated")
|
assert.Contains(t, hook.AllRecords()[0].Message, "job summary truncated")
|
||||||
}
|
}
|
||||||
cm.AssertExpectations(t)
|
cm.AssertExpectations(t)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestReadSingleFileFromContainerArchiveKeepsExactLimitWithoutWarning(t *testing.T) {
|
func TestReadSingleFileFromContainerArchiveKeepsExactLimitWithoutWarning(t *testing.T) {
|
||||||
logger, hook := logrustest.NewNullLogger()
|
hook := logtest.New()
|
||||||
|
logger := logging.New(hook)
|
||||||
ctx := common.WithLogger(context.Background(), logger)
|
ctx := common.WithLogger(context.Background(), logger)
|
||||||
cm := &containerMock{}
|
cm := &containerMock{}
|
||||||
cm.On("GetContainerArchive", ctx, "/var/run/act/workflow/SUMMARY.md").Return(
|
cm.On("GetContainerArchive", ctx, "/var/run/act/workflow/SUMMARY.md").Return(
|
||||||
@@ -1012,7 +1009,7 @@ func TestReadSingleFileFromContainerArchiveKeepsExactLimitWithoutWarning(t *test
|
|||||||
// A summary that is exactly at the limit is kept whole and not flagged as truncated.
|
// A summary that is exactly at the limit is kept whole and not flagged as truncated.
|
||||||
assert.True(t, ok)
|
assert.True(t, ok)
|
||||||
assert.Equal(t, []byte("abc"), body)
|
assert.Equal(t, []byte("abc"), body)
|
||||||
assert.Empty(t, hook.Entries)
|
assert.Empty(t, hook.AllRecords())
|
||||||
cm.AssertExpectations(t)
|
cm.AssertExpectations(t)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -42,7 +42,7 @@ func (rc *RunContext) runJobHook(ctx context.Context, hookPath, name string) err
|
|||||||
}
|
}
|
||||||
|
|
||||||
cmd, shell := hookCommand(hookPath)
|
cmd, shell := hookCommand(hookPath)
|
||||||
rawLogger := common.Logger(ctx).WithField(rawOutputField, true)
|
rawLogger := common.Logger(ctx).With(rawOutputField, true)
|
||||||
defer rawLogger.Infof("::endgroup::")
|
defer rawLogger.Infof("::endgroup::")
|
||||||
rawLogger.Infof("::group::Run '%s'", EscapeCommandData(hookPath))
|
rawLogger.Infof("::group::Run '%s'", EscapeCommandData(hookPath))
|
||||||
rawLogger.Infof("A %s hook has been configured by the runner administrator", name)
|
rawLogger.Infof("A %s hook has been configured by the runner administrator", name)
|
||||||
|
|||||||
@@ -12,9 +12,10 @@ import (
|
|||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"gitea.com/gitea/runner/act/common"
|
"gitea.com/gitea/runner/act/common"
|
||||||
|
"gitea.com/gitea/runner/act/common/logging"
|
||||||
|
"gitea.com/gitea/runner/act/common/logging/logtest"
|
||||||
|
|
||||||
"gitea.dev/actionslib/pkg/model"
|
"gitea.dev/actionslib/pkg/model"
|
||||||
"github.com/sirupsen/logrus/hooks/test"
|
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
)
|
)
|
||||||
@@ -64,8 +65,8 @@ func newHookRunContext(jobContainer *hookContainer, config *Config) (*RunContext
|
|||||||
Run: &model.Run{JobID: "job", Workflow: &model.Workflow{Jobs: map[string]*model.Job{"job": {}}}},
|
Run: &model.Run{JobID: "job", Workflow: &model.Workflow{Jobs: map[string]*model.Job{"job": {}}}},
|
||||||
JobContainer: jobContainer,
|
JobContainer: jobContainer,
|
||||||
}
|
}
|
||||||
logger, _ := test.NewNullLogger()
|
logger := logging.New(logtest.New())
|
||||||
ctx := common.WithJobErrorContainer(common.WithLogger(context.Background(), logger.WithField("test", true)))
|
ctx := common.WithJobErrorContainer(common.WithLogger(context.Background(), logger.With("test", true)))
|
||||||
rc.ExprEval = rc.NewExpressionEvaluator(ctx)
|
rc.ExprEval = rc.NewExpressionEvaluator(ctx)
|
||||||
return rc, ctx
|
return rc, ctx
|
||||||
}
|
}
|
||||||
|
|||||||
+126
-99
@@ -19,8 +19,8 @@ import (
|
|||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
"gitea.com/gitea/runner/act/common"
|
"gitea.com/gitea/runner/act/common"
|
||||||
|
"gitea.com/gitea/runner/act/common/logging"
|
||||||
|
|
||||||
"github.com/sirupsen/logrus"
|
|
||||||
"golang.org/x/term"
|
"golang.org/x/term"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -73,8 +73,9 @@ func WithMasks(ctx context.Context, masks *[]string) context.Context {
|
|||||||
return context.WithValue(ctx, masksContextKeyVal, masks)
|
return context.WithValue(ctx, masksContextKeyVal, masks)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// JobLoggerFactory supplies the handler a job's own log output goes to, in place of stdout.
|
||||||
type JobLoggerFactory interface {
|
type JobLoggerFactory interface {
|
||||||
WithJobLogger() *logrus.Logger
|
WithJobLogger() logging.Handler
|
||||||
}
|
}
|
||||||
|
|
||||||
type jobLoggerFactoryContextKey string
|
type jobLoggerFactoryContextKey string
|
||||||
@@ -89,84 +90,70 @@ func WithJobLoggerFactory(ctx context.Context, factory JobLoggerFactory) context
|
|||||||
func WithJobLogger(ctx context.Context, jobID, jobName string, config *Config, masks *[]string, matrix map[string]any) context.Context {
|
func WithJobLogger(ctx context.Context, jobID, jobName string, config *Config, masks *[]string, matrix map[string]any) context.Context {
|
||||||
ctx = WithMasks(ctx, masks)
|
ctx = WithMasks(ctx, masks)
|
||||||
|
|
||||||
var logger *logrus.Logger
|
var output logging.Handler
|
||||||
if jobLoggerFactory, ok := ctx.Value(jobLoggerFactoryContextKeyVal).(JobLoggerFactory); ok && jobLoggerFactory != nil {
|
if jobLoggerFactory, ok := ctx.Value(jobLoggerFactoryContextKeyVal).(JobLoggerFactory); ok && jobLoggerFactory != nil {
|
||||||
logger = jobLoggerFactory.WithJobLogger()
|
output = jobLoggerFactory.WithJobLogger()
|
||||||
|
} else if config.JSONLogger {
|
||||||
|
output = newJobLogJSONHandler(os.Stdout)
|
||||||
} else {
|
} else {
|
||||||
var formatter logrus.Formatter
|
mux.Lock()
|
||||||
if config.JSONLogger {
|
nextColor++
|
||||||
formatter = &logrus.JSONFormatter{}
|
color := colors[nextColor%len(colors)]
|
||||||
} else {
|
mux.Unlock()
|
||||||
mux.Lock()
|
output = newJobLogHandler(os.Stdout, color)
|
||||||
defer mux.Unlock()
|
|
||||||
nextColor++
|
|
||||||
formatter = &jobLogFormatter{color: colors[nextColor%len(colors)]}
|
|
||||||
}
|
|
||||||
|
|
||||||
logger = logrus.New()
|
|
||||||
logger.SetOutput(os.Stdout)
|
|
||||||
logger.SetLevel(logrus.GetLevel())
|
|
||||||
logger.SetFormatter(formatter)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
{ // Adapt to Gitea
|
var handlers []logging.Handler
|
||||||
if hook := common.LoggerHook(ctx); hook != nil {
|
if h := common.LoggerHandler(ctx); h != nil { // Adapt to Gitea
|
||||||
logger.AddHook(hook)
|
handlers = append(handlers, h)
|
||||||
}
|
|
||||||
if config.JobLoggerLevel != nil {
|
|
||||||
logger.SetLevel(*config.JobLoggerLevel)
|
|
||||||
} else {
|
|
||||||
logger.SetLevel(logrus.TraceLevel)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
handlers = append(handlers, &maskedHandler{
|
||||||
logger.SetFormatter(&maskedFormatter{
|
Handler: output,
|
||||||
Formatter: logger.Formatter,
|
masker: valueMasker(config.InsecureSecrets, config.maskers()),
|
||||||
masker: valueMasker(config.InsecureSecrets, config.maskers()),
|
|
||||||
})
|
})
|
||||||
rtn := logger.WithFields(logrus.Fields{
|
|
||||||
"job": jobName,
|
level := logging.LevelTrace
|
||||||
"jobID": jobID,
|
if config.JobLoggerLevel != nil {
|
||||||
"dryrun": common.Dryrun(ctx),
|
level = *config.JobLoggerLevel
|
||||||
"matrix": matrix,
|
}
|
||||||
}).WithContext(ctx)
|
|
||||||
|
rtn := logging.New(logging.NewLevelHandler(logging.NewMultiHandler(handlers...), level)).With(
|
||||||
|
"job", jobName,
|
||||||
|
"jobID", jobID,
|
||||||
|
"dryrun", common.Dryrun(ctx),
|
||||||
|
"matrix", matrix,
|
||||||
|
).WithContext(ctx)
|
||||||
|
|
||||||
return common.WithLogger(ctx, rtn)
|
return common.WithLogger(ctx, rtn)
|
||||||
}
|
}
|
||||||
|
|
||||||
func WithCompositeLogger(ctx context.Context, masks *[]string) context.Context {
|
func WithCompositeLogger(ctx context.Context, masks *[]string) context.Context {
|
||||||
ctx = WithMasks(ctx, masks)
|
ctx = WithMasks(ctx, masks)
|
||||||
return common.WithLogger(ctx, common.Logger(ctx).WithFields(logrus.Fields{}).WithContext(ctx))
|
return common.WithLogger(ctx, common.Logger(ctx).WithContext(ctx))
|
||||||
}
|
}
|
||||||
|
|
||||||
func WithCompositeStepLogger(ctx context.Context, stepID string) context.Context {
|
func WithCompositeStepLogger(ctx context.Context, stepID string) context.Context {
|
||||||
val := common.Logger(ctx)
|
stepIDs := append(slices.Clone(common.StepIDs(ctx)), stepID)
|
||||||
stepIDs := make([]string, 0)
|
ctx = common.WithStepIDs(ctx, stepIDs)
|
||||||
|
|
||||||
if logger, ok := val.(*logrus.Entry); ok {
|
return common.WithLogger(ctx, common.Logger(ctx).With("stepID", stepIDs).WithContext(ctx))
|
||||||
if oldStepIDs, ok := logger.Data["stepID"].([]string); ok {
|
|
||||||
stepIDs = append(stepIDs, oldStepIDs...)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
stepIDs = append(stepIDs, stepID)
|
|
||||||
|
|
||||||
return common.WithLogger(ctx, common.Logger(ctx).WithFields(logrus.Fields{
|
|
||||||
"stepID": stepIDs,
|
|
||||||
}).WithContext(ctx))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func withStepLogger(ctx context.Context, stepNumber int, stepID, stepName, stageName string) context.Context {
|
func withStepLogger(ctx context.Context, stepNumber int, stepID, stepName, stageName string) context.Context {
|
||||||
rtn := common.Logger(ctx).WithFields(logrus.Fields{
|
stepIDs := []string{stepID}
|
||||||
"stepNumber": stepNumber,
|
ctx = common.WithStepIDs(ctx, stepIDs)
|
||||||
"step": stepName,
|
|
||||||
"stepID": []string{stepID},
|
rtn := common.Logger(ctx).With(
|
||||||
"stage": stageName,
|
"stepNumber", stepNumber,
|
||||||
})
|
"step", stepName,
|
||||||
|
"stepID", stepIDs,
|
||||||
|
"stage", stageName,
|
||||||
|
)
|
||||||
return common.WithLogger(ctx, rtn)
|
return common.WithLogger(ctx, rtn)
|
||||||
}
|
}
|
||||||
|
|
||||||
type entryProcessor func(entry *logrus.Entry) *logrus.Entry
|
// entryProcessor rewrites a record before the output handler sees it.
|
||||||
|
type entryProcessor func(ctx context.Context, r logging.Record) logging.Record
|
||||||
|
|
||||||
var secretValueEncoders = []func(string) string{
|
var secretValueEncoders = []func(string) string{
|
||||||
func(v string) string { return base64.StdEncoding.EncodeToString([]byte(v)) },
|
func(v string) string { return base64.StdEncoding.EncodeToString([]byte(v)) },
|
||||||
@@ -321,16 +308,16 @@ func valueMasker(insecureSecrets bool, oldnew []string) entryProcessor {
|
|||||||
replacer *strings.Replacer
|
replacer *strings.Replacer
|
||||||
)
|
)
|
||||||
|
|
||||||
return func(entry *logrus.Entry) *logrus.Entry {
|
return func(ctx context.Context, r logging.Record) logging.Record {
|
||||||
if insecureSecrets {
|
if insecureSecrets {
|
||||||
return entry
|
return r
|
||||||
}
|
}
|
||||||
|
|
||||||
masks := Masks(entry.Context)
|
masks := Masks(ctx)
|
||||||
|
|
||||||
if len(*masks) == 0 {
|
if len(*masks) == 0 {
|
||||||
entry.Message = defReplacer.Replace(entry.Message)
|
r.Message = defReplacer.Replace(r.Message)
|
||||||
return entry
|
return r
|
||||||
}
|
}
|
||||||
|
|
||||||
mu.Lock()
|
mu.Lock()
|
||||||
@@ -349,89 +336,129 @@ func valueMasker(insecureSecrets bool, oldnew []string) entryProcessor {
|
|||||||
cmasker := replacer
|
cmasker := replacer
|
||||||
mu.Unlock()
|
mu.Unlock()
|
||||||
|
|
||||||
entry.Message = cmasker.Replace(entry.Message)
|
r.Message = cmasker.Replace(r.Message)
|
||||||
|
|
||||||
return entry
|
return r
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
type maskedFormatter struct {
|
type maskedHandler struct {
|
||||||
logrus.Formatter
|
logging.Handler
|
||||||
masker entryProcessor
|
masker entryProcessor
|
||||||
}
|
}
|
||||||
|
|
||||||
func (f *maskedFormatter) Format(entry *logrus.Entry) ([]byte, error) {
|
func (m *maskedHandler) Handle(ctx context.Context, r logging.Record) error {
|
||||||
return f.Formatter.Format(f.masker(entry))
|
return m.Handler.Handle(ctx, m.masker(ctx, r))
|
||||||
}
|
}
|
||||||
|
|
||||||
type jobLogFormatter struct {
|
func (m *maskedHandler) WithAttrs(attrs []logging.Attr) logging.Handler {
|
||||||
|
return &maskedHandler{Handler: m.Handler.WithAttrs(attrs), masker: m.masker}
|
||||||
|
}
|
||||||
|
|
||||||
|
// jobLogHandler writes a job's log to w, one record per line in the shape format gives it.
|
||||||
|
type jobLogHandler struct {
|
||||||
|
w io.Writer
|
||||||
|
mu *sync.Mutex // shared by every handler derived with WithAttrs, as they share w
|
||||||
|
attrs []logging.Attr
|
||||||
|
format func(logging.Record) ([]byte, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
// newJobLogHandler writes readable text.
|
||||||
|
func newJobLogHandler(w io.Writer, color int) *jobLogHandler {
|
||||||
|
return &jobLogHandler{w: w, mu: &sync.Mutex{}, format: textFormatter{w: w, color: color}.format}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *jobLogHandler) Enabled(context.Context, logging.Level) bool { return true }
|
||||||
|
|
||||||
|
func (h *jobLogHandler) WithAttrs(attrs []logging.Attr) logging.Handler {
|
||||||
|
c := *h
|
||||||
|
c.attrs = slices.Concat(h.attrs, attrs)
|
||||||
|
return &c
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *jobLogHandler) Handle(_ context.Context, r logging.Record) error {
|
||||||
|
r.Attrs = slices.Concat(h.attrs, r.Attrs)
|
||||||
|
line, err := h.format(r)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
h.mu.Lock()
|
||||||
|
defer h.mu.Unlock()
|
||||||
|
_, err = h.w.Write(line)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
type textFormatter struct {
|
||||||
|
w io.Writer
|
||||||
color int
|
color int
|
||||||
}
|
}
|
||||||
|
|
||||||
func (f *jobLogFormatter) Format(entry *logrus.Entry) ([]byte, error) {
|
func (f textFormatter) format(r logging.Record) ([]byte, error) {
|
||||||
b := &bytes.Buffer{}
|
b := &bytes.Buffer{}
|
||||||
|
|
||||||
// the web renderer decodes command data, so this local view has to as well
|
// the web renderer decodes command data, so this local view has to as well
|
||||||
if _, _, _, ok := tryParseRawActionCommand(entry.Message + "\n"); ok {
|
if _, _, _, ok := tryParseRawActionCommand(r.Message + "\n"); ok {
|
||||||
entry.Message = UnescapeCommandData(entry.Message)
|
r.Message = UnescapeCommandData(r.Message)
|
||||||
}
|
}
|
||||||
|
r.Message = strings.TrimSuffix(r.Message, "\n")
|
||||||
|
|
||||||
if f.isColored(entry) {
|
if f.isColored() {
|
||||||
f.printColored(b, entry)
|
f.printColored(b, r)
|
||||||
} else {
|
} else {
|
||||||
f.print(b, entry)
|
f.print(b, r)
|
||||||
}
|
}
|
||||||
|
|
||||||
b.WriteByte('\n')
|
b.WriteByte('\n')
|
||||||
return b.Bytes(), nil
|
return b.Bytes(), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (f *jobLogFormatter) printColored(b *bytes.Buffer, entry *logrus.Entry) {
|
func attrIsTrue(r logging.Record, key string) bool {
|
||||||
entry.Message = strings.TrimSuffix(entry.Message, "\n")
|
v, _ := r.Attr(key)
|
||||||
|
return v == true
|
||||||
|
}
|
||||||
|
|
||||||
job := entry.Data["job"]
|
func (f textFormatter) printColored(b *bytes.Buffer, r logging.Record) {
|
||||||
|
job, _ := r.Attr("job")
|
||||||
|
|
||||||
debugFlag := ""
|
debugFlag := ""
|
||||||
if entry.Level == logrus.DebugLevel {
|
if r.Level == logging.LevelDebug {
|
||||||
debugFlag = "[DEBUG] "
|
debugFlag = "[DEBUG] "
|
||||||
}
|
}
|
||||||
|
|
||||||
switch {
|
switch {
|
||||||
case entry.Data[rawOutputField] == true:
|
case attrIsTrue(r, rawOutputField):
|
||||||
if entry.Data[scriptLineCyanField] == true {
|
if attrIsTrue(r, scriptLineCyanField) {
|
||||||
fmt.Fprintf(b, "\x1b[%dm|\x1b[0m \x1b[36;1m%s\x1b[0m", f.color, entry.Message)
|
fmt.Fprintf(b, "\x1b[%dm|\x1b[0m \x1b[36;1m%s\x1b[0m", f.color, r.Message)
|
||||||
} else {
|
} else {
|
||||||
fmt.Fprintf(b, "\x1b[%dm|\x1b[0m %s", f.color, entry.Message)
|
fmt.Fprintf(b, "\x1b[%dm|\x1b[0m %s", f.color, r.Message)
|
||||||
}
|
}
|
||||||
case entry.Data["dryrun"] == true:
|
case attrIsTrue(r, "dryrun"):
|
||||||
fmt.Fprintf(b, "\x1b[1m\x1b[%dm\x1b[7m*DRYRUN*\x1b[0m \x1b[%dm[%s] \x1b[0m%s%s", gray, f.color, job, debugFlag, entry.Message)
|
fmt.Fprintf(b, "\x1b[1m\x1b[%dm\x1b[7m*DRYRUN*\x1b[0m \x1b[%dm[%s] \x1b[0m%s%s", gray, f.color, job, debugFlag, r.Message)
|
||||||
default:
|
default:
|
||||||
fmt.Fprintf(b, "\x1b[%dm[%s] \x1b[0m%s%s", f.color, job, debugFlag, entry.Message)
|
fmt.Fprintf(b, "\x1b[%dm[%s] \x1b[0m%s%s", f.color, job, debugFlag, r.Message)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (f *jobLogFormatter) print(b *bytes.Buffer, entry *logrus.Entry) {
|
func (f textFormatter) print(b *bytes.Buffer, r logging.Record) {
|
||||||
entry.Message = strings.TrimSuffix(entry.Message, "\n")
|
job, _ := r.Attr("job")
|
||||||
|
|
||||||
job := entry.Data["job"]
|
|
||||||
|
|
||||||
debugFlag := ""
|
debugFlag := ""
|
||||||
if entry.Level == logrus.DebugLevel {
|
if r.Level == logging.LevelDebug {
|
||||||
debugFlag = "[DEBUG] "
|
debugFlag = "[DEBUG] "
|
||||||
}
|
}
|
||||||
|
|
||||||
switch {
|
switch {
|
||||||
case entry.Data[rawOutputField] == true:
|
case attrIsTrue(r, rawOutputField):
|
||||||
fmt.Fprintf(b, "[%s] | %s", job, entry.Message)
|
fmt.Fprintf(b, "[%s] | %s", job, r.Message)
|
||||||
case entry.Data["dryrun"] == true:
|
case attrIsTrue(r, "dryrun"):
|
||||||
fmt.Fprintf(b, "*DRYRUN* [%s] %s%s", job, debugFlag, entry.Message)
|
fmt.Fprintf(b, "*DRYRUN* [%s] %s%s", job, debugFlag, r.Message)
|
||||||
default:
|
default:
|
||||||
fmt.Fprintf(b, "[%s] %s%s", job, debugFlag, entry.Message)
|
fmt.Fprintf(b, "[%s] %s%s", job, debugFlag, r.Message)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (f *jobLogFormatter) isColored(entry *logrus.Entry) bool {
|
func (f textFormatter) isColored() bool {
|
||||||
isColored := checkIfTerminal(entry.Logger.Out)
|
isColored := checkIfTerminal(f.w)
|
||||||
|
|
||||||
if force, ok := os.LookupEnv("CLICOLOR_FORCE"); ok && force != "0" {
|
if force, ok := os.LookupEnv("CLICOLOR_FORCE"); ok && force != "0" {
|
||||||
isColored = true
|
isColored = true
|
||||||
|
|||||||
@@ -0,0 +1,52 @@
|
|||||||
|
// Copyright 2026 The Gitea Authors. All rights reserved.
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
package runner
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"gitea.com/gitea/runner/act/common/logging"
|
||||||
|
)
|
||||||
|
|
||||||
|
// newJobLogJSONHandler writes one JSON object per record, in the shape of logrus's JSONFormatter.
|
||||||
|
func newJobLogJSONHandler(w io.Writer) *jobLogHandler {
|
||||||
|
return &jobLogHandler{w: w, mu: &sync.Mutex{}, format: formatJSON}
|
||||||
|
}
|
||||||
|
|
||||||
|
func formatJSON(r logging.Record) ([]byte, error) {
|
||||||
|
data := make(map[string]any, len(r.Attrs)+3)
|
||||||
|
for _, a := range r.Attrs {
|
||||||
|
switch v := a.Value.(type) {
|
||||||
|
case error:
|
||||||
|
data[a.Key] = v.Error() // an error marshals to {}
|
||||||
|
default:
|
||||||
|
data[a.Key] = v
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for _, key := range []string{"time", "msg", "level"} {
|
||||||
|
if v, ok := data[key]; ok { // keep the fixed keys from being overwritten
|
||||||
|
data["fields."+key] = v
|
||||||
|
}
|
||||||
|
}
|
||||||
|
data["time"] = r.Time.Format(time.RFC3339)
|
||||||
|
data["msg"] = r.Message
|
||||||
|
data["level"] = jsonLevel(r.Level)
|
||||||
|
|
||||||
|
line, err := json.Marshal(data)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to marshal fields to JSON, %w", err)
|
||||||
|
}
|
||||||
|
return append(line, '\n'), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func jsonLevel(l logging.Level) string {
|
||||||
|
if l.String() == "warn" {
|
||||||
|
return "warning" // what logrus calls it
|
||||||
|
}
|
||||||
|
return l.String()
|
||||||
|
}
|
||||||
+64
-15
@@ -4,13 +4,19 @@
|
|||||||
package runner
|
package runner
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
"encoding/base64"
|
"encoding/base64"
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
"io"
|
"io"
|
||||||
"net/url"
|
"net/url"
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/sirupsen/logrus"
|
"gitea.com/gitea/runner/act/common"
|
||||||
|
"gitea.com/gitea/runner/act/common/logging"
|
||||||
|
"gitea.com/gitea/runner/act/common/logging/logtest"
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
)
|
)
|
||||||
@@ -49,10 +55,7 @@ func TestValueMasker(t *testing.T) {
|
|||||||
ctx := WithMasks(t.Context(), &entry.masks)
|
ctx := WithMasks(t.Context(), &entry.masks)
|
||||||
masker := valueMasker(false, AppendSecretMaskers(nil, entry.secrets))
|
masker := valueMasker(false, AppendSecretMaskers(nil, entry.secrets))
|
||||||
for line := range strings.SplitSeq(entry.lines, "\n") {
|
for line := range strings.SplitSeq(entry.lines, "\n") {
|
||||||
lentry := masker(&logrus.Entry{
|
lentry := masker(ctx, logging.Record{Message: line})
|
||||||
Context: ctx,
|
|
||||||
Message: line,
|
|
||||||
})
|
|
||||||
for _, line := range entry.disallowed {
|
for _, line := range entry.disallowed {
|
||||||
assert.NotContains(t, lentry.Message, line)
|
assert.NotContains(t, lentry.Message, line)
|
||||||
}
|
}
|
||||||
@@ -79,7 +82,7 @@ func TestValueMaskerEncodedSecrets(t *testing.T) {
|
|||||||
} {
|
} {
|
||||||
t.Run(tc.name, func(t *testing.T) {
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
masker := valueMasker(false, AppendSecretMaskers(nil, map[string]string{"TOKEN": tc.secret}))
|
masker := valueMasker(false, AppendSecretMaskers(nil, map[string]string{"TOKEN": tc.secret}))
|
||||||
entry := masker(&logrus.Entry{Context: t.Context(), Message: strings.Join(tc.encoded, " ")})
|
entry := masker(t.Context(), logging.Record{Message: strings.Join(tc.encoded, " ")})
|
||||||
|
|
||||||
assert.Contains(t, entry.Message, "***")
|
assert.Contains(t, entry.Message, "***")
|
||||||
for _, disallowed := range tc.encoded {
|
for _, disallowed := range tc.encoded {
|
||||||
@@ -105,7 +108,7 @@ func TestValueMaskerJSONEscapesBothWays(t *testing.T) {
|
|||||||
{"literal (JS JSON.stringify / .NET)", jsonStringEscapeNoHTML(secret)},
|
{"literal (JS JSON.stringify / .NET)", jsonStringEscapeNoHTML(secret)},
|
||||||
} {
|
} {
|
||||||
t.Run(tc.name, func(t *testing.T) {
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
entry := masker(&logrus.Entry{Context: t.Context(), Message: `{"t":"` + tc.form + `"}`})
|
entry := masker(t.Context(), logging.Record{Message: `{"t":"` + tc.form + `"}`})
|
||||||
|
|
||||||
assert.Contains(t, entry.Message, "***")
|
assert.Contains(t, entry.Message, "***")
|
||||||
assert.NotContains(t, entry.Message, tc.form)
|
assert.NotContains(t, entry.Message, tc.form)
|
||||||
@@ -118,7 +121,7 @@ func TestValueMaskerJSONEscapesBothWays(t *testing.T) {
|
|||||||
func TestValueMaskerHidesExtraMasks(t *testing.T) {
|
func TestValueMaskerHidesExtraMasks(t *testing.T) {
|
||||||
masker := valueMasker(false, (&Config{ExtraMasks: []string{"pr0xypw"}}).maskers())
|
masker := valueMasker(false, (&Config{ExtraMasks: []string{"pr0xypw"}}).maskers())
|
||||||
|
|
||||||
entry := masker(&logrus.Entry{Context: t.Context(), Message: "proxy is http://user:pr0xypw@proxy:3128"})
|
entry := masker(t.Context(), logging.Record{Message: "proxy is http://user:pr0xypw@proxy:3128"})
|
||||||
|
|
||||||
assert.Equal(t, "proxy is http://user:***@proxy:3128", entry.Message)
|
assert.Equal(t, "proxy is http://user:***@proxy:3128", entry.Message)
|
||||||
}
|
}
|
||||||
@@ -137,7 +140,7 @@ func TestValueMaskerEncodedMasks(t *testing.T) {
|
|||||||
{"encoded: " + base64.StdEncoding.EncodeToString([]byte("s3cr3t")), "encoded: ***"},
|
{"encoded: " + base64.StdEncoding.EncodeToString([]byte("s3cr3t")), "encoded: ***"},
|
||||||
{"encoded: " + base64.StdEncoding.EncodeToString([]byte("first")), "encoded: ***"},
|
{"encoded: " + base64.StdEncoding.EncodeToString([]byte("first")), "encoded: ***"},
|
||||||
} {
|
} {
|
||||||
entry := masker(&logrus.Entry{Context: WithMasks(t.Context(), &masks), Message: tc.line})
|
entry := masker(WithMasks(t.Context(), &masks), logging.Record{Message: tc.line})
|
||||||
assert.Equal(t, tc.want, entry.Message, tc.line)
|
assert.Equal(t, tc.want, entry.Message, tc.line)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -154,7 +157,7 @@ func TestValueMaskerBase64Alignments(t *testing.T) {
|
|||||||
for _, prefix := range []string{"x-access-token:", "user:", "ab:"} {
|
for _, prefix := range []string{"x-access-token:", "user:", "ab:"} {
|
||||||
t.Run(prefix, func(t *testing.T) {
|
t.Run(prefix, func(t *testing.T) {
|
||||||
encoded := base64.StdEncoding.EncodeToString([]byte(prefix + secret))
|
encoded := base64.StdEncoding.EncodeToString([]byte(prefix + secret))
|
||||||
entry := masker(&logrus.Entry{Context: t.Context(), Message: "Authorization: Basic " + encoded})
|
entry := masker(t.Context(), logging.Record{Message: "Authorization: Basic " + encoded})
|
||||||
|
|
||||||
assert.Contains(t, entry.Message, "***")
|
assert.Contains(t, entry.Message, "***")
|
||||||
assert.NotEqual(t, "Authorization: Basic "+encoded, entry.Message)
|
assert.NotEqual(t, "Authorization: Basic "+encoded, entry.Message)
|
||||||
@@ -172,7 +175,7 @@ func TestValueMaskerBase64Alignments(t *testing.T) {
|
|||||||
func TestValueMaskerCachedReplacerSeesNewMasks(t *testing.T) {
|
func TestValueMaskerCachedReplacerSeesNewMasks(t *testing.T) {
|
||||||
masker := valueMasker(false, AppendSecretMaskers(nil, map[string]string{"TOKEN": "secret-token"}))
|
masker := valueMasker(false, AppendSecretMaskers(nil, map[string]string{"TOKEN": "secret-token"}))
|
||||||
mask := func(masks *[]string, message string) string {
|
mask := func(masks *[]string, message string) string {
|
||||||
return masker(&logrus.Entry{Context: WithMasks(t.Context(), masks), Message: message}).Message
|
return masker(WithMasks(t.Context(), masks), logging.Record{Message: message}).Message
|
||||||
}
|
}
|
||||||
|
|
||||||
job := []string{"first mask"}
|
job := []string{"first mask"}
|
||||||
@@ -206,15 +209,61 @@ func TestAppendSecretMaskerSkipsUselessEncodings(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestJobLogFormatterDecodesCommandData(t *testing.T) {
|
func TestJobLogFormatterDecodesCommandData(t *testing.T) {
|
||||||
logger := logrus.New()
|
|
||||||
logger.Out = io.Discard
|
|
||||||
format := func(message string) string {
|
format := func(message string) string {
|
||||||
out, err := (&jobLogFormatter{}).Format(&logrus.Entry{Logger: logger, Message: message, Data: logrus.Fields{rawOutputField: true}})
|
line, err := textFormatter{w: io.Discard}.format(logging.Record{Message: message, Attrs: []logging.Attr{{Key: rawOutputField, Value: true}}})
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
return string(out)
|
return string(line)
|
||||||
}
|
}
|
||||||
|
|
||||||
assert.Contains(t, format("##[error]deploy 50%25 traffic"), "##[error]deploy 50% traffic")
|
assert.Contains(t, format("##[error]deploy 50%25 traffic"), "##[error]deploy 50% traffic")
|
||||||
// a plain line is not command data and keeps its literal escapes
|
// a plain line is not command data and keeps its literal escapes
|
||||||
assert.Contains(t, format("progress 50%25 done"), "progress 50%25 done")
|
assert.Contains(t, format("progress 50%25 done"), "progress 50%25 done")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type factoryFunc func() logging.Handler
|
||||||
|
|
||||||
|
func (f factoryFunc) WithJobLogger() logging.Handler { return f() }
|
||||||
|
|
||||||
|
func TestWithJobLoggerFansOutAndMasksOnlyOutput(t *testing.T) {
|
||||||
|
output, hook := logtest.NewWithLevel(logging.LevelTrace), logtest.NewWithLevel(logging.LevelTrace)
|
||||||
|
ctx := WithJobLoggerFactory(t.Context(), factoryFunc(func() logging.Handler { return output }))
|
||||||
|
ctx = common.WithLoggerHandler(ctx, hook)
|
||||||
|
level := logging.LevelInfo
|
||||||
|
cfg := &Config{Secrets: map[string]string{"TOKEN": "s3cr3t-value"}, JobLoggerLevel: &level}
|
||||||
|
|
||||||
|
ctx = WithJobLogger(ctx, "j1", "build", cfg, &[]string{}, nil)
|
||||||
|
ctx = withStepLogger(ctx, 2, "s2", "step two", "Main")
|
||||||
|
ctx = WithCompositeStepLogger(ctx, "inner")
|
||||||
|
common.Logger(ctx).Infof("token is %s", "s3cr3t-value")
|
||||||
|
common.Logger(ctx).Debugf("below the job logger level")
|
||||||
|
|
||||||
|
// the reporter masks for itself, so its record keeps the original
|
||||||
|
require.Len(t, hook.AllRecords(), 1)
|
||||||
|
assert.Equal(t, "token is s3cr3t-value", hook.LastRecord().Message)
|
||||||
|
require.Len(t, output.AllRecords(), 1)
|
||||||
|
assert.Equal(t, "token is ***", output.LastRecord().Message)
|
||||||
|
|
||||||
|
for _, want := range []struct {
|
||||||
|
key string
|
||||||
|
val any
|
||||||
|
}{{"job", "build"}, {"jobID", "j1"}, {"stepNumber", 2}, {"stage", "Main"}, {"stepID", []string{"s2", "inner"}}} {
|
||||||
|
got, ok := hook.LastRecord().Attr(want.key)
|
||||||
|
assert.True(t, ok, want.key)
|
||||||
|
assert.Equal(t, want.val, got, want.key)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestJobLogJSONHandler(t *testing.T) {
|
||||||
|
buf := &bytes.Buffer{}
|
||||||
|
logger := logging.New(newJobLogJSONHandler(buf)).With("job", "build", "msg", "clash").WithError(errors.New("boom"))
|
||||||
|
logger.Warnf("careful %d", 1)
|
||||||
|
|
||||||
|
var got map[string]any
|
||||||
|
require.NoError(t, json.Unmarshal(buf.Bytes(), &got))
|
||||||
|
assert.Equal(t, "careful 1", got["msg"])
|
||||||
|
assert.Equal(t, "warning", got["level"])
|
||||||
|
assert.Equal(t, "build", got["job"])
|
||||||
|
assert.Equal(t, "boom", got["error"])
|
||||||
|
assert.Equal(t, "clash", got["fields.msg"])
|
||||||
|
assert.NotEmpty(t, got["time"])
|
||||||
|
}
|
||||||
@@ -193,7 +193,7 @@ func setReusedWorkflowCallerResult(rc *RunContext, runner *runnerImpl) common.Ex
|
|||||||
unlock := lockJob(rc.Run.Job())
|
unlock := lockJob(rc.Run.Job())
|
||||||
rc.result(reusedWorkflowJobResult)
|
rc.result(reusedWorkflowJobResult)
|
||||||
unlock()
|
unlock()
|
||||||
common.Logger(ctx).WithField("jobResult", reusedWorkflowJobResult).Infof("Job %s", reusedWorkflowJobResultMessage)
|
common.Logger(ctx).With("jobResult", reusedWorkflowJobResult).Infof("Job %s", reusedWorkflowJobResultMessage)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -511,7 +511,7 @@ func (rc *RunContext) startKubernetesPod() common.Executor {
|
|||||||
// printStartJobContainerGroup mirrors actions/runner's "Starting job container"
|
// printStartJobContainerGroup mirrors actions/runner's "Starting job container"
|
||||||
// section: emit the group header and summary, return a closer for ::endgroup::.
|
// section: emit the group header and summary, return a closer for ::endgroup::.
|
||||||
func printStartJobContainerGroup(ctx context.Context, image, name, network string) func() {
|
func printStartJobContainerGroup(ctx context.Context, image, name, network string) func() {
|
||||||
rawLogger := common.Logger(ctx).WithField(rawOutputField, true)
|
rawLogger := common.Logger(ctx).With(rawOutputField, true)
|
||||||
rawLogger.Infof("::group::Starting job container")
|
rawLogger.Infof("::group::Starting job container")
|
||||||
rawLogger.Infof("image: %s", image)
|
rawLogger.Infof("image: %s", image)
|
||||||
rawLogger.Infof("name: %s", name)
|
rawLogger.Infof("name: %s", name)
|
||||||
@@ -706,7 +706,7 @@ func (rc *RunContext) commandLogWriter(ctx context.Context) io.Writer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func rawLogLine(ctx context.Context) common.LineHandler {
|
func rawLogLine(ctx context.Context) common.LineHandler {
|
||||||
rawLogger := common.Logger(ctx).WithField(rawOutputField, true)
|
rawLogger := common.Logger(ctx).With(rawOutputField, true)
|
||||||
return func(line string) bool {
|
return func(line string) bool {
|
||||||
rawLogger.Infof("%s", line)
|
rawLogger.Infof("%s", line)
|
||||||
return true
|
return true
|
||||||
@@ -957,7 +957,7 @@ func (rc *RunContext) waitForServiceContainers() common.Executor {
|
|||||||
// waitUntilHealthy waits on the healthcheck alone, so a container that declares none is
|
// waitUntilHealthy waits on the healthcheck alone, so a container that declares none is
|
||||||
// ready at once and one that exited is left to the steps that need it.
|
// ready at once and one that exited is left to the steps that need it.
|
||||||
func (svc *serviceContainer) waitUntilHealthy(ctx context.Context, timeout time.Duration) error {
|
func (svc *serviceContainer) waitUntilHealthy(ctx context.Context, timeout time.Duration) error {
|
||||||
rawLogger := common.Logger(ctx).WithField(rawOutputField, true)
|
rawLogger := common.Logger(ctx).With(rawOutputField, true)
|
||||||
loggedStarting := false
|
loggedStarting := false
|
||||||
|
|
||||||
ctx, cancel := context.WithTimeout(ctx, timeout)
|
ctx, cancel := context.WithTimeout(ctx, timeout)
|
||||||
@@ -1015,7 +1015,7 @@ const serviceLogsTimeout = 15 * time.Second // cleanup gets one minute in total
|
|||||||
func (rc *RunContext) printServiceLogs(ctx context.Context) {
|
func (rc *RunContext) printServiceLogs(ctx context.Context) {
|
||||||
ctx, cancel := context.WithTimeout(ctx, serviceLogsTimeout)
|
ctx, cancel := context.WithTimeout(ctx, serviceLogsTimeout)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
rawLogger := common.Logger(ctx).WithField(rawOutputField, true)
|
rawLogger := common.Logger(ctx).With(rawOutputField, true)
|
||||||
for _, svc := range rc.serviceContainers {
|
for _, svc := range rc.serviceContainers {
|
||||||
if !svc.created || svc.logsDumped {
|
if !svc.created || svc.logsDumped {
|
||||||
continue
|
continue
|
||||||
@@ -1338,7 +1338,7 @@ func (rc *RunContext) isEnabled(ctx context.Context) (bool, error) {
|
|||||||
rc.caller.setReusedWorkflowJobResult(rc.Run.JobID, "skipped")
|
rc.caller.setReusedWorkflowJobResult(rc.Run.JobID, "skipped")
|
||||||
return false, nil
|
return false, nil
|
||||||
}
|
}
|
||||||
l.WithField("jobResult", "skipped").Debugf("Skipping job '%s' due to '%s'", job.Name, job.If.Value)
|
l.With("jobResult", "skipped").Debugf("Skipping job '%s' due to '%s'", job.Name, job.If.Value)
|
||||||
return false, nil
|
return false, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -25,14 +25,14 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"gitea.com/gitea/runner/act/common"
|
"gitea.com/gitea/runner/act/common"
|
||||||
|
"gitea.com/gitea/runner/act/common/logging"
|
||||||
|
"gitea.com/gitea/runner/act/common/logging/logtest"
|
||||||
"gitea.com/gitea/runner/act/container"
|
"gitea.com/gitea/runner/act/container"
|
||||||
|
|
||||||
"gitea.dev/actionslib/pkg/exprparser"
|
"gitea.dev/actionslib/pkg/exprparser"
|
||||||
"gitea.dev/actionslib/pkg/model"
|
"gitea.dev/actionslib/pkg/model"
|
||||||
"github.com/docker/cli/cli/compose/loader"
|
"github.com/docker/cli/cli/compose/loader"
|
||||||
"github.com/moby/moby/api/types/volume"
|
"github.com/moby/moby/api/types/volume"
|
||||||
log "github.com/sirupsen/logrus"
|
|
||||||
logrustest "github.com/sirupsen/logrus/hooks/test"
|
|
||||||
assert "github.com/stretchr/testify/assert"
|
assert "github.com/stretchr/testify/assert"
|
||||||
"github.com/stretchr/testify/mock"
|
"github.com/stretchr/testify/mock"
|
||||||
require "github.com/stretchr/testify/require"
|
require "github.com/stretchr/testify/require"
|
||||||
@@ -899,7 +899,7 @@ func TestInterpolateOutputsFromExpression(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestGetGitHubContext(t *testing.T) {
|
func TestGetGitHubContext(t *testing.T) {
|
||||||
log.SetLevel(log.DebugLevel)
|
logging.SetLevel(logging.LevelDebug)
|
||||||
|
|
||||||
cwd, err := os.Getwd()
|
cwd, err := os.Getwd()
|
||||||
assert.NoError(t, err) //nolint:testifylint // pre-existing issue from nektos/act
|
assert.NoError(t, err) //nolint:testifylint // pre-existing issue from nektos/act
|
||||||
@@ -929,7 +929,7 @@ func TestGetGitHubContext(t *testing.T) {
|
|||||||
|
|
||||||
ghc := rc.getGithubContext(context.Background())
|
ghc := rc.getGithubContext(context.Background())
|
||||||
|
|
||||||
log.Debugf("%v", ghc)
|
logging.Debugf("%v", ghc)
|
||||||
|
|
||||||
actor := "nektos/act"
|
actor := "nektos/act"
|
||||||
if a := os.Getenv("ACT_ACTOR"); a != "" {
|
if a := os.Getenv("ACT_ACTOR"); a != "" {
|
||||||
@@ -1060,7 +1060,7 @@ func createJob(t *testing.T, input, result string) *model.Job {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestRunContextRunsOnPlatformNames(t *testing.T) {
|
func TestRunContextRunsOnPlatformNames(t *testing.T) {
|
||||||
log.SetLevel(log.DebugLevel)
|
logging.SetLevel(logging.LevelDebug)
|
||||||
assertObject := assert.New(t)
|
assertObject := assert.New(t)
|
||||||
|
|
||||||
rc := createIfTestRunContext(map[string]*model.Job{
|
rc := createIfTestRunContext(map[string]*model.Job{
|
||||||
@@ -1107,7 +1107,7 @@ func TestRunContextRunsOnPlatformNames(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestRunContextIsEnabled(t *testing.T) {
|
func TestRunContextIsEnabled(t *testing.T) {
|
||||||
log.SetLevel(log.DebugLevel)
|
logging.SetLevel(logging.LevelDebug)
|
||||||
assertObject := assert.New(t)
|
assertObject := assert.New(t)
|
||||||
|
|
||||||
// success()
|
// success()
|
||||||
@@ -1306,11 +1306,8 @@ func TestCreateContainerNameBoundedForLongMatrixInput(t *testing.T) {
|
|||||||
|
|
||||||
func TestPrintStartJobContainerGroupGolden(t *testing.T) {
|
func TestPrintStartJobContainerGroupGolden(t *testing.T) {
|
||||||
buf := &bytes.Buffer{}
|
buf := &bytes.Buffer{}
|
||||||
logger := log.New()
|
logger := logging.New(logging.NewLevelHandler(newJobLogHandler(buf, cyan), logging.LevelInfo))
|
||||||
logger.SetOutput(buf)
|
entry := logger.With("job", "j1")
|
||||||
logger.SetLevel(log.InfoLevel)
|
|
||||||
logger.SetFormatter(&jobLogFormatter{color: cyan})
|
|
||||||
entry := logger.WithFields(log.Fields{"job": "j1"})
|
|
||||||
ctx := common.WithLogger(context.Background(), entry)
|
ctx := common.WithLogger(context.Background(), entry)
|
||||||
|
|
||||||
printStartJobContainerGroup(ctx, "node:20", "GITEA-WORKFLOW-build-JOB-test", "gitea-runner-network")()
|
printStartJobContainerGroup(ctx, "node:20", "GITEA-WORKFLOW-build-JOB-test", "gitea-runner-network")()
|
||||||
@@ -1402,7 +1399,8 @@ func TestRunContext_cleanupFailedStart(t *testing.T) {
|
|||||||
Run: &model.Run{JobID: "job", Workflow: workflow},
|
Run: &model.Run{JobID: "job", Workflow: workflow},
|
||||||
platformImage: "node:20",
|
platformImage: "node:20",
|
||||||
}
|
}
|
||||||
logger, hook := logrustest.NewNullLogger()
|
hook := logtest.New()
|
||||||
|
logger := logging.New(hook)
|
||||||
ctx := common.WithLogger(common.WithDryrun(t.Context(), true), logger)
|
ctx := common.WithLogger(common.WithDryrun(t.Context(), true), logger)
|
||||||
rc.ExprEval = rc.NewExpressionEvaluator(ctx)
|
rc.ExprEval = rc.NewExpressionEvaluator(ctx)
|
||||||
err = rc.startContainer().Then(rc.stopContainer()).Finally(rc.closeContainer())(ctx)
|
err = rc.startContainer().Then(rc.stopContainer()).Finally(rc.closeContainer())(ctx)
|
||||||
@@ -1423,7 +1421,7 @@ func TestRunContext_cleanupFailedStart(t *testing.T) {
|
|||||||
}
|
}
|
||||||
assert.Equal(t, strings.Fields(want+" job.Remove service.Remove service.Close job.Close"), operations)
|
assert.Equal(t, strings.Fields(want+" job.Remove service.Remove service.Close job.Close"), operations)
|
||||||
assert.Empty(t, rc.Masks)
|
assert.Empty(t, rc.Masks)
|
||||||
assert.Equal(t, testcase.pullError == nil && testcase.health != container.HealthUnhealthy, slices.ContainsFunc(hook.AllEntries(), func(entry *log.Entry) bool {
|
assert.Equal(t, testcase.pullError == nil && testcase.health != container.HealthUnhealthy, slices.ContainsFunc(hook.AllRecords(), func(entry logging.Record) bool {
|
||||||
return strings.HasPrefix(entry.Message, "::group::Print service container logs")
|
return strings.HasPrefix(entry.Message, "::group::Print service container logs")
|
||||||
}))
|
}))
|
||||||
})
|
})
|
||||||
@@ -1463,9 +1461,8 @@ func TestWaitForServiceContainers(t *testing.T) {
|
|||||||
Return(&container.Info{ID: "id", State: "running", Health: container.HealthHealthy}, nil).Once()
|
Return(&container.Info{ID: "id", State: "running", Health: container.HealthHealthy}, nil).Once()
|
||||||
|
|
||||||
var output bytes.Buffer
|
var output bytes.Buffer
|
||||||
logger := log.New()
|
logger := logging.New(logging.NewLevelHandler(newJobLogHandler(&output, cyan), logging.LevelInfo))
|
||||||
logger.SetOutput(&output)
|
require.NoError(t, newRunContext(0, &serviceContainer{name: "postgres", container: service}).waitForServiceContainers()(common.WithLogger(t.Context(), logger)))
|
||||||
require.NoError(t, newRunContext(0, &serviceContainer{name: "postgres", container: service}).waitForServiceContainers()(common.WithLogger(t.Context(), logger.WithFields(nil))))
|
|
||||||
assert.Equal(t, []time.Duration{0, time.Second, 2 * time.Second}, pollTimes)
|
assert.Equal(t, []time.Duration{0, time.Second, 2 * time.Second}, pollTimes)
|
||||||
assert.Equal(t, 1, strings.Count(output.String(), "postgres service is starting."))
|
assert.Equal(t, 1, strings.Count(output.String(), "postgres service is starting."))
|
||||||
assert.Contains(t, output.String(), "postgres service is healthy.")
|
assert.Contains(t, output.String(), "postgres service is healthy.")
|
||||||
@@ -1797,8 +1794,7 @@ func TestRunContextUpdateExtraPath(t *testing.T) {
|
|||||||
{name: "over 16 MiB", content: []byte(strings.Repeat("x", 16*1024*1024+1)), wantErr: true},
|
{name: "over 16 MiB", content: []byte(strings.Repeat("x", 16*1024*1024+1)), wantErr: true},
|
||||||
} {
|
} {
|
||||||
t.Run(testcase.name, func(t *testing.T) {
|
t.Run(testcase.name, func(t *testing.T) {
|
||||||
logger := log.New()
|
logger := logging.New(logging.DiscardHandler)
|
||||||
logger.SetOutput(io.Discard)
|
|
||||||
ctx := common.WithLogger(t.Context(), logger)
|
ctx := common.WithLogger(t.Context(), logger)
|
||||||
jobContainer := &containerMock{}
|
jobContainer := &containerMock{}
|
||||||
jobContainer.On("GetContainerArchive", mock.Anything, "/github/path").
|
jobContainer.On("GetContainerArchive", mock.Anything, "/github/path").
|
||||||
|
|||||||
+13
-12
@@ -16,6 +16,7 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"gitea.com/gitea/runner/act/common"
|
"gitea.com/gitea/runner/act/common"
|
||||||
|
"gitea.com/gitea/runner/act/common/logging"
|
||||||
"gitea.com/gitea/runner/act/container"
|
"gitea.com/gitea/runner/act/container"
|
||||||
"gitea.com/gitea/runner/act/ghcontext"
|
"gitea.com/gitea/runner/act/ghcontext"
|
||||||
|
|
||||||
@@ -80,18 +81,18 @@ type Config struct {
|
|||||||
PlatformPicker func(labels []string) string
|
PlatformPicker func(labels []string) string
|
||||||
KubernetesPicker func(labels []string) container.KubernetesOptions
|
KubernetesPicker func(labels []string) container.KubernetesOptions
|
||||||
InstanceTransport http.RoundTripper
|
InstanceTransport http.RoundTripper
|
||||||
JobLoggerLevel *log.Level // the level of job logger
|
JobLoggerLevel *logging.Level // the level of job logger
|
||||||
ValidVolumes []string // only volumes (and bind mounts) in this slice can be mounted on the job container or service containers
|
ValidVolumes []string // only volumes (and bind mounts) in this slice can be mounted on the job container or service containers
|
||||||
SharedToolCache bool // one tool cache for all jobs instead of one per job
|
SharedToolCache bool // one tool cache for all jobs instead of one per job
|
||||||
InsecureSkipTLS bool // whether to skip verifying TLS certificate of the Gitea instance
|
InsecureSkipTLS bool // whether to skip verifying TLS certificate of the Gitea instance
|
||||||
ClientCertFile, ClientKeyFile string // PEM client certificate for the Gitea instance
|
ClientCertFile, ClientKeyFile string // PEM client certificate for the Gitea instance
|
||||||
MaxParallel int // max parallel jobs to run across all workflows (0 = no limit, uses CPU count)
|
MaxParallel int // max parallel jobs to run across all workflows (0 = no limit, uses CPU count)
|
||||||
AllocatePTY bool // allocate a pseudo-TTY for each step's process
|
AllocatePTY bool // allocate a pseudo-TTY for each step's process
|
||||||
ServiceReadyTimeout time.Duration // how long a job waits for its service containers to report healthy (0 uses the default)
|
ServiceReadyTimeout time.Duration // how long a job waits for its service containers to report healthy (0 uses the default)
|
||||||
NoSweep bool // keep the containers, networks and volumes a job creates through its docker proxy
|
NoSweep bool // keep the containers, networks and volumes a job creates through its docker proxy
|
||||||
RunnerName string // name this runner registered with, reported as `runner.name`, defaults to the hostname
|
RunnerName string // name this runner registered with, reported as `runner.name`, defaults to the hostname
|
||||||
JobStartedHook string // script run inside the job environment before the job's first step; ACTIONS_RUNNER_HOOK_JOB_STARTED is read from Env when empty
|
JobStartedHook string // script run inside the job environment before the job's first step; ACTIONS_RUNNER_HOOK_JOB_STARTED is read from Env when empty
|
||||||
JobCompletedHook string // script run inside the job environment after the job's last step; ACTIONS_RUNNER_HOOK_JOB_COMPLETED is read from Env when empty
|
JobCompletedHook string // script run inside the job environment after the job's last step; ACTIONS_RUNNER_HOOK_JOB_COMPLETED is read from Env when empty
|
||||||
}
|
}
|
||||||
|
|
||||||
// RunnerDebug reports whether debug logging is on, exposed as `runner.debug` and
|
// RunnerDebug reports whether debug logging is on, exposed as `runner.debug` and
|
||||||
|
|||||||
+5
-5
@@ -97,14 +97,14 @@ func runStepExecutor(step step, stage stepStage, executor common.Executor) commo
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
stepResult.Conclusion = model.StepStatusFailure
|
stepResult.Conclusion = model.StepStatusFailure
|
||||||
stepResult.Outcome = model.StepStatusFailure
|
stepResult.Outcome = model.StepStatusFailure
|
||||||
logger.WithField("stepResult", stepResult.Conclusion).Infof("Failure - %s %s", stage, stepModel)
|
logger.With("stepResult", stepResult.Conclusion).Infof("Failure - %s %s", stage, stepModel)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
if !runStep {
|
if !runStep {
|
||||||
stepResult.Conclusion = model.StepStatusSkipped
|
stepResult.Conclusion = model.StepStatusSkipped
|
||||||
stepResult.Outcome = model.StepStatusSkipped
|
stepResult.Outcome = model.StepStatusSkipped
|
||||||
logger.WithField("stepResult", stepResult.Conclusion).Debugf("Skipping step '%s' due to '%s'", stepModel, ifExpression)
|
logger.With("stepResult", stepResult.Conclusion).Debugf("Skipping step '%s' due to '%s'", stepModel, ifExpression)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -207,14 +207,14 @@ func runStepExecutor(step step, stage stepStage, executor common.Executor) commo
|
|||||||
_ = rc.JobContainer.Copy(actPath, files...)(fileCommandCtx)
|
_ = rc.JobContainer.Copy(actPath, files...)(fileCommandCtx)
|
||||||
|
|
||||||
if err == nil {
|
if err == nil {
|
||||||
logger.WithField("stepResult", stepResult.Conclusion).Infof("Success - %s %s", stage, stepString)
|
logger.With("stepResult", stepResult.Conclusion).Infof("Success - %s %s", stage, stepString)
|
||||||
} else {
|
} else {
|
||||||
stepResult.Outcome = model.StepStatusFailure
|
stepResult.Outcome = model.StepStatusFailure
|
||||||
|
|
||||||
continueOnError, parseErr := isContinueOnError(ctx, stepModel.RawContinueOnError, step, stage)
|
continueOnError, parseErr := isContinueOnError(ctx, stepModel.RawContinueOnError, step, stage)
|
||||||
if parseErr != nil {
|
if parseErr != nil {
|
||||||
stepResult.Conclusion = model.StepStatusFailure
|
stepResult.Conclusion = model.StepStatusFailure
|
||||||
logger.WithField("stepResult", stepResult.Conclusion).Infof("Failure - %s %s", stage, stepString)
|
logger.With("stepResult", stepResult.Conclusion).Infof("Failure - %s %s", stage, stepString)
|
||||||
return parseErr
|
return parseErr
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -229,7 +229,7 @@ func runStepExecutor(step step, stage stepStage, executor common.Executor) commo
|
|||||||
|
|
||||||
// Infof: Errorf entries are promoted to the user log by the reporter,
|
// Infof: Errorf entries are promoted to the user log by the reporter,
|
||||||
// which would duplicate the ##[error] annotation emitted elsewhere.
|
// which would duplicate the ##[error] annotation emitted elsewhere.
|
||||||
logger.WithField("stepResult", stepResult.Conclusion).Infof("Failure - %s %s", stage, stepString)
|
logger.With("stepResult", stepResult.Conclusion).Infof("Failure - %s %s", stage, stepString)
|
||||||
}
|
}
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ func (sab *stepActionBuiltin) main() common.Executor {
|
|||||||
}
|
}
|
||||||
|
|
||||||
printRunActionHeader(ctx, sab.Step, sab.env, sab.RunContext)
|
printRunActionHeader(ctx, sab.Step, sab.env, sab.RunContext)
|
||||||
rawLogger := common.Logger(ctx).WithField(rawOutputField, true)
|
rawLogger := common.Logger(ctx).With(rawOutputField, true)
|
||||||
defer rawLogger.Infof("::endgroup::")
|
defer rawLogger.Infof("::endgroup::")
|
||||||
|
|
||||||
rc := sab.RunContext
|
rc := sab.RunContext
|
||||||
|
|||||||
@@ -43,7 +43,7 @@ func (sal *stepActionLocal) main() common.Executor {
|
|||||||
}
|
}
|
||||||
|
|
||||||
printRunActionHeader(ctx, sal.Step, sal.env, sal.getRunContext())
|
printRunActionHeader(ctx, sal.Step, sal.env, sal.getRunContext())
|
||||||
rawLogger := common.Logger(ctx).WithField(rawOutputField, true)
|
rawLogger := common.Logger(ctx).With(rawOutputField, true)
|
||||||
defer rawLogger.Infof("::endgroup::")
|
defer rawLogger.Infof("::endgroup::")
|
||||||
|
|
||||||
actionDir := filepath.Join(sal.getRunContext().Config.Workdir, sal.Step.Uses)
|
actionDir := filepath.Join(sal.getRunContext().Config.Workdir, sal.Step.Uses)
|
||||||
|
|||||||
@@ -134,7 +134,7 @@ func (sar *stepActionRemote) main() common.Executor {
|
|||||||
sar.prepareActionExecutor(),
|
sar.prepareActionExecutor(),
|
||||||
runStepExecutor(sar, stepStageMain, func(ctx context.Context) error {
|
runStepExecutor(sar, stepStageMain, func(ctx context.Context) error {
|
||||||
printRunActionHeader(ctx, sar.Step, sar.env, sar.RunContext)
|
printRunActionHeader(ctx, sar.Step, sar.env, sar.RunContext)
|
||||||
rawLogger := common.Logger(ctx).WithField(rawOutputField, true)
|
rawLogger := common.Logger(ctx).With(rawOutputField, true)
|
||||||
defer rawLogger.Infof("::endgroup::")
|
defer rawLogger.Infof("::endgroup::")
|
||||||
|
|
||||||
github := sar.getGithubContext(ctx)
|
github := sar.getGithubContext(ctx)
|
||||||
|
|||||||
@@ -14,12 +14,12 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"gitea.com/gitea/runner/act/common"
|
"gitea.com/gitea/runner/act/common"
|
||||||
|
"gitea.com/gitea/runner/act/common/logging"
|
||||||
"gitea.com/gitea/runner/act/container"
|
"gitea.com/gitea/runner/act/container"
|
||||||
"gitea.com/gitea/runner/act/lookpath"
|
"gitea.com/gitea/runner/act/lookpath"
|
||||||
|
|
||||||
"gitea.dev/actionslib/pkg/model"
|
"gitea.dev/actionslib/pkg/model"
|
||||||
"github.com/kballard/go-shellquote"
|
"github.com/kballard/go-shellquote"
|
||||||
"github.com/sirupsen/logrus"
|
|
||||||
yaml "go.yaml.in/yaml/v4"
|
yaml "go.yaml.in/yaml/v4"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -58,8 +58,8 @@ func (sr *stepRun) main() common.Executor {
|
|||||||
// printRunScriptActionDetails mirrors actions/runner ScriptHandler.PrintActionDetails
|
// printRunScriptActionDetails mirrors actions/runner ScriptHandler.PrintActionDetails
|
||||||
// for script steps.
|
// for script steps.
|
||||||
func (sr *stepRun) printRunScriptActionDetails(ctx context.Context) {
|
func (sr *stepRun) printRunScriptActionDetails(ctx context.Context) {
|
||||||
rawLogger := common.Logger(ctx).WithField(rawOutputField, true)
|
rawLogger := common.Logger(ctx).With(rawOutputField, true)
|
||||||
scriptLineLogger := rawLogger.WithField(scriptLineCyanField, true)
|
scriptLineLogger := rawLogger.With(scriptLineCyanField, true)
|
||||||
|
|
||||||
normalized := strings.TrimRight(strings.ReplaceAll(sr.interpolatedScript, "\r\n", "\n"), "\n")
|
normalized := strings.TrimRight(strings.ReplaceAll(sr.interpolatedScript, "\r\n", "\n"), "\n")
|
||||||
|
|
||||||
@@ -84,7 +84,7 @@ func printRunActionHeader(ctx context.Context, step *model.Step, env map[string]
|
|||||||
if step == nil {
|
if step == nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
rawLogger := common.Logger(ctx).WithField(rawOutputField, true)
|
rawLogger := common.Logger(ctx).With(rawOutputField, true)
|
||||||
|
|
||||||
title := step.Uses
|
title := step.Uses
|
||||||
if step.Name != "" {
|
if step.Name != "" {
|
||||||
@@ -105,7 +105,7 @@ func printRunActionHeader(ctx context.Context, step *model.Step, env map[string]
|
|||||||
// printStepEnvBlock emits the declared-env block (YAML order, internal vars filtered)
|
// printStepEnvBlock emits the declared-env block (YAML order, internal vars filtered)
|
||||||
// shared by the run: and uses: "Run" headers.
|
// shared by the run: and uses: "Run" headers.
|
||||||
func printStepEnvBlock(ctx context.Context, step *model.Step, env map[string]string, rc *RunContext) {
|
func printStepEnvBlock(ctx context.Context, step *model.Step, env map[string]string, rc *RunContext) {
|
||||||
rawLogger := common.Logger(ctx).WithField(rawOutputField, true)
|
rawLogger := common.Logger(ctx).With(rawOutputField, true)
|
||||||
caseInsensitive := rc != nil && rc.JobContainer != nil && rc.JobContainer.IsEnvironmentCaseInsensitive()
|
caseInsensitive := rc != nil && rc.JobContainer != nil && rc.JobContainer.IsEnvironmentCaseInsensitive()
|
||||||
var visible []string
|
var visible []string
|
||||||
for _, k := range stepDeclaredEnvKeysInOrder(step) {
|
for _, k := range stepDeclaredEnvKeysInOrder(step) {
|
||||||
@@ -135,7 +135,7 @@ func printStepEnvBlock(ctx context.Context, step *model.Step, env map[string]str
|
|||||||
|
|
||||||
// logKeyedValue prints one row per line of value: Gitea stores one log row per line, so an
|
// logKeyedValue prints one row per line of value: Gitea stores one log row per line, so an
|
||||||
// embedded newline would reach the user as a literal "\n".
|
// embedded newline would reach the user as a literal "\n".
|
||||||
func logKeyedValue(rawLogger *logrus.Entry, key, value string) {
|
func logKeyedValue(rawLogger *logging.Logger, key, value string) {
|
||||||
lines := strings.Split(strings.ReplaceAll(value, "\r\n", "\n"), "\n")
|
lines := strings.Split(strings.ReplaceAll(value, "\r\n", "\n"), "\n")
|
||||||
rawLogger.Infof(" %s: %s", key, lines[0])
|
rawLogger.Infof(" %s: %s", key, lines[0])
|
||||||
for _, line := range lines[1:] {
|
for _, line := range lines[1:] {
|
||||||
|
|||||||
@@ -7,13 +7,14 @@ package runner
|
|||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"context"
|
"context"
|
||||||
|
"io"
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"gitea.com/gitea/runner/act/common"
|
"gitea.com/gitea/runner/act/common"
|
||||||
|
"gitea.com/gitea/runner/act/common/logging"
|
||||||
|
|
||||||
"gitea.dev/actionslib/pkg/model"
|
"gitea.dev/actionslib/pkg/model"
|
||||||
"github.com/sirupsen/logrus"
|
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
yaml "go.yaml.in/yaml/v4"
|
yaml "go.yaml.in/yaml/v4"
|
||||||
@@ -84,11 +85,8 @@ env:
|
|||||||
require.NoError(t, yaml.Unmarshal([]byte(raw), &step))
|
require.NoError(t, yaml.Unmarshal([]byte(raw), &step))
|
||||||
|
|
||||||
buf := &bytes.Buffer{}
|
buf := &bytes.Buffer{}
|
||||||
logger := logrus.New()
|
logger := logging.New(logging.NewLevelHandler(newJobLogHandler(buf, cyan), logging.LevelInfo))
|
||||||
logger.SetOutput(buf)
|
entry := logger.With("job", "j1")
|
||||||
logger.SetLevel(logrus.InfoLevel)
|
|
||||||
logger.SetFormatter(&jobLogFormatter{color: cyan})
|
|
||||||
entry := logger.WithFields(logrus.Fields{"job": "j1"})
|
|
||||||
ctx := common.WithLogger(context.Background(), entry)
|
ctx := common.WithLogger(context.Background(), entry)
|
||||||
|
|
||||||
sr := &stepRun{
|
sr := &stepRun{
|
||||||
@@ -133,11 +131,8 @@ env:
|
|||||||
require.NoError(t, yaml.Unmarshal([]byte(raw), &step))
|
require.NoError(t, yaml.Unmarshal([]byte(raw), &step))
|
||||||
|
|
||||||
buf := &bytes.Buffer{}
|
buf := &bytes.Buffer{}
|
||||||
logger := logrus.New()
|
logger := logging.New(logging.NewLevelHandler(newJobLogHandler(buf, cyan), logging.LevelInfo))
|
||||||
logger.SetOutput(buf)
|
entry := logger.With("job", "j1")
|
||||||
logger.SetLevel(logrus.InfoLevel)
|
|
||||||
logger.SetFormatter(&jobLogFormatter{color: cyan})
|
|
||||||
entry := logger.WithFields(logrus.Fields{"job": "j1"})
|
|
||||||
ctx := common.WithLogger(context.Background(), entry)
|
ctx := common.WithLogger(context.Background(), entry)
|
||||||
|
|
||||||
printRunActionHeader(ctx, &step, map[string]string{"CUSTOM": "value", "GITHUB_TOKEN": "tok"}, &RunContext{})
|
printRunActionHeader(ctx, &step, map[string]string{"CUSTOM": "value", "GITHUB_TOKEN": "tok"}, &RunContext{})
|
||||||
@@ -166,17 +161,17 @@ func TestIsInternalEnvKey(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestPrintColoredScriptLineCyan(t *testing.T) {
|
func TestPrintColoredScriptLineCyan(t *testing.T) {
|
||||||
f := &jobLogFormatter{color: cyan}
|
f := textFormatter{w: io.Discard, color: cyan}
|
||||||
entry := &logrus.Entry{
|
record := logging.Record{
|
||||||
Level: logrus.InfoLevel,
|
Level: logging.LevelInfo,
|
||||||
Message: "echo one",
|
Message: "echo one",
|
||||||
Data: logrus.Fields{
|
Attrs: []logging.Attr{
|
||||||
"job": "j1",
|
{Key: "job", Value: "j1"},
|
||||||
rawOutputField: true,
|
{Key: rawOutputField, Value: true},
|
||||||
scriptLineCyanField: true,
|
{Key: scriptLineCyanField, Value: true},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
buf := &bytes.Buffer{}
|
buf := &bytes.Buffer{}
|
||||||
f.printColored(buf, entry)
|
f.printColored(buf, record)
|
||||||
assert.Equal(t, "\x1b[36m|\x1b[0m \x1b[36;1mecho one\x1b[0m", buf.String())
|
assert.Equal(t, "\x1b[36m|\x1b[0m \x1b[36;1mecho one\x1b[0m", buf.String())
|
||||||
}
|
}
|
||||||
+15
-8
@@ -14,13 +14,13 @@ import (
|
|||||||
"testing/synctest"
|
"testing/synctest"
|
||||||
|
|
||||||
"gitea.com/gitea/runner/act/common"
|
"gitea.com/gitea/runner/act/common"
|
||||||
|
"gitea.com/gitea/runner/act/common/logging"
|
||||||
|
"gitea.com/gitea/runner/act/common/logging/logtest"
|
||||||
"gitea.com/gitea/runner/act/container"
|
"gitea.com/gitea/runner/act/container"
|
||||||
"gitea.com/gitea/runner/internal/pkg/telemetry"
|
"gitea.com/gitea/runner/internal/pkg/telemetry"
|
||||||
|
|
||||||
"gitea.dev/actionslib/pkg/model"
|
"gitea.dev/actionslib/pkg/model"
|
||||||
runnerv1 "gitea.dev/actionslib/runner/v1"
|
runnerv1 "gitea.dev/actionslib/runner/v1"
|
||||||
log "github.com/sirupsen/logrus"
|
|
||||||
logrustest "github.com/sirupsen/logrus/hooks/test"
|
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
"github.com/stretchr/testify/mock"
|
"github.com/stretchr/testify/mock"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
@@ -299,7 +299,7 @@ func TestIsStepEnabled(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
log.SetLevel(log.DebugLevel)
|
logging.SetLevel(logging.LevelDebug)
|
||||||
assertObject := assert.New(t)
|
assertObject := assert.New(t)
|
||||||
|
|
||||||
// success()
|
// success()
|
||||||
@@ -383,7 +383,7 @@ func TestIsContinueOnError(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
log.SetLevel(log.DebugLevel)
|
logging.SetLevel(logging.LevelDebug)
|
||||||
assertObject := assert.New(t)
|
assertObject := assert.New(t)
|
||||||
|
|
||||||
// absent
|
// absent
|
||||||
@@ -513,18 +513,20 @@ func TestRunStepExecutorParity(t *testing.T) {
|
|||||||
} {
|
} {
|
||||||
t.Run(test.name, func(t *testing.T) {
|
t.Run(test.name, func(t *testing.T) {
|
||||||
step := newStep(t, test.step)
|
step := newStep(t, test.step)
|
||||||
logger, hook := logrustest.NewNullLogger()
|
hook := logtest.New()
|
||||||
|
logger := logging.New(hook)
|
||||||
err := runStepExecutor(step, stepStageMain, test.executor)(common.WithLogger(context.Background(), logger))
|
err := runStepExecutor(step, stepStageMain, test.executor)(common.WithLogger(context.Background(), logger))
|
||||||
|
|
||||||
require.ErrorContains(t, err, test.wantError)
|
require.ErrorContains(t, err, test.wantError)
|
||||||
assert.Equal(t, model.StepStatusFailure, step.RunContext.StepResults[test.step.ID].Conclusion)
|
assert.Equal(t, model.StepStatusFailure, step.RunContext.StepResults[test.step.ID].Conclusion)
|
||||||
assert.Equal(t, model.StepStatusFailure, hook.LastEntry().Data["stepResult"])
|
assert.Equal(t, model.StepStatusFailure, lastRecordAttr(hook, "stepResult"))
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
t.Run("file command error honors continue-on-error", func(t *testing.T) {
|
t.Run("file command error honors continue-on-error", func(t *testing.T) {
|
||||||
step := newStep(t, &model.Step{ID: "commands", RawContinueOnError: "true"})
|
step := newStep(t, &model.Step{ID: "commands", RawContinueOnError: "true"})
|
||||||
logger, hook := logrustest.NewNullLogger()
|
hook := logtest.New()
|
||||||
|
logger := logging.New(hook)
|
||||||
err := runStepExecutor(step, stepStageMain, func(context.Context) error {
|
err := runStepExecutor(step, stepStageMain, func(context.Context) error {
|
||||||
require.NoError(t, os.WriteFile(step.env["GITHUB_ENV"], []byte("GOOD=1\nmalformed\n"), 0o600))
|
require.NoError(t, os.WriteFile(step.env["GITHUB_ENV"], []byte("GOOD=1\nmalformed\n"), 0o600))
|
||||||
require.NoError(t, os.WriteFile(step.env["GITHUB_OUTPUT"], []byte("kept=value\n"), 0o600))
|
require.NoError(t, os.WriteFile(step.env["GITHUB_OUTPUT"], []byte("kept=value\n"), 0o600))
|
||||||
@@ -536,7 +538,7 @@ func TestRunStepExecutorParity(t *testing.T) {
|
|||||||
result := step.RunContext.StepResults[step.Step.ID]
|
result := step.RunContext.StepResults[step.Step.ID]
|
||||||
assert.Equal(t, model.StepStatusFailure, result.Outcome)
|
assert.Equal(t, model.StepStatusFailure, result.Outcome)
|
||||||
assert.Equal(t, model.StepStatusSuccess, result.Conclusion)
|
assert.Equal(t, model.StepStatusSuccess, result.Conclusion)
|
||||||
assert.Equal(t, model.StepStatusSuccess, hook.LastEntry().Data["stepResult"])
|
assert.Equal(t, model.StepStatusSuccess, lastRecordAttr(hook, "stepResult"))
|
||||||
assert.Equal(t, "1", step.RunContext.Env["GOOD"])
|
assert.Equal(t, "1", step.RunContext.Env["GOOD"])
|
||||||
assert.Equal(t, "value", result.Outputs["kept"])
|
assert.Equal(t, "value", result.Outputs["kept"])
|
||||||
assert.Equal(t, "value", step.RunContext.IntraActionState[step.Step.ID]["saved"])
|
assert.Equal(t, "value", step.RunContext.IntraActionState[step.Step.ID]["saved"])
|
||||||
@@ -630,3 +632,8 @@ func TestRunStepExecutorParity(t *testing.T) {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func lastRecordAttr(h *logtest.Handler, key string) any {
|
||||||
|
v, _ := h.LastRecord().Attr(key)
|
||||||
|
return v
|
||||||
|
}
|
||||||
@@ -8,6 +8,7 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
"gitea.com/gitea/runner/act/artifactcache"
|
"gitea.com/gitea/runner/act/artifactcache"
|
||||||
|
"gitea.com/gitea/runner/act/common/logging"
|
||||||
"gitea.com/gitea/runner/internal/app/run"
|
"gitea.com/gitea/runner/internal/app/run"
|
||||||
"gitea.com/gitea/runner/internal/pkg/config"
|
"gitea.com/gitea/runner/internal/pkg/config"
|
||||||
|
|
||||||
@@ -60,7 +61,7 @@ func runCacheServer(configFile *string, cacheArgs *cacheServerArgs) func(cmd *co
|
|||||||
Port: port,
|
Port: port,
|
||||||
InternalSecret: secret,
|
InternalSecret: secret,
|
||||||
Policy: run.CachePolicy(cfg),
|
Policy: run.CachePolicy(cfg),
|
||||||
Logger: log.StandardLogger().WithField("module", "cache_request"),
|
Logger: logging.Default().With("module", "cache_request"),
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ import (
|
|||||||
"gitea.com/gitea/runner/act/artifactcache"
|
"gitea.com/gitea/runner/act/artifactcache"
|
||||||
"gitea.com/gitea/runner/act/artifacts"
|
"gitea.com/gitea/runner/act/artifacts"
|
||||||
"gitea.com/gitea/runner/act/common"
|
"gitea.com/gitea/runner/act/common"
|
||||||
|
"gitea.com/gitea/runner/act/common/logging"
|
||||||
"gitea.com/gitea/runner/act/runner"
|
"gitea.com/gitea/runner/act/runner"
|
||||||
"gitea.com/gitea/runner/internal/app/run"
|
"gitea.com/gitea/runner/internal/app/run"
|
||||||
"gitea.com/gitea/runner/internal/pkg/config"
|
"gitea.com/gitea/runner/internal/pkg/config"
|
||||||
@@ -482,7 +483,7 @@ func runExec(ctx context.Context, execArgs *executeArgs) func(cmd *cobra.Command
|
|||||||
// init a cache server
|
// init a cache server
|
||||||
handler, err := artifactcache.StartHandler(artifactcache.Options{
|
handler, err := artifactcache.StartHandler(artifactcache.Options{
|
||||||
Policy: run.CachePolicy(&config.Config{Cache: config.DefaultCache()}),
|
Policy: run.CachePolicy(&config.Config{Cache: config.DefaultCache()}),
|
||||||
Logger: log.StandardLogger().WithField("module", "cache_request"),
|
Logger: logging.Default().With("module", "cache_request"),
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -553,7 +554,7 @@ func runExec(ctx context.Context, execArgs *executeArgs) func(cmd *cobra.Command
|
|||||||
}
|
}
|
||||||
|
|
||||||
if !execArgs.debug {
|
if !execArgs.debug {
|
||||||
logLevel := log.InfoLevel
|
logLevel := logging.LevelInfo
|
||||||
config.JobLoggerLevel = &logLevel
|
config.JobLoggerLevel = &logLevel
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -4,21 +4,15 @@
|
|||||||
package run
|
package run
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"io"
|
"gitea.com/gitea/runner/act/common/logging"
|
||||||
|
|
||||||
log "github.com/sirupsen/logrus"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// NullLogger is used to create a new JobLogger to discard logs. This
|
// NullLogger is used to create a new JobLogger to discard logs. This
|
||||||
// will prevent these logs from being logged to the stdout, but
|
// will prevent these logs from being logged to the stdout, but
|
||||||
// forward them to the Reporter via its hook.
|
// forward them to the Reporter via its handler.
|
||||||
type NullLogger struct{}
|
type NullLogger struct{}
|
||||||
|
|
||||||
// WithJobLogger creates a new logrus.Logger that will discard all logs.
|
// WithJobLogger returns a handler that discards all logs.
|
||||||
func (n NullLogger) WithJobLogger() *log.Logger {
|
func (n NullLogger) WithJobLogger() logging.Handler {
|
||||||
logger := log.New()
|
return logging.DiscardHandler
|
||||||
logger.SetOutput(io.Discard)
|
|
||||||
logger.SetLevel(log.TraceLevel)
|
|
||||||
|
|
||||||
return logger
|
|
||||||
}
|
}
|
||||||
@@ -10,12 +10,12 @@ import (
|
|||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"gitea.com/gitea/runner/act/common/logging"
|
||||||
"gitea.com/gitea/runner/internal/pkg/config"
|
"gitea.com/gitea/runner/internal/pkg/config"
|
||||||
"gitea.com/gitea/runner/internal/pkg/metrics"
|
"gitea.com/gitea/runner/internal/pkg/metrics"
|
||||||
"gitea.com/gitea/runner/internal/pkg/report"
|
"gitea.com/gitea/runner/internal/pkg/report"
|
||||||
|
|
||||||
runnerv1 "gitea.dev/actionslib/runner/v1"
|
runnerv1 "gitea.dev/actionslib/runner/v1"
|
||||||
log "github.com/sirupsen/logrus"
|
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
"google.golang.org/protobuf/types/known/structpb"
|
"google.golang.org/protobuf/types/known/structpb"
|
||||||
@@ -146,12 +146,12 @@ func TestRunPostTaskScriptIntegration(t *testing.T) {
|
|||||||
|
|
||||||
func setReporterJobResult(t *testing.T, reporter *report.Reporter, result runnerv1.Result) {
|
func setReporterJobResult(t *testing.T, reporter *report.Reporter, result runnerv1.Result) {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
require.NoError(t, reporter.Fire(&log.Entry{
|
require.NoError(t, reporter.Handle(t.Context(), logging.Record{
|
||||||
Time: time.Now(),
|
Time: time.Now(),
|
||||||
Message: "job finished",
|
Message: "job finished",
|
||||||
Data: log.Fields{
|
Attrs: []logging.Attr{
|
||||||
"stage": "Post",
|
{Key: "stage", Value: "Post"},
|
||||||
"jobResult": metrics.ResultToStatusLabel(result),
|
{Key: "jobResult", Value: metrics.ResultToStatusLabel(result)},
|
||||||
},
|
},
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
@@ -28,6 +28,7 @@ import (
|
|||||||
|
|
||||||
"gitea.com/gitea/runner/act/artifactcache"
|
"gitea.com/gitea/runner/act/artifactcache"
|
||||||
"gitea.com/gitea/runner/act/common"
|
"gitea.com/gitea/runner/act/common"
|
||||||
|
"gitea.com/gitea/runner/act/common/logging"
|
||||||
"gitea.com/gitea/runner/act/container"
|
"gitea.com/gitea/runner/act/container"
|
||||||
"gitea.com/gitea/runner/act/runner"
|
"gitea.com/gitea/runner/act/runner"
|
||||||
"gitea.com/gitea/runner/internal/pkg/client"
|
"gitea.com/gitea/runner/internal/pkg/client"
|
||||||
@@ -109,7 +110,7 @@ func NewRunner(cfg *config.Config, reg *config.Registration, cli client.Client)
|
|||||||
Upstream: cfg.Cache.ExternalServer,
|
Upstream: cfg.Cache.ExternalServer,
|
||||||
S3: cacheS3(cfg),
|
S3: cacheS3(cfg),
|
||||||
Policy: CachePolicy(cfg),
|
Policy: CachePolicy(cfg),
|
||||||
Logger: log.StandardLogger().WithField("module", "cache_request"),
|
Logger: logging.Default().With("module", "cache_request"),
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Errorf("cannot init cache server, it will be disabled: %v", err)
|
log.Errorf("cannot init cache server, it will be disabled: %v", err)
|
||||||
@@ -645,7 +646,7 @@ func (r *Runner) run(ctx context.Context, task *runnerv1.Task, reporter *report.
|
|||||||
reporter.Logf("workflow prepared")
|
reporter.Logf("workflow prepared")
|
||||||
|
|
||||||
// add logger recorders
|
// add logger recorders
|
||||||
ctx = common.WithLoggerHook(ctx, reporter)
|
ctx = common.WithLoggerHandler(ctx, reporter)
|
||||||
|
|
||||||
if !log.IsLevelEnabled(log.DebugLevel) {
|
if !log.IsLevelEnabled(log.DebugLevel) {
|
||||||
ctx = runner.WithJobLoggerFactory(ctx, NullLogger{})
|
ctx = runner.WithJobLoggerFactory(ctx, NullLogger{})
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"gitea.com/gitea/runner/act/common"
|
"gitea.com/gitea/runner/act/common"
|
||||||
|
"gitea.com/gitea/runner/act/common/logging"
|
||||||
"gitea.com/gitea/runner/act/runner"
|
"gitea.com/gitea/runner/act/runner"
|
||||||
clientmocks "gitea.com/gitea/runner/internal/pkg/client/mocks"
|
clientmocks "gitea.com/gitea/runner/internal/pkg/client/mocks"
|
||||||
"gitea.com/gitea/runner/internal/pkg/config"
|
"gitea.com/gitea/runner/internal/pkg/config"
|
||||||
@@ -79,9 +80,9 @@ func TestRunnerRunningCountAndNullLogger(t *testing.T) {
|
|||||||
r.runningCount.Add(2)
|
r.runningCount.Add(2)
|
||||||
require.Equal(t, int64(2), r.RunningCount())
|
require.Equal(t, int64(2), r.RunningCount())
|
||||||
|
|
||||||
logger := NullLogger{}.WithJobLogger()
|
handler := NullLogger{}.WithJobLogger()
|
||||||
require.NotNil(t, logger)
|
require.NotNil(t, handler)
|
||||||
require.NotNil(t, logger.Out)
|
require.False(t, handler.Enabled(t.Context(), logging.LevelError))
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestRunnerReclaimsVolumesAfterReporting(t *testing.T) {
|
func TestRunnerReclaimsVolumesAfterReporting(t *testing.T) {
|
||||||
|
|||||||
@@ -11,12 +11,12 @@ import (
|
|||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"gitea.com/gitea/runner/act/common/logging"
|
||||||
"gitea.com/gitea/runner/internal/pkg/client/mocks"
|
"gitea.com/gitea/runner/internal/pkg/client/mocks"
|
||||||
"gitea.com/gitea/runner/internal/pkg/config"
|
"gitea.com/gitea/runner/internal/pkg/config"
|
||||||
|
|
||||||
connect_go "connectrpc.com/connect"
|
connect_go "connectrpc.com/connect"
|
||||||
runnerv1 "gitea.dev/actionslib/runner/v1"
|
runnerv1 "gitea.dev/actionslib/runner/v1"
|
||||||
log "github.com/sirupsen/logrus"
|
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
"github.com/stretchr/testify/mock"
|
"github.com/stretchr/testify/mock"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
@@ -52,10 +52,10 @@ func TestJobLog_MirrorsUploadedRows(t *testing.T) {
|
|||||||
reporter.ResetSteps(1)
|
reporter.ResetSteps(1)
|
||||||
|
|
||||||
fire := func(message string) {
|
fire := func(message string) {
|
||||||
require.NoError(t, reporter.Fire(&log.Entry{
|
require.NoError(t, fire(reporter, logEntry{
|
||||||
Message: message,
|
Message: message,
|
||||||
Level: log.InfoLevel,
|
Level: logging.LevelInfo,
|
||||||
Data: log.Fields{"stage": "Main", "stepNumber": 0, "raw_output": true},
|
Data: map[string]any{"stage": "Main", "stepNumber": 0, "raw_output": true},
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
fire("the token is s3cret-value")
|
fire("the token is s3cret-value")
|
||||||
|
|||||||
@@ -8,11 +8,13 @@ import (
|
|||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"regexp"
|
"regexp"
|
||||||
|
"slices"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
"sync/atomic"
|
"sync/atomic"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"gitea.com/gitea/runner/act/common/logging"
|
||||||
"gitea.com/gitea/runner/act/runner"
|
"gitea.com/gitea/runner/act/runner"
|
||||||
"gitea.com/gitea/runner/internal/pkg/client"
|
"gitea.com/gitea/runner/internal/pkg/client"
|
||||||
"gitea.com/gitea/runner/internal/pkg/config"
|
"gitea.com/gitea/runner/internal/pkg/config"
|
||||||
@@ -166,8 +168,30 @@ func (r *Reporter) ResetSteps(l int) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *Reporter) Levels() []log.Level {
|
// Enabled makes the Reporter see every record, the job logger's level filter runs before it.
|
||||||
return log.AllLevels
|
func (r *Reporter) Enabled(context.Context, logging.Level) bool { return true }
|
||||||
|
|
||||||
|
// WithAttrs returns a Handler feeding the same Reporter, with attrs added to every record.
|
||||||
|
func (r *Reporter) WithAttrs(attrs []logging.Attr) logging.Handler {
|
||||||
|
return &reporterHandler{r: r, attrs: attrs}
|
||||||
|
}
|
||||||
|
|
||||||
|
type reporterHandler struct {
|
||||||
|
r *Reporter
|
||||||
|
attrs []logging.Attr
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *reporterHandler) Enabled(ctx context.Context, level logging.Level) bool {
|
||||||
|
return h.r.Enabled(ctx, level)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *reporterHandler) Handle(ctx context.Context, rec logging.Record) error {
|
||||||
|
rec.Attrs = slices.Concat(h.attrs, rec.Attrs)
|
||||||
|
return h.r.Handle(ctx, rec)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *reporterHandler) WithAttrs(attrs []logging.Attr) logging.Handler {
|
||||||
|
return &reporterHandler{r: h.r, attrs: slices.Concat(h.attrs, attrs)}
|
||||||
}
|
}
|
||||||
|
|
||||||
// appendLogRow masks a row before buffering it for Gitea and the local job.log, the one point
|
// appendLogRow masks a row before buffering it for Gitea and the local job.log, the one point
|
||||||
@@ -181,11 +205,19 @@ func (r *Reporter) appendLogRow(row *runnerv1.LogRow) {
|
|||||||
r.jobLog.write(row.Time.AsTime(), row.Content)
|
r.jobLog.write(row.Time.AsTime(), row.Content)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func attrArgs(attrs []logging.Attr) []any {
|
||||||
|
args := make([]any, len(attrs))
|
||||||
|
for i, a := range attrs {
|
||||||
|
args[i] = a
|
||||||
|
}
|
||||||
|
return args
|
||||||
|
}
|
||||||
|
|
||||||
// isJobStepEntry is used to not report composite step results incorrectly as step result
|
// isJobStepEntry is used to not report composite step results incorrectly as step result
|
||||||
// returns true if the logentry is on job level
|
// returns true if the logentry is on job level
|
||||||
// returns false for composite action step messages
|
// returns false for composite action step messages
|
||||||
func isJobStepEntry(entry *log.Entry) bool {
|
func isJobStepEntry(rec logging.Record) bool {
|
||||||
if v, ok := entry.Data["stepID"]; ok {
|
if v, ok := rec.Attr("stepID"); ok {
|
||||||
if v, ok := v.([]string); ok && len(v) > 1 {
|
if v, ok := v.([]string); ok && len(v) > 1 {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
@@ -219,15 +251,15 @@ func (r *Reporter) unlockAndNotify(urgentState bool) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *Reporter) Fire(entry *log.Entry) error {
|
func (r *Reporter) Handle(_ context.Context, entry logging.Record) error {
|
||||||
urgentState := false
|
urgentState := false
|
||||||
|
|
||||||
r.stateMu.Lock()
|
r.stateMu.Lock()
|
||||||
|
|
||||||
r.stateChanged = true
|
r.stateChanged = true
|
||||||
|
|
||||||
if log.IsLevelEnabled(log.TraceLevel) {
|
if logging.Enabled(logging.LevelTrace) {
|
||||||
log.WithFields(entry.Data).Trace(r.mask(entry.Message)) // the process masker has no ::add-mask:: value
|
logging.With(attrArgs(entry.Attrs)...).Trace(r.mask(entry.Message)) // the process masker has no ::add-mask:: value
|
||||||
}
|
}
|
||||||
|
|
||||||
timestamp := entry.Time
|
timestamp := entry.Time
|
||||||
@@ -235,10 +267,10 @@ func (r *Reporter) Fire(entry *log.Entry) error {
|
|||||||
r.state.StartedAt = timestamppb.New(timestamp)
|
r.state.StartedAt = timestamppb.New(timestamp)
|
||||||
}
|
}
|
||||||
|
|
||||||
stage := entry.Data["stage"]
|
stage, _ := entry.Attr("stage")
|
||||||
|
|
||||||
if stage != "Main" {
|
if stage != "Main" {
|
||||||
if v, ok := entry.Data["jobResult"]; ok {
|
if v, ok := entry.Attr("jobResult"); ok {
|
||||||
if jobResult, ok := r.parseResult(v); ok {
|
if jobResult, ok := r.parseResult(v); ok {
|
||||||
// We need to ensure log upload before this upload
|
// We need to ensure log upload before this upload
|
||||||
r.state.Result = jobResult
|
r.state.Result = jobResult
|
||||||
@@ -262,7 +294,7 @@ func (r *Reporter) Fire(entry *log.Entry) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var step *runnerv1.StepState
|
var step *runnerv1.StepState
|
||||||
if v, ok := entry.Data["stepNumber"]; ok {
|
if v, ok := entry.Attr("stepNumber"); ok {
|
||||||
if v, ok := v.(int); ok && len(r.state.Steps) > v {
|
if v, ok := v.(int); ok && len(r.state.Steps) > v {
|
||||||
step = r.state.Steps[v]
|
step = r.state.Steps[v]
|
||||||
}
|
}
|
||||||
@@ -284,11 +316,12 @@ func (r *Reporter) Fire(entry *log.Entry) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Force reporting log errors as raw output to prevent silent failures
|
// Force reporting log errors as raw output to prevent silent failures
|
||||||
if entry.Level == log.ErrorLevel {
|
v, ok := entry.Attr("raw_output")
|
||||||
entry.Data["raw_output"] = true
|
if entry.Level >= logging.LevelError {
|
||||||
|
v, ok = true, true
|
||||||
}
|
}
|
||||||
|
|
||||||
if v, ok := entry.Data["raw_output"]; ok {
|
if ok {
|
||||||
if rawOutput, ok := v.(bool); ok && rawOutput {
|
if rawOutput, ok := v.(bool); ok && rawOutput {
|
||||||
if row := r.parseLogRow(entry); row != nil {
|
if row := r.parseLogRow(entry); row != nil {
|
||||||
if step.LogLength == 0 {
|
if step.LogLength == 0 {
|
||||||
@@ -301,7 +334,7 @@ func (r *Reporter) Fire(entry *log.Entry) error {
|
|||||||
} else if r.shouldAppendLogRow(entry) {
|
} else if r.shouldAppendLogRow(entry) {
|
||||||
r.appendLogRow(r.parseLogRow(entry))
|
r.appendLogRow(r.parseLogRow(entry))
|
||||||
}
|
}
|
||||||
if v, ok := entry.Data["stepResult"]; ok && isJobStepEntry(entry) {
|
if v, ok := entry.Attr("stepResult"); ok && isJobStepEntry(entry) {
|
||||||
if stepResult, ok := r.parseResult(v); ok {
|
if stepResult, ok := r.parseResult(v); ok {
|
||||||
if step.LogLength == 0 {
|
if step.LogLength == 0 {
|
||||||
step.LogIndex = int64(r.logOffset + len(r.logRows))
|
step.LogIndex = int64(r.logOffset + len(r.logRows))
|
||||||
@@ -671,7 +704,7 @@ func (r *Reporter) ReportState(reportResult bool) error {
|
|||||||
r.outputsMu.Unlock()
|
r.outputsMu.Unlock()
|
||||||
|
|
||||||
// Consume stateChanged atomically with the snapshot; restored on error
|
// Consume stateChanged atomically with the snapshot; restored on error
|
||||||
// below so a concurrent Fire() during UpdateTask isn't silently lost.
|
// below so a log record handled during UpdateTask isn't silently lost.
|
||||||
// Heartbeat at stateReportInterval even when nothing changed, so the server
|
// Heartbeat at stateReportInterval even when nothing changed, so the server
|
||||||
// doesn't time out long-running silent jobs as orphaned (#826).
|
// doesn't time out long-running silent jobs as orphaned (#826).
|
||||||
last := r.lastReportedAtNanos.Load()
|
last := r.lastReportedAtNanos.Load()
|
||||||
@@ -753,8 +786,8 @@ func (r *Reporter) duringSteps() bool {
|
|||||||
// shouldAppendLogRow reports whether a non-raw_output entry should be written
|
// shouldAppendLogRow reports whether a non-raw_output entry should be written
|
||||||
// to the job log: only when we are between steps and the entry's level is
|
// to the job log: only when we are between steps and the entry's level is
|
||||||
// within the globally configured log level.
|
// within the globally configured log level.
|
||||||
func (r *Reporter) shouldAppendLogRow(entry *log.Entry) bool {
|
func (r *Reporter) shouldAppendLogRow(entry logging.Record) bool {
|
||||||
return !r.duringSteps() && entry.Level <= log.GetLevel()
|
return !r.duringSteps() && entry.Level >= logging.GetLevel()
|
||||||
}
|
}
|
||||||
|
|
||||||
var stringToResult = map[string]runnerv1.Result{
|
var stringToResult = map[string]runnerv1.Result{
|
||||||
@@ -879,7 +912,7 @@ func cutPrefixFold(s, prefix string) (string, bool) {
|
|||||||
return s[len(prefix):], true
|
return s[len(prefix):], true
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *Reporter) parseLogRow(entry *log.Entry) *runnerv1.LogRow {
|
func (r *Reporter) parseLogRow(entry logging.Record) *runnerv1.LogRow {
|
||||||
content := strings.TrimRight(entry.Message, "\r\n")
|
content := strings.TrimRight(entry.Message, "\r\n")
|
||||||
|
|
||||||
// cmdRegex only covers the ::cmd:: form, so the ##[add-mask] one would otherwise reach
|
// cmdRegex only covers the ::cmd:: form, so the ##[add-mask] one would otherwise reach
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ import (
|
|||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"gitea.com/gitea/runner/act/common/logging"
|
||||||
"gitea.com/gitea/runner/act/runner"
|
"gitea.com/gitea/runner/act/runner"
|
||||||
"gitea.com/gitea/runner/internal/pkg/client/mocks"
|
"gitea.com/gitea/runner/internal/pkg/client/mocks"
|
||||||
"gitea.com/gitea/runner/internal/pkg/config"
|
"gitea.com/gitea/runner/internal/pkg/config"
|
||||||
@@ -25,7 +26,6 @@ import (
|
|||||||
connect_go "connectrpc.com/connect"
|
connect_go "connectrpc.com/connect"
|
||||||
"gitea.dev/actionslib/pkg/model"
|
"gitea.dev/actionslib/pkg/model"
|
||||||
runnerv1 "gitea.dev/actionslib/runner/v1"
|
runnerv1 "gitea.dev/actionslib/runner/v1"
|
||||||
log "github.com/sirupsen/logrus"
|
|
||||||
logrustest "github.com/sirupsen/logrus/hooks/test"
|
logrustest "github.com/sirupsen/logrus/hooks/test"
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
"github.com/stretchr/testify/mock"
|
"github.com/stretchr/testify/mock"
|
||||||
@@ -206,7 +206,7 @@ func TestReporter_parseLogRow(t *testing.T) {
|
|||||||
debugOutputEnabled: tt.debugOutputEnabled,
|
debugOutputEnabled: tt.debugOutputEnabled,
|
||||||
}
|
}
|
||||||
for idx, arg := range tt.args {
|
for idx, arg := range tt.args {
|
||||||
rv := r.parseLogRow(&log.Entry{Message: arg})
|
rv := r.parseLogRow(logging.Record{Message: arg})
|
||||||
got := "<nil>"
|
got := "<nil>"
|
||||||
|
|
||||||
if rv != nil {
|
if rv != nil {
|
||||||
@@ -225,13 +225,13 @@ func TestReporter_parseLogRowAddMask(t *testing.T) {
|
|||||||
for _, line := range []string{"::add-mask::supersecret", "##[add-mask]supersecret"} {
|
for _, line := range []string{"::add-mask::supersecret", "##[add-mask]supersecret"} {
|
||||||
r := &Reporter{logReplacer: strings.NewReplacer()}
|
r := &Reporter{logReplacer: strings.NewReplacer()}
|
||||||
|
|
||||||
assert.Nil(t, r.parseLogRow(&log.Entry{Message: line}), line)
|
assert.Nil(t, r.parseLogRow(logging.Record{Message: line}), line)
|
||||||
|
|
||||||
assert.Equal(t, "using *** now", r.mask("using supersecret now"), line)
|
assert.Equal(t, "using *** now", r.mask("using supersecret now"), line)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestReporter_Fire(t *testing.T) {
|
func TestReporter_Handle(t *testing.T) {
|
||||||
t.Run("ignore command lines", func(t *testing.T) {
|
t.Run("ignore command lines", func(t *testing.T) {
|
||||||
client := mocks.NewClient(t)
|
client := mocks.NewClient(t)
|
||||||
client.On("UpdateLog", mock.Anything, mock.Anything).Return(func(_ context.Context, req *connect_go.Request[runnerv1.UpdateLogRequest]) (*connect_go.Response[runnerv1.UpdateLogResponse], error) {
|
client.On("UpdateLog", mock.Anything, mock.Anything).Return(func(_ context.Context, req *connect_go.Request[runnerv1.UpdateLogRequest]) (*connect_go.Response[runnerv1.UpdateLogResponse], error) {
|
||||||
@@ -263,13 +263,13 @@ func TestReporter_Fire(t *testing.T) {
|
|||||||
"raw_output": true,
|
"raw_output": true,
|
||||||
}
|
}
|
||||||
|
|
||||||
require.NoError(t, reporter.Fire(&log.Entry{Message: "regular log line", Data: dataStep0}))
|
require.NoError(t, fire(reporter, logEntry{Message: "regular log line", Data: dataStep0}))
|
||||||
require.NoError(t, reporter.Fire(&log.Entry{Message: "::debug::debug log line", Data: dataStep0}))
|
require.NoError(t, fire(reporter, logEntry{Message: "::debug::debug log line", Data: dataStep0}))
|
||||||
require.NoError(t, reporter.Fire(&log.Entry{Message: "regular log line", Data: dataStep0}))
|
require.NoError(t, fire(reporter, logEntry{Message: "regular log line", Data: dataStep0}))
|
||||||
require.NoError(t, reporter.Fire(&log.Entry{Message: "::debug::debug log line", Data: dataStep0}))
|
require.NoError(t, fire(reporter, logEntry{Message: "::debug::debug log line", Data: dataStep0}))
|
||||||
require.NoError(t, reporter.Fire(&log.Entry{Message: "::debug::debug log line", Data: dataStep0}))
|
require.NoError(t, fire(reporter, logEntry{Message: "::debug::debug log line", Data: dataStep0}))
|
||||||
require.NoError(t, reporter.Fire(&log.Entry{Message: "regular log line", Data: dataStep0}))
|
require.NoError(t, fire(reporter, logEntry{Message: "regular log line", Data: dataStep0}))
|
||||||
require.NoError(t, reporter.Fire(&log.Entry{Message: "composite step result", Data: map[string]any{
|
require.NoError(t, fire(reporter, logEntry{Message: "composite step result", Data: map[string]any{
|
||||||
"stage": "Main",
|
"stage": "Main",
|
||||||
"stepID": []string{"0", "0"},
|
"stepID": []string{"0", "0"},
|
||||||
"stepNumber": 0,
|
"stepNumber": 0,
|
||||||
@@ -277,7 +277,7 @@ func TestReporter_Fire(t *testing.T) {
|
|||||||
"stepResult": model.StepStatusFailure,
|
"stepResult": model.StepStatusFailure,
|
||||||
}}))
|
}}))
|
||||||
assert.Equal(t, runnerv1.Result_RESULT_UNSPECIFIED, reporter.state.Steps[0].Result)
|
assert.Equal(t, runnerv1.Result_RESULT_UNSPECIFIED, reporter.state.Steps[0].Result)
|
||||||
require.NoError(t, reporter.Fire(&log.Entry{Message: "step result", Data: map[string]any{
|
require.NoError(t, fire(reporter, logEntry{Message: "step result", Data: map[string]any{
|
||||||
"stage": "Main",
|
"stage": "Main",
|
||||||
"stepNumber": 0,
|
"stepNumber": 0,
|
||||||
"raw_output": true,
|
"raw_output": true,
|
||||||
@@ -291,9 +291,9 @@ func TestReporter_Fire(t *testing.T) {
|
|||||||
|
|
||||||
func TestReporter_LogLevelFiltering(t *testing.T) {
|
func TestReporter_LogLevelFiltering(t *testing.T) {
|
||||||
// Set global level to Info so Debug entries should be filtered.
|
// Set global level to Info so Debug entries should be filtered.
|
||||||
origLevel := log.GetLevel()
|
origLevel := logging.GetLevel()
|
||||||
log.SetLevel(log.InfoLevel)
|
logging.SetLevel(logging.LevelInfo)
|
||||||
defer log.SetLevel(origLevel)
|
defer logging.SetLevel(origLevel)
|
||||||
|
|
||||||
client := mocks.NewClient(t)
|
client := mocks.NewClient(t)
|
||||||
client.On("UpdateLog", mock.Anything, mock.Anything).Return(func(_ context.Context, req *connect_go.Request[runnerv1.UpdateLogRequest]) (*connect_go.Response[runnerv1.UpdateLogResponse], error) {
|
client.On("UpdateLog", mock.Anything, mock.Anything).Return(func(_ context.Context, req *connect_go.Request[runnerv1.UpdateLogRequest]) (*connect_go.Response[runnerv1.UpdateLogResponse], error) {
|
||||||
@@ -316,27 +316,27 @@ func TestReporter_LogLevelFiltering(t *testing.T) {
|
|||||||
}()
|
}()
|
||||||
reporter.ResetSteps(2)
|
reporter.ResetSteps(2)
|
||||||
|
|
||||||
dataStep0 := log.Fields{"stage": "Main", "stepNumber": 0, "raw_output": true}
|
dataStep0 := map[string]any{"stage": "Main", "stepNumber": 0, "raw_output": true}
|
||||||
dataStep0Internal := log.Fields{"stage": "Main", "stepNumber": 0}
|
dataStep0Internal := map[string]any{"stage": "Main", "stepNumber": 0}
|
||||||
|
|
||||||
// raw_output entries always appear in job log regardless of level.
|
// raw_output entries always appear in job log regardless of level.
|
||||||
require.NoError(t, reporter.Fire(&log.Entry{Message: "step output", Data: dataStep0, Level: log.InfoLevel}))
|
require.NoError(t, fire(reporter, logEntry{Message: "step output", Data: dataStep0, Level: logging.LevelInfo}))
|
||||||
require.NoError(t, reporter.Fire(&log.Entry{Message: "step debug output", Data: dataStep0, Level: log.DebugLevel}))
|
require.NoError(t, fire(reporter, logEntry{Message: "step debug output", Data: dataStep0, Level: logging.LevelDebug}))
|
||||||
assert.Equal(t, int64(2), reporter.state.Steps[0].LogLength, "raw_output entries must always be forwarded")
|
assert.Equal(t, int64(2), reporter.state.Steps[0].LogLength, "raw_output entries must always be forwarded")
|
||||||
|
|
||||||
// Non-raw_output entries during steps are not added to logRows regardless of level.
|
// Non-raw_output entries during steps are not added to logRows regardless of level.
|
||||||
require.NoError(t, reporter.Fire(&log.Entry{Message: "internal info", Data: dataStep0Internal, Level: log.InfoLevel}))
|
require.NoError(t, fire(reporter, logEntry{Message: "internal info", Data: dataStep0Internal, Level: logging.LevelInfo}))
|
||||||
require.NoError(t, reporter.Fire(&log.Entry{Message: "internal debug", Data: dataStep0Internal, Level: log.DebugLevel}))
|
require.NoError(t, fire(reporter, logEntry{Message: "internal debug", Data: dataStep0Internal, Level: logging.LevelDebug}))
|
||||||
|
|
||||||
// stepResult at DebugLevel (skipped step) must still update state even when filtered from log.
|
// stepResult at DebugLevel (skipped step) must still update state even when filtered from log.
|
||||||
require.NoError(t, reporter.Fire(&log.Entry{
|
require.NoError(t, fire(reporter, logEntry{
|
||||||
Message: "Skipping step",
|
Message: "Skipping step",
|
||||||
Data: log.Fields{
|
Data: map[string]any{
|
||||||
"stage": "Main",
|
"stage": "Main",
|
||||||
"stepNumber": 1,
|
"stepNumber": 1,
|
||||||
"stepResult": "skipped",
|
"stepResult": "skipped",
|
||||||
},
|
},
|
||||||
Level: log.DebugLevel,
|
Level: logging.LevelDebug,
|
||||||
}))
|
}))
|
||||||
assert.Equal(t, runnerv1.Result_RESULT_SKIPPED, reporter.state.Steps[1].Result,
|
assert.Equal(t, runnerv1.Result_RESULT_SKIPPED, reporter.state.Steps[1].Result,
|
||||||
"stepResult at DebugLevel must update step state even when log entry is filtered from job log output")
|
"stepResult at DebugLevel must update step state even when log entry is filtered from job log output")
|
||||||
@@ -383,9 +383,9 @@ func TestReporter_EphemeralRunnerDeletion(t *testing.T) {
|
|||||||
reporter.ResetSteps(1)
|
reporter.ResetSteps(1)
|
||||||
|
|
||||||
// Fire a log entry to create pending data
|
// Fire a log entry to create pending data
|
||||||
require.NoError(t, reporter.Fire(&log.Entry{
|
require.NoError(t, fire(reporter, logEntry{
|
||||||
Message: "build output",
|
Message: "build output",
|
||||||
Data: log.Fields{"stage": "Main", "stepNumber": 0, "raw_output": true},
|
Data: map[string]any{"stage": "Main", "stepNumber": 0, "raw_output": true},
|
||||||
}))
|
}))
|
||||||
|
|
||||||
// Step 1: RunDaemon calls ReportLog(false) — runner is still alive
|
// Step 1: RunDaemon calls ReportLog(false) — runner is still alive
|
||||||
@@ -500,9 +500,9 @@ func TestReporter_MaxLatencyTimer(t *testing.T) {
|
|||||||
}()
|
}()
|
||||||
|
|
||||||
// Fire a single log line — not enough to trigger batch flush
|
// Fire a single log line — not enough to trigger batch flush
|
||||||
require.NoError(t, reporter.Fire(&log.Entry{
|
require.NoError(t, fire(reporter, logEntry{
|
||||||
Message: "single log line",
|
Message: "single log line",
|
||||||
Data: log.Fields{"stage": "Main", "stepNumber": 0, "raw_output": true},
|
Data: map[string]any{"stage": "Main", "stepNumber": 0, "raw_output": true},
|
||||||
}))
|
}))
|
||||||
|
|
||||||
// maxLatencyTimer should flush within ~100ms. Wait up to 500ms.
|
// maxLatencyTimer should flush within ~100ms. Wait up to 500ms.
|
||||||
@@ -552,9 +552,9 @@ func TestReporter_BatchSizeFlush(t *testing.T) {
|
|||||||
|
|
||||||
// Fire exactly batchSize log lines
|
// Fire exactly batchSize log lines
|
||||||
for i := range 5 {
|
for i := range 5 {
|
||||||
require.NoError(t, reporter.Fire(&log.Entry{
|
require.NoError(t, fire(reporter, logEntry{
|
||||||
Message: fmt.Sprintf("log line %d", i),
|
Message: fmt.Sprintf("log line %d", i),
|
||||||
Data: log.Fields{"stage": "Main", "stepNumber": 0, "raw_output": true},
|
Data: map[string]any{"stage": "Main", "stepNumber": 0, "raw_output": true},
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -565,7 +565,7 @@ func TestReporter_BatchSizeFlush(t *testing.T) {
|
|||||||
"batch size threshold should have triggered immediate flush")
|
"batch size threshold should have triggered immediate flush")
|
||||||
}
|
}
|
||||||
|
|
||||||
// TestReporter_StateChangedNotLostDuringReport asserts that a Fire() arriving
|
// TestReporter_StateChangedNotLostDuringReport asserts that a log record arriving
|
||||||
// mid-UpdateTask re-dirties the flag so the change is picked up by the next report.
|
// mid-UpdateTask re-dirties the flag so the change is picked up by the next report.
|
||||||
func TestReporter_StateChangedNotLostDuringReport(t *testing.T) {
|
func TestReporter_StateChangedNotLostDuringReport(t *testing.T) {
|
||||||
var updateTaskCalls atomic.Int64
|
var updateTaskCalls atomic.Int64
|
||||||
@@ -607,11 +607,11 @@ func TestReporter_StateChangedNotLostDuringReport(t *testing.T) {
|
|||||||
// Wait until UpdateTask is in flight (snapshot taken, flag consumed).
|
// Wait until UpdateTask is in flight (snapshot taken, flag consumed).
|
||||||
<-inFlight
|
<-inFlight
|
||||||
|
|
||||||
// Concurrent Fire() modifies state — must re-flip stateChanged so the
|
// A concurrent log record modifies state — must re-flip stateChanged so the
|
||||||
// change is not lost when the in-flight ReportState finishes.
|
// change is not lost when the in-flight ReportState finishes.
|
||||||
require.NoError(t, reporter.Fire(&log.Entry{
|
require.NoError(t, fire(reporter, logEntry{
|
||||||
Message: "step starts",
|
Message: "step starts",
|
||||||
Data: log.Fields{"stage": "Main", "stepNumber": 1, "raw_output": true},
|
Data: map[string]any{"stage": "Main", "stepNumber": 1, "raw_output": true},
|
||||||
}))
|
}))
|
||||||
|
|
||||||
// Release the in-flight UpdateTask and wait for it to return.
|
// Release the in-flight UpdateTask and wait for it to return.
|
||||||
@@ -619,15 +619,15 @@ func TestReporter_StateChangedNotLostDuringReport(t *testing.T) {
|
|||||||
require.NoError(t, <-done)
|
require.NoError(t, <-done)
|
||||||
|
|
||||||
// stateChanged must still be true so the next ReportState picks up the
|
// stateChanged must still be true so the next ReportState picks up the
|
||||||
// concurrent Fire()'s change instead of skipping via the early-return path.
|
// concurrent record's change instead of skipping via the early-return path.
|
||||||
reporter.stateMu.RLock()
|
reporter.stateMu.RLock()
|
||||||
changed := reporter.stateChanged
|
changed := reporter.stateChanged
|
||||||
reporter.stateMu.RUnlock()
|
reporter.stateMu.RUnlock()
|
||||||
assert.True(t, changed, "stateChanged must remain true after a concurrent Fire() during in-flight ReportState")
|
assert.True(t, changed, "stateChanged must remain true after a concurrent log record during in-flight ReportState")
|
||||||
|
|
||||||
// And the next ReportState must actually send a second UpdateTask.
|
// And the next ReportState must actually send a second UpdateTask.
|
||||||
require.NoError(t, reporter.ReportState(false))
|
require.NoError(t, reporter.ReportState(false))
|
||||||
assert.Equal(t, int64(2), updateTaskCalls.Load(), "concurrent Fire() change must trigger a second UpdateTask, not be silently lost")
|
assert.Equal(t, int64(2), updateTaskCalls.Load(), "concurrent log record change must trigger a second UpdateTask, not be silently lost")
|
||||||
}
|
}
|
||||||
|
|
||||||
// TestReporter_StateChangedRestoredOnError verifies that when UpdateTask fails,
|
// TestReporter_StateChangedRestoredOnError verifies that when UpdateTask fails,
|
||||||
@@ -709,9 +709,9 @@ func TestReporter_StateNotifyFlush(t *testing.T) {
|
|||||||
}()
|
}()
|
||||||
|
|
||||||
// Fire a log entry that starts a step — this triggers stateNotify
|
// Fire a log entry that starts a step — this triggers stateNotify
|
||||||
require.NoError(t, reporter.Fire(&log.Entry{
|
require.NoError(t, fire(reporter, logEntry{
|
||||||
Message: "step starting",
|
Message: "step starting",
|
||||||
Data: log.Fields{"stage": "Main", "stepNumber": 0, "raw_output": true},
|
Data: map[string]any{"stage": "Main", "stepNumber": 0, "raw_output": true},
|
||||||
}))
|
}))
|
||||||
|
|
||||||
// stateNotify should trigger immediate UpdateTask call
|
// stateNotify should trigger immediate UpdateTask call
|
||||||
@@ -1054,8 +1054,8 @@ func TestAppendLogRow(t *testing.T) {
|
|||||||
assert.Equal(t, "***", r.logRows[0].Content)
|
assert.Equal(t, "***", r.logRows[0].Content)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestReporter_Levels(t *testing.T) {
|
func TestReporter_Enabled(t *testing.T) {
|
||||||
assert.Equal(t, log.AllLevels, (&Reporter{}).Levels())
|
assert.True(t, (&Reporter{}).Enabled(t.Context(), logging.LevelTrace))
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestReporter_Result(t *testing.T) {
|
func TestReporter_Result(t *testing.T) {
|
||||||
@@ -1218,10 +1218,10 @@ func TestReporter_AckIndexBeyondBuffer(t *testing.T) {
|
|||||||
reporter := NewReporter(ctx, cancel, client, &runnerv1.Task{Context: taskCtx}, cfg)
|
reporter := NewReporter(ctx, cancel, client, &runnerv1.Task{Context: taskCtx}, cfg)
|
||||||
reporter.ResetSteps(1)
|
reporter.ResetSteps(1)
|
||||||
|
|
||||||
require.NoError(t, reporter.Fire(&log.Entry{
|
require.NoError(t, fire(reporter, logEntry{
|
||||||
Message: "hello",
|
Message: "hello",
|
||||||
Data: log.Fields{"stage": "Main", "stepNumber": 0, "raw_output": true},
|
Data: map[string]any{"stage": "Main", "stepNumber": 0, "raw_output": true},
|
||||||
Level: log.InfoLevel,
|
Level: logging.LevelInfo,
|
||||||
}))
|
}))
|
||||||
|
|
||||||
require.NoError(t, reporter.ReportLog(false))
|
require.NoError(t, reporter.ReportLog(false))
|
||||||
@@ -1258,10 +1258,10 @@ func TestReporter_CloseWithStuckDaemon(t *testing.T) {
|
|||||||
reporter.RunDaemon()
|
reporter.RunDaemon()
|
||||||
reporter.ResetSteps(1)
|
reporter.ResetSteps(1)
|
||||||
|
|
||||||
require.NoError(t, reporter.Fire(&log.Entry{
|
require.NoError(t, fire(reporter, logEntry{
|
||||||
Message: "hello",
|
Message: "hello",
|
||||||
Data: log.Fields{"stage": "Main", "stepNumber": 0, "raw_output": true},
|
Data: map[string]any{"stage": "Main", "stepNumber": 0, "raw_output": true},
|
||||||
Level: log.InfoLevel,
|
Level: logging.LevelInfo,
|
||||||
}))
|
}))
|
||||||
|
|
||||||
select {
|
select {
|
||||||
@@ -1441,17 +1441,17 @@ func TestReporter_StepRangeCoversAckedRows(t *testing.T) {
|
|||||||
_ = reporter.Close("")
|
_ = reporter.Close("")
|
||||||
}()
|
}()
|
||||||
|
|
||||||
stepData := log.Fields{"stage": "Main", "stepNumber": 0, "raw_output": true}
|
stepData := map[string]any{"stage": "Main", "stepNumber": 0, "raw_output": true}
|
||||||
|
|
||||||
// The step's first row is a transition, so it flushes state as well as logs.
|
// The step's first row is a transition, so it flushes state as well as logs.
|
||||||
require.NoError(t, reporter.Fire(&log.Entry{Message: "step starting", Data: stepData}))
|
require.NoError(t, fire(reporter, logEntry{Message: "step starting", Data: stepData}))
|
||||||
require.Eventually(t, func() bool {
|
require.Eventually(t, func() bool {
|
||||||
rows, claimed := server.snapshot()
|
rows, claimed := server.snapshot()
|
||||||
return rows == 1 && claimed == rows
|
return rows == 1 && claimed == rows
|
||||||
}, time.Second, 10*time.Millisecond, "the step transition should have flushed both log and state")
|
}, time.Second, 10*time.Millisecond, "the step transition should have flushed both log and state")
|
||||||
|
|
||||||
for i := range cfg.Runner.LogReportBatchSize {
|
for i := range cfg.Runner.LogReportBatchSize {
|
||||||
require.NoError(t, reporter.Fire(&log.Entry{
|
require.NoError(t, fire(reporter, logEntry{
|
||||||
Message: fmt.Sprintf("step output %d", i),
|
Message: fmt.Sprintf("step output %d", i),
|
||||||
Data: stepData,
|
Data: stepData,
|
||||||
}))
|
}))
|
||||||
@@ -1489,3 +1489,18 @@ func TestReporter_FlushWithoutAckedRowsSkipsState(t *testing.T) {
|
|||||||
reporter.Logf("row")
|
reporter.Logf("row")
|
||||||
reporter.reportLogWithState()
|
reporter.reportLogWithState()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// logEntry is the record a job logger hands the Reporter, with the fields it accumulated as Data.
|
||||||
|
type logEntry struct {
|
||||||
|
Message string
|
||||||
|
Data map[string]any
|
||||||
|
Level logging.Level
|
||||||
|
}
|
||||||
|
|
||||||
|
func fire(r *Reporter, e logEntry) error {
|
||||||
|
attrs := make([]logging.Attr, 0, len(e.Data))
|
||||||
|
for k, v := range e.Data {
|
||||||
|
attrs = append(attrs, logging.Attr{Key: k, Value: v})
|
||||||
|
}
|
||||||
|
return r.WithAttrs(attrs).Handle(context.Background(), logging.Record{Message: e.Message, Level: e.Level})
|
||||||
|
}
|
||||||
Reference in new issue
Block a user