mirror of
https://gitea.com/gitea/runner.git
synced 2026-11-05 14:55:44 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
470969dc74 | ||
|
|
be7ddcd15a | ||
|
|
c3c647ef83 | ||
|
|
160e18e705 | ||
|
|
20c8b4abc4 | ||
|
|
ccb9afd7a5 | ||
|
|
49e34813ae | ||
|
|
e0ef637805 | ||
|
|
8502663d0e | ||
|
|
4e8037a93b | ||
|
|
76110557ab | ||
|
|
3bc5cc03f8 | ||
|
|
de961b91eb | ||
|
|
ad9eeb668a | ||
|
|
b741335222 | ||
|
|
20143c9558 |
No files matched your search
@@ -19,7 +19,7 @@ jobs:
|
||||
timeout-minutes: 5
|
||||
steps:
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
|
||||
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7
|
||||
- uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7
|
||||
with:
|
||||
node-version: 24
|
||||
- run: make lint-pr-title
|
||||
|
||||
@@ -28,7 +28,7 @@ jobs:
|
||||
# daemon retains them between runs, so this is usually a fast manifest re-check.
|
||||
- name: pre-pull test images
|
||||
env:
|
||||
TEST_JOB_IMAGE: node:24-bookworm-slim@sha256:0e0ff40c39bc087845bfb27465a0df4ea419520094bc35842ff83dd8cbe6f9b6 # renovate: datasource=docker
|
||||
TEST_JOB_IMAGE: node:24-bookworm-slim@sha256:d6aa754f16b3197301076f047b5def2f02ea1dbbc2ca920407d46d7ec7f87b20 # renovate: datasource=docker
|
||||
TEST_SERVICE_IMAGE: nginx:alpine@sha256:df221db836e1754089190208cee7eeda94f233197056426eda74a43ab1abeac2 # renovate: datasource=docker
|
||||
run: |
|
||||
for image in "$TEST_JOB_IMAGE" "$TEST_SERVICE_IMAGE"; do
|
||||
|
||||
@@ -39,6 +39,15 @@ linters:
|
||||
deny:
|
||||
- pkg: golang.org/x/exp
|
||||
desc: it's experimental and unreliable
|
||||
logging:
|
||||
files:
|
||||
- $all
|
||||
- "!**/act/common/logging/**"
|
||||
deny:
|
||||
- pkg: github.com/sirupsen/logrus
|
||||
desc: log through gitea.com/gitea/runner/act/common/logging
|
||||
- pkg: log/slog
|
||||
desc: log through gitea.com/gitea/runner/act/common/logging
|
||||
nolintlint:
|
||||
allow-unused: false
|
||||
require-explanation: true
|
||||
|
||||
@@ -31,6 +31,23 @@ depending on the prefix:
|
||||
- Command *properties* also escape `%3A` and `%2C`, which the UI never decodes, so the reporter
|
||||
decodes exactly those two when folding a location into an annotation.
|
||||
|
||||
## Logging
|
||||
|
||||
Log through `act/common/logging`; the linter rejects importing `log/slog` or logrus elsewhere.
|
||||
A job's logger comes from `common.Logger(ctx)`; code with no job uses the package functions
|
||||
(`logging.Infof`, `logging.With`). Both take printf-style messages, and `With("key", value)` adds
|
||||
fields.
|
||||
|
||||
- The process-wide logger masks the secrets of every live task, the job logger masks its own.
|
||||
Going around either leaks them.
|
||||
- Libraries that log through logrus's or slog's global logger are forwarded to the process-wide
|
||||
one, so they follow its level and masking too.
|
||||
- That forwarding only works while logrus's global logger is left as `logging` configures it. A
|
||||
dependency that calls `logrus.SetOutput` or `SetLevel` breaks it, so check
|
||||
`logging.LogrusBridgeIntact()` when adding or upgrading one.
|
||||
- Tests read what was logged with `logtest.NewLogger()`, or `logtest.SetDefault(t)` for the
|
||||
package functions.
|
||||
|
||||
## Built-in actions
|
||||
|
||||
Each lives in `internal/pkg/action/<name>` and is registered in `builtinActions` in
|
||||
|
||||
@@ -15,7 +15,6 @@ import (
|
||||
"encoding/json/v2"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/http/httputil"
|
||||
@@ -30,10 +29,10 @@ import (
|
||||
"time"
|
||||
|
||||
"gitea.com/gitea/runner/act/common"
|
||||
"gitea.com/gitea/runner/act/common/logging"
|
||||
"gitea.com/gitea/runner/internal/pkg/disk"
|
||||
|
||||
"github.com/julienschmidt/httprouter"
|
||||
"github.com/sirupsen/logrus"
|
||||
"github.com/timshannon/bolthold"
|
||||
"go.etcd.io/bbolt"
|
||||
)
|
||||
@@ -85,7 +84,7 @@ type Handler struct {
|
||||
listener net.Listener
|
||||
port int
|
||||
server *http.Server
|
||||
logger logrus.FieldLogger
|
||||
logger *logging.Logger
|
||||
|
||||
gcing atomic.Bool
|
||||
gcAt time.Time
|
||||
@@ -133,7 +132,7 @@ type Options struct {
|
||||
InternalSecret string
|
||||
|
||||
Policy Policy
|
||||
Logger logrus.FieldLogger
|
||||
Logger *logging.Logger
|
||||
}
|
||||
|
||||
// StartHandler starts an embedded cache or forwards cache requests to Upstream.
|
||||
@@ -147,11 +146,9 @@ func StartHandler(opts Options) (*Handler, error) {
|
||||
}
|
||||
|
||||
if logger == nil {
|
||||
discard := logrus.New()
|
||||
discard.Out = io.Discard
|
||||
logger = discard
|
||||
logger = logging.New(logging.DiscardHandler)
|
||||
}
|
||||
logger = logger.WithField("module", "artifactcache")
|
||||
logger = logger.With("module", "artifactcache")
|
||||
h.logger = logger
|
||||
|
||||
if opts.OutboundIP != "" {
|
||||
|
||||
@@ -230,7 +230,8 @@ func Serve(ctx context.Context, artifactPath, addr, port string) context.CancelF
|
||||
go func() {
|
||||
logger.Infof("Start server on http://%s", server.Addr)
|
||||
if err := server.ListenAndServe(); err != nil && err != http.ErrServerClosed {
|
||||
logger.Fatal(err)
|
||||
logger.Error(err)
|
||||
os.Exit(1)
|
||||
}
|
||||
}()
|
||||
|
||||
|
||||
@@ -4,15 +4,15 @@
|
||||
package common
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"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) {
|
||||
ctx := context.Background()
|
||||
ctx := t.Context()
|
||||
if Dryrun(ctx) {
|
||||
t.Fatal("plain context should not be dryrun")
|
||||
}
|
||||
@@ -25,7 +25,7 @@ func TestDryrunContext(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestJobErrorContainer(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
ctx := t.Context()
|
||||
err := errors.New("job failed")
|
||||
|
||||
SetJobError(ctx, err)
|
||||
@@ -40,34 +40,27 @@ func TestJobErrorContainer(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoggerAndHookContext(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
if Logger(ctx) != logrus.StandardLogger() {
|
||||
t.Fatal("plain context should use standard logger")
|
||||
func TestLoggerAndHandlerContext(t *testing.T) {
|
||||
ctx := t.Context()
|
||||
if Logger(ctx) != logging.Default() {
|
||||
t.Fatal("plain context should use the default logger")
|
||||
}
|
||||
if LoggerHook(ctx) != nil {
|
||||
t.Fatal("plain context should not have a logger hook")
|
||||
if LoggerHandler(ctx) != nil {
|
||||
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)
|
||||
if Logger(ctx) != logger {
|
||||
t.Fatal("WithLogger should set logger")
|
||||
}
|
||||
|
||||
hook := testHook{}
|
||||
ctx = WithLoggerHook(ctx, hook)
|
||||
if LoggerHook(ctx) != hook {
|
||||
t.Fatal("WithLoggerHook should set hook")
|
||||
handler := logtest.New()
|
||||
ctx = WithLoggerHandler(ctx, handler)
|
||||
if LoggerHandler(ctx) != handler {
|
||||
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
|
||||
}
|
||||
@@ -9,7 +9,7 @@ import (
|
||||
"fmt"
|
||||
"runtime/debug"
|
||||
|
||||
log "github.com/sirupsen/logrus"
|
||||
"gitea.com/gitea/runner/act/common/logging"
|
||||
)
|
||||
|
||||
// Executor define contract for the steps of a workflow
|
||||
@@ -74,33 +74,33 @@ func NewParallelExecutor(parallel int, executors ...Executor) Executor {
|
||||
errs := make(chan error, len(executors))
|
||||
|
||||
if 1 > parallel {
|
||||
log.Debugf("Parallel tasks (%d) below minimum, setting to 1", parallel)
|
||||
logging.Debugf("Parallel tasks (%d) below minimum, setting to 1", parallel)
|
||||
parallel = 1
|
||||
}
|
||||
|
||||
log.Infof("NewParallelExecutor: Creating %d workers for %d executors", parallel, len(executors))
|
||||
logging.Infof("NewParallelExecutor: Creating %d workers for %d executors", parallel, len(executors))
|
||||
|
||||
for i := 0; i < parallel; i++ {
|
||||
go func(workerID int, work <-chan Executor, errs chan<- error) {
|
||||
log.Debugf("Worker %d started", workerID)
|
||||
logging.Debugf("Worker %d started", workerID)
|
||||
taskCount := 0
|
||||
for executor := range work {
|
||||
taskCount++
|
||||
log.Debugf("Worker %d executing task %d", workerID, taskCount)
|
||||
logging.Debugf("Worker %d executing task %d", workerID, taskCount)
|
||||
// Recover from panics in executors to avoid crashing the worker
|
||||
// goroutine which would leave the runner process hung.
|
||||
// https://gitea.com/gitea/runner/issues/371
|
||||
errs <- func() (err error) {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
log.Errorf("panic in executor: %v\n%s", r, debug.Stack())
|
||||
logging.Errorf("panic in executor: %v\n%s", r, debug.Stack())
|
||||
err = fmt.Errorf("panic: %v", r)
|
||||
}
|
||||
}()
|
||||
return executor(ctx)
|
||||
}()
|
||||
}
|
||||
log.Debugf("Worker %d finished (%d tasks executed)", workerID, taskCount)
|
||||
logging.Debugf("Worker %d finished (%d tasks executed)", workerID, taskCount)
|
||||
}(i, work, errs)
|
||||
}
|
||||
|
||||
|
||||
@@ -20,7 +20,7 @@ import (
|
||||
func TestNewWorkflow(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
|
||||
ctx := context.Background()
|
||||
ctx := t.Context()
|
||||
|
||||
// empty
|
||||
emptyWorkflow := NewPipelineExecutor()
|
||||
@@ -72,7 +72,7 @@ func concurrencyProbe(wantActive int32) (exec Executor, count, maxActive *atomic
|
||||
}
|
||||
|
||||
func TestNewParallelExecutor(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
ctx := t.Context()
|
||||
|
||||
exec, count, maxActive := concurrencyProbe(2)
|
||||
require.NoError(t, NewParallelExecutor(2, exec, exec, exec)(ctx))
|
||||
@@ -89,10 +89,10 @@ func TestNewParallelExecutor(t *testing.T) {
|
||||
func TestNewParallelExecutorEmpty(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
|
||||
ctx := context.Background()
|
||||
ctx := t.Context()
|
||||
require.NoError(t, NewParallelExecutor(2)(ctx))
|
||||
|
||||
canceledCtx, cancel := context.WithCancel(context.Background())
|
||||
canceledCtx, cancel := context.WithCancel(t.Context())
|
||||
cancel()
|
||||
|
||||
err := NewParallelExecutor(2)(canceledCtx)
|
||||
@@ -102,7 +102,7 @@ func TestNewParallelExecutorEmpty(t *testing.T) {
|
||||
func TestNewParallelExecutorFailed(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
ctx, cancel := context.WithCancel(t.Context())
|
||||
cancel()
|
||||
|
||||
count := 0
|
||||
@@ -118,7 +118,7 @@ func TestNewParallelExecutorFailed(t *testing.T) {
|
||||
func TestNewParallelExecutorCanceled(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
ctx, cancel := context.WithCancel(t.Context())
|
||||
cancel()
|
||||
|
||||
errExpected := errors.New("fake error")
|
||||
@@ -150,12 +150,12 @@ func TestNewParallelExecutorRunsRemainingAfterFailure(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
require.Error(t, NewParallelExecutor(2, executors...)(context.Background()))
|
||||
require.Error(t, NewParallelExecutor(2, executors...)(t.Context()))
|
||||
assert.Equal(t, int32(4), successCount.Load(), "a failing executor must not stop the others")
|
||||
}
|
||||
|
||||
func TestExecutorConditionalsAndFinally(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
ctx := t.Context()
|
||||
var calls []string
|
||||
record := func(name string) Executor {
|
||||
return func(ctx context.Context) error {
|
||||
@@ -180,7 +180,7 @@ func TestExecutorFinallyReturnsFinallyErrorWithOriginal(t *testing.T) {
|
||||
mainErr := errors.New("main failed")
|
||||
finalErr := errors.New("cleanup failed")
|
||||
|
||||
err := NewErrorExecutor(mainErr).Finally(NewErrorExecutor(finalErr))(context.Background())
|
||||
err := NewErrorExecutor(mainErr).Finally(NewErrorExecutor(finalErr))(t.Context())
|
||||
require.Error(t, err)
|
||||
if !strings.Contains(err.Error(), "cleanup failed") || !strings.Contains(err.Error(), "main failed") {
|
||||
t.Fatalf("finally error = %q, want both cleanup and original error", err)
|
||||
|
||||
+93
-33
@@ -10,8 +10,10 @@ import (
|
||||
"encoding/base64"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/url"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"runtime"
|
||||
@@ -21,16 +23,13 @@ import (
|
||||
"time"
|
||||
|
||||
"gitea.com/gitea/runner/act/common"
|
||||
"gitea.com/gitea/runner/act/common/logging"
|
||||
"gitea.com/gitea/runner/internal/pkg/lock"
|
||||
|
||||
log "github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
var (
|
||||
codeCommitHTTPRegex = regexp.MustCompile(`^https?://git-codecommit\.(.+)\.amazonaws.com/v1/repos/(.+)$`)
|
||||
codeCommitSSHRegex = regexp.MustCompile(`ssh://git-codecommit\.(.+)\.amazonaws.com/v1/repos/(.+)$`)
|
||||
githubHTTPRegex = regexp.MustCompile(`^https?://.*github.com.*/(.+)/(.+?)(?:.git)?$`)
|
||||
githubSSHRegex = regexp.MustCompile(`github.com[:/](.+)/(.+?)(?:.git)?$`)
|
||||
hexRefRegex = regexp.MustCompile(`^[0-9a-fA-F]+$`)
|
||||
|
||||
LocalEnvVars = []string{ // `git rev-parse --local-env-vars`, inherited ones point git at another repository
|
||||
@@ -44,12 +43,29 @@ var (
|
||||
ErrShortRef = errors.New("short SHA references are not supported")
|
||||
)
|
||||
|
||||
// AcquireCloneLock returns an unlock function after locking the per-directory mutex for dir.
|
||||
// Only concurrent operations targeting the same directory are serialized; clones into different directories run in parallel.
|
||||
// Callers reading files inside dir (e.g. tarring a checked-out action into a job container) must hold this lock too,
|
||||
// otherwise a concurrent NewGitCloneExecutor on the same dir can mutate the worktree mid-read.
|
||||
func AcquireCloneLock(dir string) func() {
|
||||
return cloneLocks.Lock(dir)
|
||||
// AcquireCloneLock serializes access to dir across runner processes, or within this one where no lock file can be created. Readers of dir must hold it too.
|
||||
func AcquireCloneLock(ctx context.Context, dir string) (func(), error) {
|
||||
dir = filepath.Clean(dir)
|
||||
if runtime.GOOS == "plan9" {
|
||||
return cloneLocks.Lock(dir), nil
|
||||
}
|
||||
if err := os.MkdirAll(filepath.Dir(dir), 0o755); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for {
|
||||
release, err := lock.TryLock(dir)
|
||||
if err == nil {
|
||||
return func() { _ = release() }, nil
|
||||
}
|
||||
if !errors.Is(err, lock.ErrLocked) {
|
||||
return cloneLocks.Lock(dir), nil
|
||||
}
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return nil, ctx.Err()
|
||||
case <-time.After(50 * time.Millisecond):
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
type Error struct {
|
||||
@@ -109,13 +125,12 @@ func FindGitRef(ctx context.Context, file string) (string, error) {
|
||||
}
|
||||
|
||||
// FindGithubRepo get the repo
|
||||
func FindGithubRepo(ctx context.Context, file, githubInstance string) (string, error) {
|
||||
func FindGithubRepo(ctx context.Context, file string) (string, error) {
|
||||
url, err := findGitRemoteURL(ctx, file)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
_, slug := findGitSlug(url, githubInstance)
|
||||
return slug, nil
|
||||
return findGitSlug(url)
|
||||
}
|
||||
|
||||
func findGitRemoteURL(ctx context.Context, file string) (string, error) {
|
||||
@@ -130,25 +145,25 @@ func findGitRemoteURL(ctx context.Context, file string) (string, error) {
|
||||
return url, nil
|
||||
}
|
||||
|
||||
func findGitSlug(url, githubInstance string) (string, string) {
|
||||
if matches := codeCommitHTTPRegex.FindStringSubmatch(url); matches != nil {
|
||||
return "CodeCommit", matches[2]
|
||||
} else if matches := codeCommitSSHRegex.FindStringSubmatch(url); matches != nil {
|
||||
return "CodeCommit", matches[2]
|
||||
} else if matches := githubHTTPRegex.FindStringSubmatch(url); matches != nil {
|
||||
return "GitHub", fmt.Sprintf("%s/%s", matches[1], matches[2])
|
||||
} else if matches := githubSSHRegex.FindStringSubmatch(url); matches != nil {
|
||||
return "GitHub", fmt.Sprintf("%s/%s", matches[1], matches[2])
|
||||
} else if githubInstance != "github.com" {
|
||||
gheHTTPRegex := regexp.MustCompile(fmt.Sprintf(`^https?://%s/(.+)/(.+?)(?:.git)?$`, githubInstance))
|
||||
gheSSHRegex := regexp.MustCompile(githubInstance + "[:/](.+)/(.+?)(?:.git)?$")
|
||||
if matches := gheHTTPRegex.FindStringSubmatch(url); matches != nil {
|
||||
return "GitHubEnterprise", fmt.Sprintf("%s/%s", matches[1], matches[2])
|
||||
} else if matches := gheSSHRegex.FindStringSubmatch(url); matches != nil {
|
||||
return "GitHubEnterprise", fmt.Sprintf("%s/%s", matches[1], matches[2])
|
||||
}
|
||||
func findGitSlug(remoteURL string) (string, error) {
|
||||
if matches := codeCommitHTTPRegex.FindStringSubmatch(remoteURL); matches != nil {
|
||||
return matches[2], nil
|
||||
} else if matches := codeCommitSSHRegex.FindStringSubmatch(remoteURL); matches != nil {
|
||||
return matches[2], nil
|
||||
}
|
||||
return "", url
|
||||
if host, repoPath, ok := strings.Cut(remoteURL, ":"); ok && filepath.VolumeName(remoteURL) == "" && !strings.Contains(host, "/") && !strings.HasPrefix(repoPath, "//") {
|
||||
remoteURL = "ssh://" + host + "/" + repoPath // scp-like [user@]host:path
|
||||
}
|
||||
parsed, err := url.Parse(remoteURL)
|
||||
if err != nil {
|
||||
return "", errors.New("cannot parse the origin remote URL")
|
||||
}
|
||||
owner, repo := path.Split(strings.TrimSuffix(strings.Trim(parsed.Path, "/"), ".git"))
|
||||
owner = path.Base(owner)
|
||||
if parsed.Host == "" || repo == "" || owner == "." || owner == "/" {
|
||||
return "", errors.New("cannot determine owner and repository from the origin remote URL")
|
||||
}
|
||||
return owner + "/" + repo, nil
|
||||
}
|
||||
|
||||
// NewGitCloneExecutorInput the input for the NewGitCloneExecutor
|
||||
@@ -173,7 +188,7 @@ type NewGitCloneExecutorInput struct {
|
||||
}
|
||||
|
||||
// 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
|
||||
_, err := os.Stat(filepath.Join(input.Dir, ".git")) // without it git would discover a repository around Dir
|
||||
if err == nil {
|
||||
@@ -182,6 +197,9 @@ func CloneIfRequired(ctx context.Context, input NewGitCloneExecutorInput, logger
|
||||
if err == nil && origin == input.URL {
|
||||
return true, nil
|
||||
}
|
||||
if ctxErr := ctx.Err(); ctxErr != nil { // a cancelled origin lookup must not discard a valid clone
|
||||
return false, ctxErr
|
||||
}
|
||||
if err == nil {
|
||||
logger.Debugf("Removing cached clone at %s because origin URL changed from %s to %s", input.Dir, origin, input.URL)
|
||||
} else if _, statErr := os.Stat(input.Dir); statErr == nil {
|
||||
@@ -209,7 +227,11 @@ func NewGitCloneExecutor(input NewGitCloneExecutorInput) common.Executor {
|
||||
logger.Infof("git clone '%s' # ref=%s", input.URL, input.Ref)
|
||||
}
|
||||
logger.Debugf(" cloning %s to %s", input.URL, input.Dir)
|
||||
defer AcquireCloneLock(input.Dir)()
|
||||
unlock, err := AcquireCloneLock(ctx, input.Dir)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer unlock()
|
||||
|
||||
reused, err := CloneIfRequired(ctx, input, logger)
|
||||
if err != nil {
|
||||
@@ -364,6 +386,44 @@ func ConfigEnv(config ...string) map[string]string {
|
||||
return env
|
||||
}
|
||||
|
||||
func ListFiles(ctx context.Context, dir string) ([]string, error) {
|
||||
listed, err := runGit(ctx, dir, nil, "ls-files", "-z", "-t", "--stage", "--others", "--exclude-standard")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var files, repos []string
|
||||
for entry := range strings.SplitSeq(string(listed), "\x00") {
|
||||
tag, file, ok := strings.Cut(entry, " ")
|
||||
if tag != "?" {
|
||||
var meta string
|
||||
meta, file, ok = strings.Cut(file, "\t")
|
||||
if strings.HasPrefix(meta, "160000 ") {
|
||||
file += "/"
|
||||
}
|
||||
}
|
||||
if repo, isRepo := strings.CutSuffix(file, "/"); isRepo {
|
||||
repos = append(repos, repo)
|
||||
} else if ok {
|
||||
files = append(files, file)
|
||||
}
|
||||
}
|
||||
slices.Sort(repos)
|
||||
for _, repo := range slices.Compact(repos) {
|
||||
files = append(files, repo)
|
||||
if _, err := os.Lstat(filepath.Join(dir, repo, ".git")); err != nil {
|
||||
continue // git lists an uninitialized submodule as "./" from inside it
|
||||
}
|
||||
nested, err := ListFiles(ctx, filepath.Join(dir, repo))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, file := range nested {
|
||||
files = append(files, repo+"/"+file)
|
||||
}
|
||||
}
|
||||
return files, nil
|
||||
}
|
||||
|
||||
func runGit(ctx context.Context, dir string, input *NewGitCloneExecutorInput, args ...string) ([]byte, error) {
|
||||
config := []string{"core.autocrlf=false", "core.eol=lf"} // keep action files LF
|
||||
if input != nil {
|
||||
|
||||
+91
-47
@@ -18,40 +18,46 @@ import (
|
||||
"sync"
|
||||
"syscall"
|
||||
"testing"
|
||||
"testing/synctest"
|
||||
"time"
|
||||
|
||||
"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"
|
||||
|
||||
log "github.com/sirupsen/logrus"
|
||||
logrustest "github.com/sirupsen/logrus/hooks/test"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestFindGitSlug(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
|
||||
slugTests := []struct {
|
||||
url string // input
|
||||
provider string // expected result
|
||||
slug string // expected result
|
||||
for _, testcase := range []struct {
|
||||
remoteURL string
|
||||
slug string
|
||||
}{
|
||||
{"https://git-codecommit.us-east-1.amazonaws.com/v1/repos/my-repo-name", "CodeCommit", "my-repo-name"},
|
||||
{"ssh://git-codecommit.us-west-2.amazonaws.com/v1/repos/my-repo", "CodeCommit", "my-repo"},
|
||||
{"[email protected]:nektos/act.git", "GitHub", "nektos/act"},
|
||||
{"[email protected]:nektos/act", "GitHub", "nektos/act"},
|
||||
{"https://github.com/nektos/act.git", "GitHub", "nektos/act"},
|
||||
{"http://github.com/nektos/act.git", "GitHub", "nektos/act"},
|
||||
{"https://github.com/nektos/act", "GitHub", "nektos/act"},
|
||||
{"http://github.com/nektos/act", "GitHub", "nektos/act"},
|
||||
{"git+ssh://[email protected]/owner/repo.git", "GitHub", "owner/repo"},
|
||||
{"http://myotherrepo.com/act.git", "", "http://myotherrepo.com/act.git"},
|
||||
}
|
||||
|
||||
for _, tt := range slugTests {
|
||||
provider, slug := findGitSlug(tt.url, "github.com")
|
||||
assert.Equal(tt.provider, provider)
|
||||
assert.Equal(tt.slug, slug)
|
||||
{"https://git-codecommit.us-east-1.amazonaws.com/v1/repos/my-repo-name", "my-repo-name"},
|
||||
{"ssh://git-codecommit.us-west-2.amazonaws.com/v1/repos/my-repo", "my-repo"},
|
||||
{"[email protected]:nektos/act.git", "nektos/act"},
|
||||
{"[email protected]:nektos/act", "nektos/act"},
|
||||
{"https://github.com/nektos/act.git", "nektos/act"},
|
||||
{"http://github.com/nektos/act.git", "nektos/act"},
|
||||
{"https://github.com/nektos/act", "nektos/act"},
|
||||
{"http://github.com/nektos/act", "nektos/act"},
|
||||
{"git+ssh://[email protected]/owner/repo.git", "owner/repo"},
|
||||
{"https://example.com:3000/gitea/owner/repo.git/", "owner/repo"},
|
||||
{"ssh://[email protected]:2222/gitea/owner/repo.git", "owner/repo"},
|
||||
{"example.com:owner/repo.git", "owner/repo"},
|
||||
{"http://myotherrepo.com/act.git", ""},
|
||||
{"https://user:[email protected]/act.git", ""},
|
||||
{"/tmp/owner/repo.git", ""},
|
||||
{"./owner/repo:branch", ""},
|
||||
} {
|
||||
t.Run(testcase.remoteURL, func(t *testing.T) {
|
||||
slug, err := findGitSlug(testcase.remoteURL)
|
||||
assert.Equal(t, testcase.slug == "", err != nil)
|
||||
assert.NotContains(t, fmt.Sprint(err), "secret")
|
||||
assert.Equal(t, testcase.slug, slug)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -93,21 +99,21 @@ func TestFindGitMetadataOfSHA256RepositoryUsesOrigin(t *testing.T) {
|
||||
require.NoError(t, gitCmd("-C", basedir, "remote", "add", "origin", remoteURL))
|
||||
require.NoError(t, gitCmd("-C", basedir, "config", "--add", "remote.origin.url", "https://github.com/other/repo.git"))
|
||||
|
||||
shortSHA, sha, err := FindGitRevision(context.Background(), basedir)
|
||||
shortSHA, sha, err := FindGitRevision(t.Context(), basedir)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, gitRevParse(t, basedir, "HEAD"), sha)
|
||||
require.Len(t, sha, 64)
|
||||
require.Equal(t, sha[:7], shortSHA)
|
||||
|
||||
ref, err := FindGitRef(context.Background(), basedir)
|
||||
ref, err := FindGitRef(t.Context(), basedir)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, "refs/heads/main", ref)
|
||||
|
||||
url, err := findGitRemoteURL(context.Background(), basedir)
|
||||
url, err := findGitRemoteURL(t.Context(), basedir)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, remoteURL, url)
|
||||
|
||||
slug, err := FindGithubRepo(context.Background(), basedir, "github.com")
|
||||
slug, err := FindGithubRepo(t.Context(), basedir)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, "owner/repo", slug)
|
||||
}
|
||||
@@ -184,7 +190,7 @@ func TestGitFindRef(t *testing.T) {
|
||||
require.NoError(t, gitCmd("-C", dir, "init", "--initial-branch=master"))
|
||||
require.NoError(t, cleanGitHooks(dir))
|
||||
tt.Prepare(t, dir)
|
||||
ref, err := FindGitRef(context.Background(), dir)
|
||||
ref, err := FindGitRef(t.Context(), dir)
|
||||
tt.Assert(t, ref, err)
|
||||
})
|
||||
}
|
||||
@@ -258,17 +264,17 @@ func TestGitCloneExecutor(t *testing.T) {
|
||||
Dir: dir,
|
||||
})
|
||||
|
||||
err := clone(context.Background())
|
||||
err := clone(t.Context())
|
||||
if tt.Err != nil {
|
||||
assert.Error(t, err) //nolint:testifylint // pre-existing issue from nektos/act
|
||||
assert.Equal(t, tt.Err, err)
|
||||
assert.Equal(t, tt.Err, clone(context.Background()), "a retry on the cache keeps the error")
|
||||
assert.Equal(t, tt.Err, clone(t.Context()), "a retry on the cache keeps the error")
|
||||
return
|
||||
}
|
||||
assert.Empty(t, err) //nolint:testifylint // pre-existing issue from nektos/act
|
||||
marker := filepath.Join(dir, "marker")
|
||||
require.NoError(t, os.WriteFile(marker, nil, 0o644))
|
||||
require.NoError(t, clone(context.Background()))
|
||||
require.NoError(t, clone(t.Context()))
|
||||
assert.FileExists(t, marker, "an insteadOf rewrite must not evict the cache")
|
||||
script, err := os.ReadFile(filepath.Join(dir, "action.sh"))
|
||||
require.NoError(t, err)
|
||||
@@ -310,6 +316,12 @@ func TestGitCloneExecutorReclonesWhenOriginURLChanges(t *testing.T) {
|
||||
markerPath := filepath.Join(cacheDir, "stale-marker")
|
||||
require.NoError(t, os.WriteFile(markerPath, []byte("stale"), 0o644))
|
||||
|
||||
cancelled, cancel := context.WithCancel(t.Context())
|
||||
cancel()
|
||||
_, err := CloneIfRequired(cancelled, NewGitCloneExecutorInput{URL: oldRemoteDir, Ref: "main", Dir: cacheDir}, logging.New(logtest.New()))
|
||||
require.ErrorIs(t, err, context.Canceled)
|
||||
require.FileExists(t, markerPath)
|
||||
|
||||
require.NoError(t, NewGitCloneExecutor(NewGitCloneExecutorInput{
|
||||
URL: newRemoteDir,
|
||||
Ref: "main",
|
||||
@@ -341,19 +353,19 @@ func TestGitCloneExecutorFollowsRemoteRefChanges(t *testing.T) {
|
||||
|
||||
dir := t.TempDir()
|
||||
clone := NewGitCloneExecutor(NewGitCloneExecutorInput{URL: remoteDir, Ref: "main", Dir: dir})
|
||||
require.NoError(t, clone(context.Background()))
|
||||
require.NoError(t, clone(t.Context()))
|
||||
|
||||
require.NoError(t, gitCmd("-C", workDir, "reset", "--hard", "HEAD~1"))
|
||||
require.NoError(t, gitCmd("-C", workDir, "commit", "--allow-empty", "-m", "feature-rewritten"))
|
||||
require.NoError(t, gitCmd("-C", workDir, "push", "--force", "origin", "main"))
|
||||
require.NoError(t, clone(context.Background()), "a non-fast-forward ref must update")
|
||||
require.NoError(t, clone(t.Context()), "a non-fast-forward ref must update")
|
||||
assert.Equal(t, "feature-rewritten", gitHeadSubject(t, dir))
|
||||
|
||||
tagClone := NewGitCloneExecutor(NewGitCloneExecutorInput{URL: remoteDir, Ref: "v9", Dir: t.TempDir()})
|
||||
require.Error(t, tagClone(context.Background()))
|
||||
require.Error(t, tagClone(t.Context()))
|
||||
require.NoError(t, gitCmd("-C", workDir, "tag", "v9"))
|
||||
require.NoError(t, gitCmd("-C", workDir, "push", "origin", "v9"))
|
||||
require.NoError(t, tagClone(context.Background()), "a tag published after a failed lookup must be fetched")
|
||||
require.NoError(t, tagClone(t.Context()), "a tag published after a failed lookup must be fetched")
|
||||
}
|
||||
|
||||
func TestGitCloneExecutorOfflineMode(t *testing.T) {
|
||||
@@ -373,7 +385,7 @@ func TestGitCloneExecutorOfflineMode(t *testing.T) {
|
||||
URL: remoteDir,
|
||||
Ref: "main",
|
||||
Dir: cacheDir,
|
||||
})(context.Background()))
|
||||
})(t.Context()))
|
||||
|
||||
t.Run("cached branch resolves without fetching", func(t *testing.T) {
|
||||
// Offline reuse of a cached branch must succeed even though ResolveRevision(input.Ref)
|
||||
@@ -383,7 +395,7 @@ func TestGitCloneExecutorOfflineMode(t *testing.T) {
|
||||
Ref: "main",
|
||||
Dir: cacheDir,
|
||||
OfflineMode: true,
|
||||
})(context.Background())
|
||||
})(t.Context())
|
||||
require.NoError(t, err)
|
||||
|
||||
out, err := exec.Command("git", "-C", cacheDir, "log", "--oneline", "-1", "--format=%s").Output()
|
||||
@@ -398,7 +410,7 @@ func TestGitCloneExecutorOfflineMode(t *testing.T) {
|
||||
Ref: "never-fetched",
|
||||
Dir: cacheDir,
|
||||
OfflineMode: true,
|
||||
})(context.Background())
|
||||
})(t.Context())
|
||||
require.Error(t, err)
|
||||
})
|
||||
}
|
||||
@@ -416,9 +428,9 @@ func TestGitCloneExecutorQuietDemotesCloneLine(t *testing.T) {
|
||||
// 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} {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
logger, hook := logrustest.NewNullLogger()
|
||||
logger.SetLevel(log.InfoLevel)
|
||||
ctx := common.WithLogger(context.Background(), logger.WithField("job", "j1"))
|
||||
hook := logtest.New()
|
||||
logger := logging.New(logging.NewLevelHandler(hook, logging.LevelInfo))
|
||||
ctx := common.WithLogger(t.Context(), logger.With("job", "j1"))
|
||||
|
||||
require.NoError(t, NewGitCloneExecutor(NewGitCloneExecutorInput{
|
||||
URL: remoteDir,
|
||||
@@ -428,7 +440,7 @@ func TestGitCloneExecutorQuietDemotesCloneLine(t *testing.T) {
|
||||
})(ctx))
|
||||
|
||||
var cloneLines int
|
||||
for _, entry := range hook.AllEntries() {
|
||||
for _, entry := range hook.AllRecords() {
|
||||
if strings.HasPrefix(entry.Message, "git clone ") {
|
||||
cloneLines++
|
||||
}
|
||||
@@ -631,14 +643,42 @@ func gitCmd(args ...string) error {
|
||||
}
|
||||
|
||||
func TestAcquireCloneLock(t *testing.T) {
|
||||
t.Run("blocks on a lock held by another process despite a trailing separator", func(t *testing.T) {
|
||||
dir := filepath.Join(t.TempDir(), "cache")
|
||||
release, err := lock.TryLock(dir)
|
||||
require.NoError(t, err)
|
||||
defer func() { require.NoError(t, release()) }()
|
||||
synctest.Test(t, func(t *testing.T) {
|
||||
ctx, cancel := context.WithTimeout(t.Context(), time.Second)
|
||||
defer cancel()
|
||||
_, err := AcquireCloneLock(ctx, dir+string(filepath.Separator))
|
||||
require.ErrorIs(t, err, context.DeadlineExceeded)
|
||||
})
|
||||
})
|
||||
|
||||
t.Run("read-only parent falls back to an in-process lock", func(t *testing.T) {
|
||||
parent := t.TempDir()
|
||||
dir := filepath.Join(parent, "cache")
|
||||
require.NoError(t, os.Mkdir(dir, 0o755))
|
||||
require.NoError(t, os.Chmod(parent, 0o555))
|
||||
defer func() { require.NoError(t, os.Chmod(parent, 0o755)) }()
|
||||
unlock, err := AcquireCloneLock(t.Context(), dir)
|
||||
require.NoError(t, err)
|
||||
unlock()
|
||||
})
|
||||
|
||||
t.Run("same directory serializes", func(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
|
||||
unlock1 := AcquireCloneLock(dir)
|
||||
unlock1, err := AcquireCloneLock(t.Context(), dir)
|
||||
require.NoError(t, err)
|
||||
|
||||
secondAcquired := make(chan struct{})
|
||||
go func() {
|
||||
unlock := AcquireCloneLock(dir)
|
||||
unlock, err := AcquireCloneLock(t.Context(), dir)
|
||||
if !assert.NoError(t, err) {
|
||||
return
|
||||
}
|
||||
close(secondAcquired)
|
||||
unlock()
|
||||
}()
|
||||
@@ -662,12 +702,16 @@ func TestAcquireCloneLock(t *testing.T) {
|
||||
dirA := t.TempDir()
|
||||
dirB := t.TempDir()
|
||||
|
||||
unlockA := AcquireCloneLock(dirA)
|
||||
unlockA, err := AcquireCloneLock(t.Context(), dirA)
|
||||
require.NoError(t, err)
|
||||
defer unlockA()
|
||||
|
||||
done := make(chan struct{})
|
||||
go func() {
|
||||
unlock := AcquireCloneLock(dirB)
|
||||
unlock, err := AcquireCloneLock(t.Context(), dirB)
|
||||
if !assert.NoError(t, err) {
|
||||
return
|
||||
}
|
||||
unlock()
|
||||
close(done)
|
||||
}()
|
||||
|
||||
+30
-23
@@ -7,46 +7,53 @@ package common
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/sirupsen/logrus"
|
||||
"gitea.com/gitea/runner/act/common/logging"
|
||||
)
|
||||
|
||||
type loggerContextKey string
|
||||
|
||||
const loggerContextKeyVal = loggerContextKey("logrus.FieldLogger")
|
||||
const loggerContextKeyVal = loggerContextKey("logging.Logger")
|
||||
|
||||
// Logger returns the appropriate logger for current context
|
||||
func Logger(ctx context.Context) logrus.FieldLogger {
|
||||
val := ctx.Value(loggerContextKeyVal)
|
||||
if val != nil {
|
||||
if logger, ok := val.(logrus.FieldLogger); ok {
|
||||
return logger
|
||||
}
|
||||
func Logger(ctx context.Context) *logging.Logger {
|
||||
if logger, ok := ctx.Value(loggerContextKeyVal).(*logging.Logger); ok {
|
||||
return logger
|
||||
}
|
||||
return logrus.StandardLogger()
|
||||
return logging.Default()
|
||||
}
|
||||
|
||||
// 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)
|
||||
}
|
||||
|
||||
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
|
||||
// the hook affects job logger, not global logger
|
||||
func LoggerHook(ctx context.Context) logrus.Hook {
|
||||
val := ctx.Value(loggerHookKeyVal)
|
||||
if val != nil {
|
||||
if hook, ok := val.(logrus.Hook); ok {
|
||||
return hook
|
||||
}
|
||||
// LoggerHandler returns the extra handler the job logger sends its records to, or nil.
|
||||
// It affects the job logger, not the global logger.
|
||||
func LoggerHandler(ctx context.Context) logging.Handler {
|
||||
if h, ok := ctx.Value(loggerHandlerKeyVal).(logging.Handler); ok {
|
||||
return h
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// WithLoggerHook adds a value to the context for the logger hook
|
||||
func WithLoggerHook(ctx context.Context, hook logrus.Hook) context.Context {
|
||||
return context.WithValue(ctx, loggerHookKeyVal, hook)
|
||||
// WithLoggerHandler adds the extra handler for the job logger to the context
|
||||
func WithLoggerHandler(ctx context.Context, h logging.Handler) context.Context {
|
||||
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,287 @@
|
||||
// Copyright 2026 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package logging
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"log/slog"
|
||||
"reflect"
|
||||
"runtime"
|
||||
"slices"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
const (
|
||||
ansiReset = "\x1b[0m"
|
||||
ansiRed = "\x1b[31m"
|
||||
ansiYellow = "\x1b[33m"
|
||||
ansiCyan = "\x1b[36m"
|
||||
ansiDimCyan = "\x1b[2;36m"
|
||||
ansiDimWhite = "\x1b[2;37m"
|
||||
)
|
||||
|
||||
// consoleStart is the zero of the elapsed-seconds stamp used when full timestamps are off.
|
||||
var consoleStart = time.Now()
|
||||
|
||||
// console is the state shared by every consoleHandler derived from one another.
|
||||
type console struct {
|
||||
mu sync.Mutex
|
||||
out io.Writer
|
||||
terminal bool
|
||||
opts TextOptions
|
||||
reportCaller bool
|
||||
level slog.LevelVar
|
||||
}
|
||||
|
||||
func newConsole(out io.Writer) *console {
|
||||
c := &console{}
|
||||
c.setOutput(out)
|
||||
return c
|
||||
}
|
||||
|
||||
func (c *console) setOutput(w io.Writer) {
|
||||
terminal := checkIfTerminal(w)
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
c.out, c.terminal = w, terminal
|
||||
}
|
||||
|
||||
// consoleHandler is a slog.Handler writing the text format the runner has always used, so that
|
||||
// moving off logrus changes nothing a user can see: sorted key=value fields on a plain stream,
|
||||
// and a colored prefix with elapsed or full time on a terminal.
|
||||
type consoleHandler struct {
|
||||
c *console
|
||||
attrs []slog.Attr
|
||||
}
|
||||
|
||||
func (h *consoleHandler) Enabled(_ context.Context, level slog.Level) bool {
|
||||
return level >= h.c.level.Level()
|
||||
}
|
||||
|
||||
func (h *consoleHandler) WithAttrs(attrs []slog.Attr) slog.Handler {
|
||||
return &consoleHandler{c: h.c, attrs: append(slices.Clone(h.attrs), attrs...)}
|
||||
}
|
||||
|
||||
// WithGroup is a no-op, as only NewSlogHandler wraps a consoleHandler and it never groups.
|
||||
func (h *consoleHandler) WithGroup(string) slog.Handler { return h }
|
||||
|
||||
func (h *consoleHandler) Handle(_ context.Context, r slog.Record) error {
|
||||
c := h.c
|
||||
c.mu.Lock()
|
||||
opts, reportCaller, terminal := c.opts, c.reportCaller, c.terminal
|
||||
c.mu.Unlock()
|
||||
|
||||
data := make(map[string]any, len(h.attrs)+r.NumAttrs())
|
||||
for _, a := range h.attrs {
|
||||
data[a.Key] = a.Value.Resolve().Any()
|
||||
}
|
||||
r.Attrs(func(a slog.Attr) bool {
|
||||
data[a.Key] = a.Value.Resolve().Any()
|
||||
return true
|
||||
})
|
||||
|
||||
var caller *runtime.Frame
|
||||
if reportCaller && r.PC != 0 {
|
||||
frame, _ := runtime.CallersFrames([]uintptr{r.PC}).Next()
|
||||
caller = &frame
|
||||
}
|
||||
prefixFieldClashes(data, caller != nil)
|
||||
|
||||
keys := make([]string, 0, len(data))
|
||||
for k := range data {
|
||||
keys = append(keys, k)
|
||||
}
|
||||
slices.Sort(keys)
|
||||
|
||||
var b bytes.Buffer
|
||||
if terminal && !opts.DisableColors {
|
||||
printColored(&b, opts, r, caller, keys, data)
|
||||
} else {
|
||||
printPlain(&b, opts, r, caller, keys, data)
|
||||
}
|
||||
|
||||
line := b.Bytes()
|
||||
if opts.Filter != nil {
|
||||
line = opts.Filter(line)
|
||||
}
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
_, err := c.out.Write(line)
|
||||
return err
|
||||
}
|
||||
|
||||
// prefixFieldClashes moves fields that share a name with a fixed key out of its way.
|
||||
func prefixFieldClashes(data map[string]any, reportCaller bool) {
|
||||
for _, key := range []string{"time", "level", "msg"} {
|
||||
if v, ok := data[key]; ok {
|
||||
data["fields."+key] = v
|
||||
delete(data, key)
|
||||
}
|
||||
}
|
||||
if reportCaller {
|
||||
for _, key := range []string{"func", "file"} {
|
||||
if v, ok := data[key]; ok {
|
||||
data["fields."+key] = v
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func printPlain(b *bytes.Buffer, opts TextOptions, r slog.Record, caller *runtime.Frame, keys []string, data map[string]any) {
|
||||
if !opts.DisableTimestamp {
|
||||
appendKeyValue(b, "time", r.Time.Format(time.RFC3339))
|
||||
}
|
||||
appendKeyValue(b, "level", plainLevel(Level(r.Level)))
|
||||
if r.Message != "" {
|
||||
appendKeyValue(b, "msg", r.Message)
|
||||
}
|
||||
if caller != nil {
|
||||
funcVal, fileVal := callerText(opts, caller)
|
||||
if funcVal != "" {
|
||||
appendKeyValue(b, "func", funcVal)
|
||||
}
|
||||
if fileVal != "" {
|
||||
appendKeyValue(b, "file", fileVal)
|
||||
}
|
||||
}
|
||||
for _, k := range keys {
|
||||
appendKeyValue(b, k, data[k])
|
||||
}
|
||||
b.WriteByte('\n')
|
||||
}
|
||||
|
||||
func callerText(opts TextOptions, caller *runtime.Frame) (funcVal, fileVal string) {
|
||||
if opts.CallerPrettyfier != nil {
|
||||
return opts.CallerPrettyfier(caller)
|
||||
}
|
||||
return caller.Function, caller.File + ":" + strconv.Itoa(caller.Line)
|
||||
}
|
||||
|
||||
func printColored(b *bytes.Buffer, opts TextOptions, r slog.Record, caller *runtime.Frame, keys []string, data map[string]any) {
|
||||
msg := strings.TrimSuffix(r.Message, "\n")
|
||||
|
||||
var callerStr string
|
||||
if caller != nil {
|
||||
funcVal, fileVal := callerText(opts, caller)
|
||||
if opts.CallerPrettyfier == nil && caller.Function != "" {
|
||||
funcVal += "()"
|
||||
}
|
||||
switch {
|
||||
case fileVal == "":
|
||||
callerStr = funcVal
|
||||
case funcVal == "":
|
||||
callerStr = fileVal
|
||||
default:
|
||||
callerStr = fileVal + " " + funcVal
|
||||
}
|
||||
}
|
||||
|
||||
level := Level(r.Level)
|
||||
prefix := colorize(level, strings.ToUpper(plainLevel(level))[:4])
|
||||
switch {
|
||||
case opts.DisableTimestamp:
|
||||
fmt.Fprintf(b, "%s%s %-44s ", prefix, callerStr, msg)
|
||||
case !opts.FullTimestamp:
|
||||
fmt.Fprintf(b, "%s[%04d]%s %-44s ", prefix, int(r.Time.Sub(consoleStart)/time.Second), callerStr, msg)
|
||||
default:
|
||||
fmt.Fprintf(b, "%s[%s]%s %-44s ", prefix, r.Time.Format(time.RFC3339), callerStr, msg)
|
||||
}
|
||||
for _, k := range keys {
|
||||
b.WriteByte(' ')
|
||||
b.WriteString(colorize(level, k))
|
||||
b.WriteByte('=')
|
||||
appendValue(b, data[k])
|
||||
}
|
||||
b.WriteByte('\n')
|
||||
}
|
||||
|
||||
// plainLevel spells Warn the way logrus did, as "warning".
|
||||
func plainLevel(l Level) string {
|
||||
if s := l.String(); s != "warn" {
|
||||
return s
|
||||
}
|
||||
return "warning"
|
||||
}
|
||||
|
||||
func colorize(l Level, s string) string {
|
||||
var color string
|
||||
switch {
|
||||
case l <= LevelTrace:
|
||||
color = ansiDimWhite
|
||||
case l <= LevelDebug:
|
||||
color = ansiDimCyan
|
||||
case l <= LevelInfo:
|
||||
color = ansiCyan
|
||||
case l <= LevelWarn:
|
||||
color = ansiYellow
|
||||
default:
|
||||
color = ansiRed
|
||||
}
|
||||
return color + s + ansiReset
|
||||
}
|
||||
|
||||
func appendKeyValue(b *bytes.Buffer, key string, value any) {
|
||||
if b.Len() > 0 {
|
||||
b.WriteByte(' ')
|
||||
}
|
||||
b.WriteString(key)
|
||||
b.WriteByte('=')
|
||||
appendValue(b, value)
|
||||
}
|
||||
|
||||
func appendValue(b *bytes.Buffer, value any) {
|
||||
switch v := value.(type) {
|
||||
case string:
|
||||
appendString(b, v)
|
||||
case []byte:
|
||||
appendString(b, string(v))
|
||||
case error:
|
||||
appendFormatted(b, v, v.Error)
|
||||
case fmt.Stringer:
|
||||
appendFormatted(b, v, v.String)
|
||||
default:
|
||||
appendString(b, fmt.Sprint(value))
|
||||
}
|
||||
}
|
||||
|
||||
// appendFormatted writes what get returns, and "<nil>" if that panics on a nil receiver.
|
||||
func appendFormatted(b *bytes.Buffer, v any, get func() string) {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
if rv := reflect.ValueOf(v); rv.Kind() == reflect.Pointer && rv.IsNil() {
|
||||
appendString(b, "<nil>")
|
||||
} else {
|
||||
appendString(b, fmt.Sprintf("%%!v(PANIC=%v)", r))
|
||||
}
|
||||
}
|
||||
}()
|
||||
appendString(b, get())
|
||||
}
|
||||
|
||||
func appendString(b *bytes.Buffer, s string) {
|
||||
if needsQuoting(s) {
|
||||
b.WriteString(strconv.Quote(s))
|
||||
return
|
||||
}
|
||||
b.WriteString(s)
|
||||
}
|
||||
|
||||
func needsQuoting(s string) bool {
|
||||
for i := range len(s) {
|
||||
c := s[i]
|
||||
switch {
|
||||
case c >= 'a' && c <= 'z', c >= 'A' && c <= 'Z', c >= '0' && c <= '9':
|
||||
case c == '-', c == '.', c == '_', c == '/', c == '@', c == '^', c == '+':
|
||||
default:
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
// Copyright 2026 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package logging
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"log/slog"
|
||||
"runtime"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func consoleLine(t *testing.T, opts TextOptions, terminal, reportCaller bool, build func(*Logger)) string {
|
||||
t.Helper()
|
||||
var out bytes.Buffer
|
||||
c := newConsole(&out)
|
||||
c.opts, c.terminal, c.reportCaller = opts, terminal, reportCaller
|
||||
c.level.Set(slog.LevelDebug)
|
||||
build(New(NewSlogHandler(&consoleHandler{c: c})))
|
||||
return out.String()
|
||||
}
|
||||
|
||||
func TestConsolePlain(t *testing.T) {
|
||||
line := consoleLine(t, TextOptions{DisableTimestamp: true}, false, false, func(l *Logger) {
|
||||
l.With("job", "build x", "n", 3).WithError(errors.New("boom")).Warnf("hello world")
|
||||
l.With("msg", "clash", "empty", "").Info("")
|
||||
})
|
||||
assert.Equal(t, "level=warning msg=\"hello world\" error=boom job=\"build x\" n=3\n"+
|
||||
"level=info empty= fields.msg=clash\n", line)
|
||||
}
|
||||
|
||||
func TestConsolePlainTimeFilterAndCaller(t *testing.T) {
|
||||
opts := TextOptions{
|
||||
CallerPrettyfier: func(f *runtime.Frame) (string, string) {
|
||||
return "[fn]", "[" + strings.TrimPrefix(f.Function, "x") + "]"
|
||||
},
|
||||
Filter: func(b []byte) []byte { return bytes.ReplaceAll(b, []byte("secret"), []byte("***")) },
|
||||
}
|
||||
line := consoleLine(t, opts, false, true, func(l *Logger) { l.With("k", "secret").Debugf("a secret") })
|
||||
assert.Regexp(t, `^time="[^"]+" level=debug msg="a \*\*\*" func="\[fn\]" file="\[.*TestConsolePlainTimeFilterAndCaller[^"]*\]" k=\*\*\*\n$`, line)
|
||||
}
|
||||
|
||||
func TestConsoleColored(t *testing.T) {
|
||||
line := consoleLine(t, TextOptions{DisableTimestamp: true}, true, false, func(l *Logger) {
|
||||
l.With("job", "j").Warn("careful")
|
||||
})
|
||||
assert.Equal(t, "\x1b[33mWARN\x1b[0m "+"careful"+strings.Repeat(" ", 44-len("careful"))+" \x1b[33mjob\x1b[0m=j\n", line)
|
||||
|
||||
full := consoleLine(t, TextOptions{FullTimestamp: true}, true, false, func(l *Logger) { l.Error("bad") })
|
||||
assert.Regexp(t, `^\x1b\[31mERRO\x1b\[0m\[\d{4}-\d\d-\d\dT[^\]]+\] bad`, full)
|
||||
|
||||
elapsed := consoleLine(t, TextOptions{}, true, false, func(l *Logger) { l.Info("x") })
|
||||
assert.Regexp(t, `^\x1b\[36mINFO\x1b\[0m\[\d{4}\] x`, elapsed)
|
||||
|
||||
plain := consoleLine(t, TextOptions{DisableColors: true, DisableTimestamp: true}, true, false, func(l *Logger) { l.Info("x") })
|
||||
assert.Equal(t, "level=info msg=x\n", plain)
|
||||
}
|
||||
|
||||
func TestConsoleLevelAndOutput(t *testing.T) {
|
||||
var out bytes.Buffer
|
||||
c := newConsole(&out)
|
||||
h := &consoleHandler{c: c}
|
||||
assert.False(t, h.Enabled(t.Context(), slog.LevelDebug)) // info unless raised
|
||||
c.level.Set(slog.LevelError)
|
||||
assert.False(t, h.Enabled(t.Context(), slog.LevelWarn))
|
||||
assert.True(t, h.Enabled(t.Context(), slog.LevelError))
|
||||
|
||||
rec := slog.NewRecord(time.Unix(0, 0), slog.LevelError, "m", 0)
|
||||
rec.AddAttrs(slog.String("c", "3"))
|
||||
require.NoError(t, h.WithAttrs([]slog.Attr{slog.Int("a", 1)}).Handle(t.Context(), rec))
|
||||
assert.Contains(t, out.String(), "a=1 c=3")
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
// Copyright 2026 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package logging
|
||||
|
||||
import (
|
||||
"io"
|
||||
"log/slog"
|
||||
"os"
|
||||
"runtime"
|
||||
"sync/atomic"
|
||||
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
// TextOptions describes how the process-wide Logger writes a record as text.
|
||||
type TextOptions struct {
|
||||
DisableColors bool
|
||||
DisableTimestamp bool
|
||||
FullTimestamp bool
|
||||
// CallerPrettyfier renames the function and file reported with a record when ReportCaller is on.
|
||||
CallerPrettyfier func(*runtime.Frame) (function, file string)
|
||||
// Filter rewrites each formatted line, for example to mask secrets.
|
||||
Filter func(line []byte) []byte
|
||||
}
|
||||
|
||||
var (
|
||||
std atomic.Pointer[Logger]
|
||||
stdState = newConsole(os.Stderr)
|
||||
)
|
||||
|
||||
func init() {
|
||||
std.Store(New(NewSlogHandler(&consoleHandler{c: stdState})))
|
||||
}
|
||||
|
||||
// Default returns the process-wide Logger.
|
||||
func Default() *Logger { return std.Load() }
|
||||
|
||||
// SetDefault replaces the process-wide Logger, which tests use to capture what the package
|
||||
// functions log. SetLevel, GetLevel, SetOutput and the format functions keep configuring the
|
||||
// initial console Logger.
|
||||
func SetDefault(l *Logger) { std.Store(l) }
|
||||
|
||||
// SetLevel sets the minimum level of the initial process-wide Logger and of what libraries log through logrus.
|
||||
func SetLevel(level Level) {
|
||||
stdState.level.Set(slog.Level(level))
|
||||
logrus.SetLevel(toLogrusLevel(level))
|
||||
}
|
||||
|
||||
// GetLevel returns the minimum level of the initial process-wide Logger.
|
||||
func GetLevel() Level { return Level(stdState.level.Level()) }
|
||||
|
||||
// SetOutput sets where the initial process-wide Logger writes.
|
||||
func SetOutput(w io.Writer) { stdState.setOutput(w) }
|
||||
|
||||
// Output returns where the initial process-wide Logger writes.
|
||||
func Output() io.Writer {
|
||||
stdState.mu.Lock()
|
||||
defer stdState.mu.Unlock()
|
||||
return stdState.out
|
||||
}
|
||||
|
||||
// SetTextFormat sets the text format of the initial process-wide Logger.
|
||||
func SetTextFormat(o TextOptions) {
|
||||
stdState.mu.Lock()
|
||||
defer stdState.mu.Unlock()
|
||||
stdState.opts = o
|
||||
}
|
||||
|
||||
// SetReportCaller makes the initial process-wide Logger record where each call came from, which costs time.
|
||||
func SetReportCaller(on bool) {
|
||||
stdState.mu.Lock()
|
||||
defer stdState.mu.Unlock()
|
||||
stdState.reportCaller = on
|
||||
logrus.SetReportCaller(on)
|
||||
}
|
||||
|
||||
func Enabled(level Level) bool { return Default().Enabled(level) }
|
||||
|
||||
func With(args ...any) *Logger { return Default().With(args...) }
|
||||
func WithError(err error) *Logger { return Default().WithError(err) }
|
||||
func Tracef(format string, args ...any) { Default().logf(LevelTrace, format, args) }
|
||||
func Debugf(format string, args ...any) { Default().logf(LevelDebug, format, args) }
|
||||
func Infof(format string, args ...any) { Default().logf(LevelInfo, format, args) }
|
||||
func Warnf(format string, args ...any) { Default().logf(LevelWarn, format, args) }
|
||||
func Errorf(format string, args ...any) { Default().logf(LevelError, format, args) }
|
||||
func Trace(args ...any) { Default().log(LevelTrace, args) }
|
||||
func Debug(args ...any) { Default().log(LevelDebug, args) }
|
||||
func Info(args ...any) { Default().log(LevelInfo, args) }
|
||||
func Warn(args ...any) { Default().log(LevelWarn, args) }
|
||||
func Error(args ...any) { Default().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,203 @@
|
||||
// Copyright 2026 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package logging_test
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"log/slog"
|
||||
"runtime"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"gitea.com/gitea/runner/act/common/logging"
|
||||
"gitea.com/gitea/runner/act/common/logging/logtest"
|
||||
|
||||
"github.com/sirupsen/logrus"
|
||||
"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) {
|
||||
l, h := logtest.NewLogger()
|
||||
l.Infof("method")
|
||||
l.Info("method")
|
||||
|
||||
prev := logging.Default()
|
||||
t.Cleanup(func() { logging.SetDefault(prev) })
|
||||
logging.SetDefault(l)
|
||||
logging.Infof("global")
|
||||
logging.Error("global")
|
||||
|
||||
require.Len(t, h.AllRecords(), 4)
|
||||
for _, r := range h.AllRecords() {
|
||||
assert.True(t, strings.HasSuffix(frameOf(t, r.PC).Function, "TestCallerPC"), r.Message)
|
||||
}
|
||||
}
|
||||
|
||||
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))
|
||||
}
|
||||
|
||||
func TestSlogHandler(t *testing.T) {
|
||||
var buf strings.Builder
|
||||
slogger := slog.New(slog.NewTextHandler(&buf, &slog.HandlerOptions{Level: slog.LevelDebug}))
|
||||
l := logging.New(logging.NewSlogHandler(slogger.Handler())).With("job", "j1")
|
||||
|
||||
l.Tracef("dropped")
|
||||
assert.False(t, l.Enabled(logging.LevelTrace))
|
||||
l.WithError(errors.New("boom")).Errorf("fail %s", "x")
|
||||
|
||||
out := buf.String()
|
||||
assert.Contains(t, out, "level=ERROR")
|
||||
assert.Contains(t, out, `msg="fail x"`)
|
||||
assert.Contains(t, out, "job=j1")
|
||||
assert.Contains(t, out, "error=boom")
|
||||
}
|
||||
|
||||
func TestLogrusBridge(t *testing.T) {
|
||||
prevLevel, prevDefault := logging.GetLevel(), logging.Default()
|
||||
t.Cleanup(func() {
|
||||
logging.SetLevel(prevLevel)
|
||||
logging.SetReportCaller(false)
|
||||
logging.SetDefault(prevDefault)
|
||||
})
|
||||
logging.SetLevel(logging.LevelInfo)
|
||||
assert.False(t, logrus.IsLevelEnabled(logrus.DebugLevel))
|
||||
logging.SetLevel(logging.LevelTrace)
|
||||
logging.SetReportCaller(true)
|
||||
h := logtest.NewWithLevel(logging.LevelTrace)
|
||||
logging.SetDefault(logging.New(h))
|
||||
|
||||
logrus.WithField("lib", "docker").Warnf("from a library %d", 1)
|
||||
_, _, line, _ := runtime.Caller(0)
|
||||
logrus.Debug("below")
|
||||
logrus.Trace("lowest")
|
||||
|
||||
recs := h.AllRecords()
|
||||
require.Len(t, recs, 3)
|
||||
assert.Equal(t, "from a library 1", recs[0].Message)
|
||||
assert.Equal(t, logging.LevelWarn, recs[0].Level)
|
||||
v, _ := recs[0].Attr("lib")
|
||||
assert.Equal(t, "docker", v)
|
||||
assert.Equal(t, logging.LevelDebug, recs[1].Level)
|
||||
f := frameOf(t, recs[1].PC)
|
||||
assert.True(t, strings.HasSuffix(f.Function, "TestLogrusBridge"), f.Function)
|
||||
assert.Equal(t, line+1, f.Line)
|
||||
assert.Equal(t, logging.LevelTrace, recs[2].Level)
|
||||
|
||||
h = logtest.New() // logrus passes Debug, the Logger does not
|
||||
logging.SetDefault(logging.New(h))
|
||||
logrus.Debug("dropped")
|
||||
assert.Empty(t, h.AllRecords())
|
||||
}
|
||||
|
||||
func TestSlogBridge(t *testing.T) {
|
||||
h := logtest.SetDefault(t)
|
||||
slog.With("lib", "actionslib").Warn("from a library", "n", 1)
|
||||
_, _, line, _ := runtime.Caller(0)
|
||||
slog.Debug("dropped")
|
||||
|
||||
recs := h.AllRecords()
|
||||
require.Len(t, recs, 1)
|
||||
assert.Equal(t, "from a library", recs[0].Message)
|
||||
assert.Equal(t, logging.LevelWarn, recs[0].Level)
|
||||
lib, _ := recs[0].Attr("lib")
|
||||
assert.Equal(t, "actionslib", lib)
|
||||
n, _ := recs[0].Attr("n")
|
||||
assert.EqualValues(t, 1, n)
|
||||
assert.Equal(t, line-1, frameOf(t, recs[0].PC).Line)
|
||||
}
|
||||
|
||||
func TestLogrusBridgeIntact(t *testing.T) {
|
||||
assert.True(t, logging.LogrusBridgeIntact())
|
||||
|
||||
prev := logrus.StandardLogger().Out
|
||||
t.Cleanup(func() { logrus.SetOutput(prev) })
|
||||
logrus.SetOutput(&strings.Builder{}) // what a dependency doing this would do
|
||||
assert.False(t, logging.LogrusBridgeIntact())
|
||||
logrus.SetOutput(prev)
|
||||
assert.True(t, logging.LogrusBridgeIntact())
|
||||
|
||||
prevLevel := logrus.GetLevel()
|
||||
t.Cleanup(func() { logrus.SetLevel(prevLevel) })
|
||||
logrus.SetLevel(logrus.PanicLevel)
|
||||
assert.False(t, logging.LogrusBridgeIntact())
|
||||
}
|
||||
@@ -0,0 +1,102 @@
|
||||
// Copyright 2026 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package logging
|
||||
|
||||
import (
|
||||
"context"
|
||||
"io"
|
||||
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
// Libraries the runner depends on, such as docker/cli and moby/go-archive, log through
|
||||
// logrus's global logger. Hand what they log to the process-wide Logger, so it follows the same
|
||||
// level, format and secret masking as the rest of the output.
|
||||
func init() {
|
||||
logrus.SetOutput(io.Discard)
|
||||
logrus.SetFormatter(discardFormatter{})
|
||||
logrus.SetLevel(toLogrusLevel(GetLevel()))
|
||||
logrus.AddHook(bridgeHook{})
|
||||
}
|
||||
|
||||
type discardFormatter struct{}
|
||||
|
||||
func (discardFormatter) Format(*logrus.Entry) ([]byte, error) { return nil, nil }
|
||||
|
||||
type bridgeHook struct{}
|
||||
|
||||
func (bridgeHook) Levels() []logrus.Level { return logrus.AllLevels }
|
||||
|
||||
func (bridgeHook) Fire(e *logrus.Entry) error {
|
||||
l := Default()
|
||||
level := fromLogrusLevel(e.Level)
|
||||
ctx := e.Context
|
||||
if ctx == nil {
|
||||
ctx = context.Background()
|
||||
}
|
||||
if !l.h.Enabled(ctx, level) {
|
||||
return nil
|
||||
}
|
||||
attrs := make([]Attr, 0, len(e.Data))
|
||||
for k, v := range e.Data {
|
||||
attrs = append(attrs, Attr{Key: k, Value: v})
|
||||
}
|
||||
r := Record{Time: e.Time, Level: level, Message: e.Message, Attrs: attrs}
|
||||
if e.Caller != nil {
|
||||
r.PC = e.Caller.PC + 1 // Frame.PC is one below the return address Record.PC holds
|
||||
}
|
||||
return l.h.Handle(ctx, r)
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
// LogrusBridgeIntact reports whether logrus's global logger is still set up as the bridge needs,
|
||||
// discarding its own output and following the process-wide level. A dependency that calls
|
||||
// logrus.SetOutput or SetLevel breaks that, and its lines then show up twice, or at the wrong level.
|
||||
func LogrusBridgeIntact() bool {
|
||||
std := logrus.StandardLogger()
|
||||
if std.Out != io.Discard || std.GetLevel() != toLogrusLevel(GetLevel()) {
|
||||
return false
|
||||
}
|
||||
for _, level := range logrus.AllLevels {
|
||||
found := false
|
||||
for _, h := range std.Hooks[level] {
|
||||
if _, ok := h.(bridgeHook); ok {
|
||||
found = true
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
// 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"
|
||||
"testing"
|
||||
|
||||
"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)
|
||||
}
|
||||
|
||||
// NewLogger returns a Logger writing to a new Handler from New, and that Handler.
|
||||
func NewLogger() (*logging.Logger, *Handler) {
|
||||
h := New()
|
||||
return logging.New(h), h
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
// SetDefault makes the process-wide Logger record into a new Handler until the test ends.
|
||||
func SetDefault(t testing.TB) *Handler {
|
||||
t.Helper()
|
||||
prev := logging.Default()
|
||||
logger, h := NewLogger()
|
||||
logging.SetDefault(logger)
|
||||
t.Cleanup(func() { logging.SetDefault(prev) })
|
||||
return h
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
// Copyright 2026 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package logging
|
||||
|
||||
import (
|
||||
"context"
|
||||
"log/slog"
|
||||
"slices"
|
||||
)
|
||||
|
||||
func init() {
|
||||
slog.SetDefault(slog.New(slogBridge{}))
|
||||
}
|
||||
|
||||
// slogBridge hands what libraries log through slog's global logger to the process-wide Logger.
|
||||
type slogBridge struct {
|
||||
attrs []Attr
|
||||
}
|
||||
|
||||
func (b slogBridge) Enabled(ctx context.Context, level slog.Level) bool {
|
||||
return Default().h.Enabled(ctx, Level(level))
|
||||
}
|
||||
|
||||
func (b slogBridge) Handle(ctx context.Context, r slog.Record) error {
|
||||
h := Default().h
|
||||
if len(b.attrs) > 0 {
|
||||
h = h.WithAttrs(b.attrs)
|
||||
}
|
||||
attrs := make([]Attr, 0, r.NumAttrs())
|
||||
r.Attrs(func(a slog.Attr) bool {
|
||||
attrs = append(attrs, fromSlogAttr(a))
|
||||
return true
|
||||
})
|
||||
return h.Handle(ctx, Record{Time: r.Time, Level: Level(r.Level), Message: r.Message, PC: r.PC, Attrs: attrs})
|
||||
}
|
||||
|
||||
func (b slogBridge) WithAttrs(attrs []slog.Attr) slog.Handler {
|
||||
out := slices.Clone(b.attrs)
|
||||
for _, a := range attrs {
|
||||
out = append(out, fromSlogAttr(a))
|
||||
}
|
||||
return slogBridge{attrs: out}
|
||||
}
|
||||
|
||||
func (b slogBridge) WithGroup(string) slog.Handler { return b }
|
||||
|
||||
func fromSlogAttr(a slog.Attr) Attr {
|
||||
return Attr{Key: a.Key, Value: a.Value.Resolve().Any()}
|
||||
}
|
||||
|
||||
type slogHandler struct {
|
||||
h slog.Handler
|
||||
}
|
||||
|
||||
// NewSlogHandler writes to h. Levels map one to one, as Level uses slog's values.
|
||||
func NewSlogHandler(h slog.Handler) Handler {
|
||||
return slogHandler{h: h}
|
||||
}
|
||||
|
||||
func (s slogHandler) Enabled(ctx context.Context, level Level) bool {
|
||||
return s.h.Enabled(ctx, slog.Level(level))
|
||||
}
|
||||
|
||||
func (s slogHandler) Handle(ctx context.Context, r Record) error {
|
||||
rec := slog.NewRecord(r.Time, slog.Level(r.Level), r.Message, r.PC)
|
||||
rec.AddAttrs(toSlogAttrs(r.Attrs)...)
|
||||
return s.h.Handle(ctx, rec)
|
||||
}
|
||||
|
||||
func (s slogHandler) WithAttrs(attrs []Attr) Handler {
|
||||
return slogHandler{h: s.h.WithAttrs(toSlogAttrs(attrs))}
|
||||
}
|
||||
|
||||
func toSlogAttrs(attrs []Attr) []slog.Attr {
|
||||
out := make([]slog.Attr, len(attrs))
|
||||
for i, a := range attrs {
|
||||
out[i] = slog.Any(a.Key, a.Value)
|
||||
}
|
||||
return out
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
// Copyright 2026 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
//go:build !windows
|
||||
|
||||
package logging
|
||||
|
||||
import (
|
||||
"io"
|
||||
"os"
|
||||
|
||||
"golang.org/x/term"
|
||||
)
|
||||
|
||||
func checkIfTerminal(w io.Writer) bool {
|
||||
if f, ok := w.(*os.File); ok {
|
||||
return term.IsTerminal(int(f.Fd()))
|
||||
}
|
||||
return false
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
// Copyright 2026 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
//go:build windows
|
||||
|
||||
package logging
|
||||
|
||||
import (
|
||||
"io"
|
||||
"os"
|
||||
|
||||
"golang.org/x/sys/windows"
|
||||
)
|
||||
|
||||
// checkIfTerminal also turns on virtual terminal processing, which the console needs to show colors.
|
||||
func checkIfTerminal(w io.Writer) bool {
|
||||
f, ok := w.(*os.File)
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
handle := windows.Handle(f.Fd())
|
||||
var mode uint32
|
||||
if err := windows.GetConsoleMode(handle, &mode); err != nil {
|
||||
return false
|
||||
}
|
||||
return windows.SetConsoleMode(handle, mode|windows.ENABLE_VIRTUAL_TERMINAL_PROCESSING) == nil
|
||||
}
|
||||
@@ -8,10 +8,10 @@ package container
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
cerrdefs "github.com/containerd/errdefs"
|
||||
"github.com/moby/moby/client"
|
||||
specs "github.com/opencontainers/image-spec/specs-go/v1"
|
||||
)
|
||||
|
||||
// ImageExistsLocally returns a boolean indicating if an image with the
|
||||
@@ -30,11 +30,23 @@ func ImageExistsLocally(ctx context.Context, imageName, platform string) (bool,
|
||||
return false, err
|
||||
}
|
||||
|
||||
if platform == "" || platform == "any" || fmt.Sprintf("%s/%s", inspectImage.Os, inspectImage.Architecture) == platform {
|
||||
if platform == "" || platform == "any" {
|
||||
return true, nil
|
||||
}
|
||||
|
||||
return false, nil
|
||||
requested, err := parsePlatform(platform)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
return platformMatches(requested, &specs.Platform{OS: inspectImage.Os, Architecture: inspectImage.Architecture, Variant: inspectImage.Variant}), nil
|
||||
}
|
||||
|
||||
func platformMatches(requested, image *specs.Platform) bool {
|
||||
variant := image.Variant
|
||||
if variant == "" {
|
||||
variant = map[string]string{"amd64": "v1", "arm": "v7", "arm64": "v8"}[image.Architecture]
|
||||
}
|
||||
return image.OS == requested.OS && image.Architecture == requested.Architecture && (requested.Variant == "" || requested.Variant == variant)
|
||||
}
|
||||
|
||||
// RemoveImage removes image from local store, the function is used to run different
|
||||
|
||||
@@ -5,20 +5,20 @@
|
||||
package container
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
log "github.com/sirupsen/logrus"
|
||||
"gitea.com/gitea/runner/act/common/logging"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func init() {
|
||||
log.SetLevel(log.DebugLevel)
|
||||
logging.SetLevel(logging.LevelDebug)
|
||||
}
|
||||
|
||||
// buildScratchImage builds a tiny empty image for the given platform locally (FROM scratch, no
|
||||
@@ -41,7 +41,7 @@ func buildScratchImage(t *testing.T, platform string) string {
|
||||
|
||||
func TestImageExistsLocally(t *testing.T) {
|
||||
requireDocker(t)
|
||||
ctx := context.Background()
|
||||
ctx := t.Context()
|
||||
|
||||
// a non-existent image is reported absent
|
||||
missing, err := ImageExistsLocally(ctx, "library/alpine:this-random-tag-will-never-exist", "linux/amd64")
|
||||
|
||||
@@ -12,7 +12,7 @@ import (
|
||||
"errors"
|
||||
"io"
|
||||
|
||||
"github.com/sirupsen/logrus"
|
||||
"gitea.com/gitea/runner/act/common/logging"
|
||||
)
|
||||
|
||||
type dockerMessage struct {
|
||||
@@ -26,7 +26,7 @@ type dockerMessage struct {
|
||||
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 {
|
||||
return nil
|
||||
}
|
||||
@@ -77,7 +77,7 @@ func logDockerResponse(logger logrus.FieldLogger, dockerResponse io.ReadCloser,
|
||||
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 {
|
||||
logger.Errorf(format, args...)
|
||||
} else {
|
||||
|
||||
@@ -19,7 +19,7 @@ import (
|
||||
)
|
||||
|
||||
func TestIsolatedCacheContainer(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
ctx := t.Context()
|
||||
cache, lan := netip.MustParseAddr("172.18.0.3"), netip.MustParseAddr("192.168.1.20")
|
||||
client := &mockDockerClient{}
|
||||
client.On("ContainerList", ctx, mobyclient.ContainerListOptions{}).Return(mobyclient.ContainerListResult{Items: []container.Summary{{}, {
|
||||
@@ -58,7 +58,7 @@ func TestIsAddressPoolExhausted(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestCreateDockerNetworkInheritsDaemonMTU(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
ctx := t.Context()
|
||||
for _, tc := range []struct {
|
||||
bridgeOptions, want map[string]string
|
||||
}{
|
||||
@@ -87,7 +87,7 @@ func (m *mockDockerClient) NetworkDisconnect(ctx context.Context, id string, opt
|
||||
}
|
||||
|
||||
func TestConnectAndDisconnectNetwork(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
ctx := t.Context()
|
||||
connect := func(answer error) error {
|
||||
client := &mockDockerClient{}
|
||||
client.On("NetworkConnect", ctx, "jobs", mobyclient.NetworkConnectOptions{
|
||||
@@ -117,7 +117,7 @@ func TestConnectAndDisconnectNetwork(t *testing.T) {
|
||||
// and old enough to predate any job now starting are the runner's to reclaim. An unexpected
|
||||
// NetworkRemove fails the test on its own, since testify has no expectation to match it against.
|
||||
func TestRemoveOrphanNetworks(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
ctx := t.Context()
|
||||
cutoff := time.Date(2026, time.April, 29, 20, 0, 0, 0, time.UTC)
|
||||
client := &mockDockerClient{}
|
||||
client.On("NetworkList", ctx, mobyclient.NetworkListOptions{
|
||||
|
||||
@@ -61,3 +61,26 @@ func TestParsePlatform(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestPlatformMatches(t *testing.T) {
|
||||
for _, testcase := range []struct {
|
||||
requested, image string
|
||||
want bool
|
||||
}{
|
||||
{"linux/amd64", "linux/amd64", true},
|
||||
{"linux/amd64/v1", "linux/amd64", true},
|
||||
{"linux/arm64", "linux/amd64", false},
|
||||
{"linux/arm64/v8", "linux/arm64", true},
|
||||
{"linux/arm/v7", "linux/arm/v7", true},
|
||||
{"linux/arm", "linux/arm/v7", true},
|
||||
{"linux/arm/v6", "linux/arm/v7", false},
|
||||
{"linux/arm", "linux/arm/v6", true},
|
||||
{"windows/arm/v7", "linux/arm/v7", false},
|
||||
} {
|
||||
requested, err := parsePlatform(testcase.requested)
|
||||
require.NoError(t, err)
|
||||
image, err := parsePlatform(testcase.image)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, testcase.want, platformMatches(requested, image), "%s on %s", testcase.requested, testcase.image)
|
||||
}
|
||||
}
|
||||
@@ -37,7 +37,7 @@ import (
|
||||
)
|
||||
|
||||
const (
|
||||
jobLabel = "com.gitea.runner.job"
|
||||
jobLabel = "com.gitea.runner.job."
|
||||
maxCreateBody = 8 << 20
|
||||
|
||||
dockerProxyProbeTimeout = 5 * time.Second
|
||||
@@ -302,7 +302,7 @@ func rewriteCreate(r *http.Request, job string, mounts map[string]string) error
|
||||
if config.Labels == nil {
|
||||
config.Labels = make(map[string]string)
|
||||
}
|
||||
config.Labels[jobLabel] = job
|
||||
config.Labels[jobLabel+job] = "" // per-job key, so nested proxies keep each other's
|
||||
if fields["Labels"], err = json.Marshal(config.Labels); err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -560,7 +560,7 @@ func RemoveDockerJobResources(ctx context.Context, job string) error {
|
||||
|
||||
func removeLabelled(ctx context.Context, cli client.APIClient, job string) error {
|
||||
logger := common.Logger(ctx)
|
||||
filters := make(client.Filters).Add("label", jobLabel+"="+job)
|
||||
filters := make(client.Filters).Add("label", jobLabel+job)
|
||||
containers, err := cli.ContainerList(ctx, client.ContainerListOptions{All: true, Filters: filters})
|
||||
errs := []error{err}
|
||||
for _, c := range containers.Items {
|
||||
|
||||
@@ -146,52 +146,52 @@ func TestDockerProxy(t *testing.T) {
|
||||
t.Run("labels creates", func(t *testing.T) {
|
||||
for _, testCase := range []struct{ path, body, want string }{
|
||||
{
|
||||
"/v1.47/containers/create", `{"Image":"alpine","Unknown":{"enabled":true},"Labels":{"own":"1","com.gitea.runner.job":"other"}}`,
|
||||
`{"Image":"alpine","Unknown":{"enabled":true},"Labels":{"own":"1","com.gitea.runner.job":"job-1"}}`,
|
||||
"/v1.47/containers/create", `{"Image":"alpine","Unknown":{"enabled":true},"Labels":{"own":"1","com.gitea.runner.job.job-0":""}}`,
|
||||
`{"Image":"alpine","Unknown":{"enabled":true},"Labels":{"own":"1","com.gitea.runner.job.job-0":"","com.gitea.runner.job.job-1":""}}`,
|
||||
},
|
||||
{
|
||||
"/containers/create", `{"HostConfig":{"Binds":["/workspace/o/r:/src:ro","/workspace/o/r/../r/sub/:/sub","workspace/o/r:/relative","/workspace/o/rest:/rest","/workspace/o/r/tmp/x:/tmp","named:/named","/anonymous","/workspace/o/r:ro","/volumes/job/_data/x:/daemon-path"]},"DriverOpts":{"o":"bind","device":"/workspace/o/r"},"Labels":{"Foo":"1","foo":"2"}}`,
|
||||
`{"HostConfig":{"Binds":["/volumes/job/_data:/src:ro","/volumes/job/_data/sub:/sub","workspace/o/r:/relative","/workspace/o/rest:/rest","/workspace/o/r/tmp/x:/tmp","named:/named","/anonymous","/workspace/o/r:ro","/volumes/job/_data/x:/daemon-path"]},"DriverOpts":{"o":"bind","device":"/workspace/o/r"},"Labels":{"Foo":"1","foo":"2","com.gitea.runner.job":"job-1"}}`,
|
||||
`{"HostConfig":{"Binds":["/volumes/job/_data:/src:ro","/volumes/job/_data/sub:/sub","workspace/o/r:/relative","/workspace/o/rest:/rest","/workspace/o/r/tmp/x:/tmp","named:/named","/anonymous","/workspace/o/r:ro","/volumes/job/_data/x:/daemon-path"]},"DriverOpts":{"o":"bind","device":"/workspace/o/r"},"Labels":{"Foo":"1","foo":"2","com.gitea.runner.job.job-1":""}}`,
|
||||
},
|
||||
{
|
||||
"/containers/create", `{"HostConfig":{"Mounts":[{"Type":"bind","Source":"/var/run/docker.sock","Target":"/var/run/docker.sock"},{"type":"bind","source":"/workspace/o/r/sub/","target":"/m"},{"Type":"bind","Source":"/workspace/o/r/sub/../data","Target":"/dotted"},{"Type":"bind","Source":"/workspace/o/r/../r","Target":"/escaping"},{"Type":"BIND","Source":"/workspace/o/r"},{"Type":"volume","Source":"/workspace/o/r"}]}}`,
|
||||
`{"HostConfig":{"Mounts":[{"Type":"bind","Source":"/tmp/p/docker.sock","Target":"/var/run/docker.sock"},{"Type":"bind","Source":"/volumes/job/_data/sub/","target":"/m"},{"Type":"bind","Source":"/volumes/job/_data/sub/../data","Target":"/dotted"},{"Type":"bind","Source":"/volumes/job/_data","Target":"/escaping"},{"Type":"BIND","Source":"/workspace/o/r"},{"Type":"volume","Source":"/workspace/o/r"}]},"Labels":{"com.gitea.runner.job":"job-1"}}`,
|
||||
`{"HostConfig":{"Mounts":[{"Type":"bind","Source":"/tmp/p/docker.sock","Target":"/var/run/docker.sock"},{"Type":"bind","Source":"/volumes/job/_data/sub/","target":"/m"},{"Type":"bind","Source":"/volumes/job/_data/sub/../data","Target":"/dotted"},{"Type":"bind","Source":"/volumes/job/_data","Target":"/escaping"},{"Type":"BIND","Source":"/workspace/o/r"},{"Type":"volume","Source":"/workspace/o/r"}]},"Labels":{"com.gitea.runner.job.job-1":""}}`,
|
||||
},
|
||||
{
|
||||
"/containers/create", `{"HostConfig":{"Mounts":[{"Type":"volume","VolumeOptions":{"DriverConfig":{"Options":{"o":"bind","device":"/workspace/o/r/data"}}}},{"Type":"volume","VolumeOptions":{"DriverConfig":{"Options":{"o":"unbindable","device":"/workspace/o/r"}}}},{"Type":"volume","VolumeOptions":{"DriverConfig":{"Options":{"o":"bind,remount","device":"/workspace/o/r"}}}},{"Type":"volume","VolumeOptions":{"DriverConfig":{"Name":"plugin","Options":{"o":"bind","device":"/workspace/o/r"}}}}]}}`,
|
||||
`{"HostConfig":{"Mounts":[{"Type":"volume","VolumeOptions":{"DriverConfig":{"Options":{"o":"bind","device":"/volumes/job/_data/data"}}}},{"Type":"volume","VolumeOptions":{"DriverConfig":{"Options":{"o":"unbindable","device":"/workspace/o/r"}}}},{"Type":"volume","VolumeOptions":{"DriverConfig":{"Options":{"o":"bind,remount","device":"/workspace/o/r"}}}},{"Type":"volume","VolumeOptions":{"DriverConfig":{"Name":"plugin","Options":{"o":"bind","device":"/workspace/o/r"}}}}]},"Labels":{"com.gitea.runner.job":"job-1"}}`,
|
||||
`{"HostConfig":{"Mounts":[{"Type":"volume","VolumeOptions":{"DriverConfig":{"Options":{"o":"bind","device":"/volumes/job/_data/data"}}}},{"Type":"volume","VolumeOptions":{"DriverConfig":{"Options":{"o":"unbindable","device":"/workspace/o/r"}}}},{"Type":"volume","VolumeOptions":{"DriverConfig":{"Options":{"o":"bind,remount","device":"/workspace/o/r"}}}},{"Type":"volume","VolumeOptions":{"DriverConfig":{"Name":"plugin","Options":{"o":"bind","device":"/workspace/o/r"}}}}]},"Labels":{"com.gitea.runner.job.job-1":""}}`,
|
||||
},
|
||||
{
|
||||
"/containers/create", `{"HostConfig":{"privileged":true,"Privileged":false,"Binds":["/workspace/o/r:/src"]},"hoſtconfig":null}`,
|
||||
`{"HostConfig":{"privileged":true,"Privileged":false,"Binds":["/workspace/o/r:/src"]},"hoſtconfig":null,"Labels":{"com.gitea.runner.job":"job-1"}}`,
|
||||
`{"HostConfig":{"privileged":true,"Privileged":false,"Binds":["/workspace/o/r:/src"]},"hoſtconfig":null,"Labels":{"com.gitea.runner.job.job-1":""}}`,
|
||||
},
|
||||
{
|
||||
"/networks/create", `{"Name":"n"}`,
|
||||
`{"Name":"n","Labels":{"com.gitea.runner.job":"job-1"}}`,
|
||||
`{"Name":"n","Labels":{"com.gitea.runner.job.job-1":""}}`,
|
||||
},
|
||||
{
|
||||
"/volumes/create", `{"Name":"v","labels":null,"DriverOpts":{"type":"none","o":"rbind,ro","device":"/workspace/o/r/"},"HostConfig":{"Binds":["/workspace/o/r:/src"]}}`,
|
||||
`{"Name":"v","Labels":{"com.gitea.runner.job":"job-1"},"DriverOpts":{"type":"none","o":"rbind,ro","device":"/volumes/job/_data/"},"HostConfig":{"Binds":["/workspace/o/r:/src"]}}`,
|
||||
`{"Name":"v","Labels":{"com.gitea.runner.job.job-1":""},"DriverOpts":{"type":"none","o":"rbind,ro","device":"/volumes/job/_data/"},"HostConfig":{"Binds":["/workspace/o/r:/src"]}}`,
|
||||
},
|
||||
{
|
||||
"/volumes/create", `{"Name":"d","DriverOpts":{"o":"bind","device":1,"device":"/workspace/o/r"}}`,
|
||||
`{"Name":"d","DriverOpts":{"o":"bind","device":1,"device":"/workspace/o/r"},"Labels":{"com.gitea.runner.job":"job-1"}}`,
|
||||
`{"Name":"d","DriverOpts":{"o":"bind","device":1,"device":"/workspace/o/r"},"Labels":{"com.gitea.runner.job.job-1":""}}`,
|
||||
},
|
||||
{
|
||||
"/volumes/create", `{"Name":"p","Driver":"plugin","DriverOpts":{"o":"bind","device":"/workspace/o/r"}}`,
|
||||
`{"Name":"p","Driver":"plugin","DriverOpts":{"o":"bind","device":"/workspace/o/r"},"Labels":{"com.gitea.runner.job":"job-1"}}`,
|
||||
`{"Name":"p","Driver":"plugin","DriverOpts":{"o":"bind","device":"/workspace/o/r"},"Labels":{"com.gitea.runner.job.job-1":""}}`,
|
||||
},
|
||||
{
|
||||
"/volumes/create", "",
|
||||
`{"Labels":{"com.gitea.runner.job":"job-1"}}`,
|
||||
`{"Labels":{"com.gitea.runner.job.job-1":""}}`,
|
||||
},
|
||||
{
|
||||
"/volumes/create", "null",
|
||||
`{"Labels":{"com.gitea.runner.job":"job-1"}}`,
|
||||
`{"Labels":{"com.gitea.runner.job.job-1":""}}`,
|
||||
},
|
||||
{
|
||||
"/volumes/create", `{"Labels":{"discard":"1"},"labels":null,"LABELS":{"own":"1"},"LABELS":{"extra":"2"}}`,
|
||||
`{"Labels":{"own":"1","extra":"2","com.gitea.runner.job":"job-1"}}`,
|
||||
`{"Labels":{"own":"1","extra":"2","com.gitea.runner.job.job-1":""}}`,
|
||||
},
|
||||
} {
|
||||
resp, err := client.Post("http://docker"+testCase.path, "application/json", strings.NewReader(testCase.body))
|
||||
@@ -244,7 +244,7 @@ func TestDockerProxy(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, resp.Body.Close())
|
||||
require.Equal(t, http.StatusCreated, resp.StatusCode)
|
||||
assert.JSONEq(t, `{"Labels":{"com.gitea.runner.job":"job-1"}}`, string(<-bodies))
|
||||
assert.JSONEq(t, `{"Labels":{"com.gitea.runner.job.job-1":""}}`, string(<-bodies))
|
||||
}
|
||||
})
|
||||
|
||||
@@ -276,7 +276,7 @@ func TestDockerProxy(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, resp.Body.Close())
|
||||
require.Equal(t, http.StatusCreated, resp.StatusCode)
|
||||
assert.JSONEq(t, `{"Labels":{"com.gitea.runner.job":"job-1"}}`, string(<-bodies))
|
||||
assert.JSONEq(t, `{"Labels":{"com.gitea.runner.job.job-1":""}}`, string(<-bodies))
|
||||
require.NoError(t, <-closed)
|
||||
})
|
||||
}
|
||||
@@ -285,8 +285,8 @@ func TestRemoveLabelledRemovesContainersNetworksAndVolumes(t *testing.T) {
|
||||
containerFailure := errors.New("container removal failed")
|
||||
listFailure := errors.New("listing failed")
|
||||
volumeFailure := errors.New("volume removal failed")
|
||||
ctx := context.Background()
|
||||
filters := make(mobyclient.Filters).Add("label", jobLabel+"=job-1")
|
||||
ctx := t.Context()
|
||||
filters := make(mobyclient.Filters).Add("label", jobLabel+"job-1")
|
||||
cli := &mockDockerClient{}
|
||||
cli.On("ContainerList", ctx, mobyclient.ContainerListOptions{All: true, Filters: filters}).
|
||||
Return(mobyclient.ContainerListResult{Items: []container.Summary{{ID: "c1", Names: []string{"/app"}}}}, nil).Once()
|
||||
@@ -312,7 +312,8 @@ func TestRemoveLabelledRemovesContainersNetworksAndVolumes(t *testing.T) {
|
||||
|
||||
func TestDockerProxyWithDaemon(t *testing.T) {
|
||||
requireDocker(t)
|
||||
ctx := context.Background()
|
||||
ctx := t.Context()
|
||||
cleanupCtx := context.WithoutCancel(ctx)
|
||||
require.NoError(t, NewDockerPullExecutor(NewDockerPullExecutorInput{Image: "alpine"})(ctx))
|
||||
direct, err := GetDockerClient(ctx)
|
||||
require.NoError(t, err)
|
||||
@@ -337,22 +338,24 @@ func TestDockerProxyWithDaemon(t *testing.T) {
|
||||
Config: &container.Config{Image: "alpine", Cmd: []string{"sleep", "300"}, Labels: map[string]string{"own": "1"}},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
t.Cleanup(func() { _, _ = direct.ContainerRemove(ctx, created.ID, mobyclient.ContainerRemoveOptions{Force: true}) })
|
||||
t.Cleanup(func() {
|
||||
_, _ = direct.ContainerRemove(cleanupCtx, created.ID, mobyclient.ContainerRemoveOptions{Force: true})
|
||||
})
|
||||
_, err = viaProxy.ContainerStart(ctx, created.ID, mobyclient.ContainerStartOptions{})
|
||||
require.NoError(t, err)
|
||||
net, err := viaProxy.NetworkCreate(ctx, job, mobyclient.NetworkCreateOptions{})
|
||||
require.NoError(t, err)
|
||||
t.Cleanup(func() { _, _ = direct.NetworkRemove(ctx, net.ID, mobyclient.NetworkRemoveOptions{}) })
|
||||
t.Cleanup(func() { _, _ = direct.NetworkRemove(cleanupCtx, net.ID, mobyclient.NetworkRemoveOptions{}) })
|
||||
_, err = viaProxy.VolumeCreate(ctx, mobyclient.VolumeCreateOptions{Name: job})
|
||||
require.NoError(t, err)
|
||||
t.Cleanup(func() { _, _ = direct.VolumeRemove(ctx, job, mobyclient.VolumeRemoveOptions{Force: true}) })
|
||||
t.Cleanup(func() { _, _ = direct.VolumeRemove(cleanupCtx, job, mobyclient.VolumeRemoveOptions{Force: true}) })
|
||||
|
||||
inspected, err := direct.ContainerInspect(ctx, created.ID, mobyclient.ContainerInspectOptions{})
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, map[string]string{"own": "1", jobLabel: job}, inspected.Container.Config.Labels)
|
||||
assert.Subset(t, inspected.Container.Config.Labels, map[string]string{"own": "1", jobLabel + job: ""})
|
||||
vol, err := direct.VolumeInspect(ctx, job, mobyclient.VolumeInspectOptions{})
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, job, vol.Volume.Labels[jobLabel])
|
||||
assert.Contains(t, vol.Volume.Labels, jobLabel+job)
|
||||
|
||||
exec, err := viaProxy.ExecCreate(ctx, created.ID, mobyclient.ExecCreateOptions{Cmd: []string{"cat"}, AttachStdin: true, AttachStdout: true, TTY: true})
|
||||
require.NoError(t, err)
|
||||
|
||||
@@ -5,20 +5,20 @@
|
||||
package container
|
||||
|
||||
import (
|
||||
"context"
|
||||
"io"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"gitea.com/gitea/runner/act/common/logging"
|
||||
"gitea.com/gitea/runner/act/common/logging/logtest"
|
||||
|
||||
"github.com/docker/cli/cli/config"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"github.com/sirupsen/logrus/hooks/test"
|
||||
assert "github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func init() {
|
||||
log.SetLevel(log.DebugLevel)
|
||||
logging.SetLevel(logging.LevelDebug)
|
||||
}
|
||||
|
||||
func TestCleanImage(t *testing.T) {
|
||||
@@ -36,13 +36,13 @@ func TestCleanImage(t *testing.T) {
|
||||
}
|
||||
|
||||
for _, table := range tables {
|
||||
imageOut := cleanImage(context.Background(), table.imageIn)
|
||||
imageOut := cleanImage(t.Context(), table.imageIn)
|
||||
assert.Equal(t, table.imageOut, imageOut)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetImagePullOptions(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
ctx := t.Context()
|
||||
|
||||
orig := config.Dir()
|
||||
t.Cleanup(func() { config.SetDir(orig) })
|
||||
@@ -79,7 +79,7 @@ func TestIsPinnedImage(t *testing.T) {
|
||||
// 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.
|
||||
func TestLogDockerResponseError(t *testing.T) {
|
||||
logger, _ := test.NewNullLogger()
|
||||
logger := logging.New(logtest.New())
|
||||
streamErr := func(line string) error {
|
||||
return logDockerResponse(logger, io.NopCloser(strings.NewReader(line)), false)
|
||||
}
|
||||
|
||||
+50
-102
@@ -25,13 +25,14 @@ import (
|
||||
"time"
|
||||
|
||||
"gitea.com/gitea/runner/act/common"
|
||||
"gitea.com/gitea/runner/act/common/logging"
|
||||
|
||||
"dario.cat/mergo"
|
||||
"github.com/bmatcuk/doublestar/v4"
|
||||
cerrdefs "github.com/containerd/errdefs"
|
||||
"github.com/docker/cli/cli/compose/loader"
|
||||
"github.com/docker/cli/cli/connhelper"
|
||||
"github.com/joho/godotenv"
|
||||
"github.com/docker/cli/opts"
|
||||
"github.com/moby/moby/api/pkg/stdcopy"
|
||||
"github.com/moby/moby/api/types/container"
|
||||
"github.com/moby/moby/api/types/mount"
|
||||
@@ -40,7 +41,6 @@ import (
|
||||
"github.com/moby/moby/client"
|
||||
"github.com/moby/moby/client/pkg/versions"
|
||||
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
|
||||
@@ -95,8 +95,8 @@ func (cr *containerReference) Start(attach bool) common.Executor {
|
||||
cr.attach().IfBool(attach),
|
||||
cr.start(),
|
||||
cr.wait().IfBool(attach),
|
||||
cr.tryReadUID(),
|
||||
cr.tryReadGID(),
|
||||
cr.tryReadID("-u", func(id int) { cr.UID = id }),
|
||||
cr.tryReadID("-g", func(id int) { cr.GID = id }),
|
||||
func(ctx context.Context) error {
|
||||
// If this fails, then folders have wrong permissions on non root container
|
||||
if cr.UID != 0 || cr.GID != 0 {
|
||||
@@ -305,7 +305,25 @@ func (cr *containerReference) UpdateFromEnv(srcPath string, env *map[string]stri
|
||||
}
|
||||
|
||||
func (cr *containerReference) UpdateFromImageEnv(env *map[string]string) common.Executor {
|
||||
return cr.extractFromImageEnv(env).IfNot(common.Dryrun)
|
||||
envMap := *env
|
||||
return common.Executor(func(ctx context.Context) error {
|
||||
inspect, err := cr.cli.ContainerInspect(ctx, cr.id, client.ContainerInspectOptions{})
|
||||
if err != nil {
|
||||
common.Logger(ctx).Error(err)
|
||||
return fmt.Errorf("inspect container: %w", err)
|
||||
}
|
||||
if inspect.Container.Config == nil {
|
||||
return nil
|
||||
}
|
||||
for key, value := range opts.ConvertKVStringsToMap(inspect.Container.Config.Env) {
|
||||
if envMap[key] == "" {
|
||||
envMap[key] = value
|
||||
} else if key == "PATH" {
|
||||
envMap[key] += ":" + value
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}).IfNot(common.Dryrun)
|
||||
}
|
||||
|
||||
func (cr *containerReference) Exec(command []string, env map[string]string, user, workdir string) common.Executor {
|
||||
@@ -567,20 +585,6 @@ func (cr *containerReference) mergeContainerConfigs(ctx context.Context, config
|
||||
return nil, nil, fmt.Errorf("cannot process container options: '%s': '%w'", options, err)
|
||||
}
|
||||
|
||||
// FIXME: If everything is fine after gitea/act v0.260.0, remove the following comment.
|
||||
// In the old fork version, the code is
|
||||
// if len(copts.netMode.Value()) == 0 {
|
||||
// if err = copts.netMode.Set("host"); err != nil {
|
||||
// return nil, nil, fmt.Errorf("cannot parse networkmode=host. This is an internal error and should not happen: '%w'", err)
|
||||
// }
|
||||
// }
|
||||
// And it has been commented with:
|
||||
// If a service container's network is set to `host`, the container will not be able to
|
||||
// connect to the specified network created for the job container and the service containers.
|
||||
// So comment out the following code.
|
||||
// Not the if it's necessary to comment it in the new version,
|
||||
// since it's cr.input.NetworkMode now.
|
||||
|
||||
if len(copts.netMode.Value()) == 0 {
|
||||
if err = copts.netMode.Set(cr.input.NetworkMode); err != nil {
|
||||
return nil, nil, fmt.Errorf("cannot parse networkmode=%s. This is an internal error and should not happen: '%w'", cr.input.NetworkMode, err)
|
||||
@@ -644,6 +648,9 @@ func (cr *containerReference) mergeContainerConfigs(ctx context.Context, config
|
||||
if flags.Changed("network") || flags.Changed("net") {
|
||||
logger.Warn("--network and --net in the options will be ignored.")
|
||||
}
|
||||
if len(cr.input.Entrypoint) != 0 && flags.Changed("entrypoint") {
|
||||
logger.Warn("--entrypoint in the options will be ignored.")
|
||||
}
|
||||
hostConfig.NetworkMode = networkMode
|
||||
logger.Debugf("Merged container.HostConfig ==> %+v", hostConfig)
|
||||
|
||||
@@ -673,17 +680,11 @@ func (cr *containerReference) create(capAdd, capDrop []string) common.Executor {
|
||||
ExposedPorts: exposedPorts,
|
||||
Tty: input.AllocatePTY,
|
||||
}
|
||||
// For Gitea, reduce log noise
|
||||
// logger.Debugf("Common container.Config ==> %+v", config)
|
||||
|
||||
if len(input.Cmd) != 0 {
|
||||
config.Cmd = input.Cmd
|
||||
}
|
||||
|
||||
if len(input.Entrypoint) != 0 {
|
||||
config.Entrypoint = input.Entrypoint
|
||||
}
|
||||
|
||||
mounts := make([]mount.Mount, 0)
|
||||
for mountSource, mountTarget := range input.Mounts {
|
||||
mounts = append(mounts, mount.Mount{
|
||||
@@ -718,20 +719,20 @@ func (cr *containerReference) create(capAdd, capDrop []string) common.Executor {
|
||||
PortBindings: portBindings,
|
||||
AutoRemove: input.AutoRemove,
|
||||
}
|
||||
// For Gitea, reduce log noise
|
||||
// logger.Debugf("Common container.HostConfig ==> %+v", hostConfig)
|
||||
|
||||
config, hostConfig, err = cr.mergeContainerConfigs(ctx, config, hostConfig)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if len(input.Entrypoint) != 0 {
|
||||
config.Entrypoint = input.Entrypoint
|
||||
}
|
||||
|
||||
// For Gitea
|
||||
config, hostConfig = cr.sanitizeConfig(ctx, config, hostConfig)
|
||||
|
||||
var networkingConfig *network.NetworkingConfig
|
||||
// For Gitea, reduce log noise
|
||||
// logger.Debugf("input.NetworkAliases ==> %v", input.NetworkAliases)
|
||||
n := hostConfig.NetworkMode
|
||||
// IsUserDefined and IsHost are broken on windows
|
||||
if n.IsUserDefined() && n != "host" && len(input.NetworkAliases) > 0 {
|
||||
@@ -764,44 +765,6 @@ func (cr *containerReference) create(capAdd, capDrop []string) common.Executor {
|
||||
}
|
||||
}
|
||||
|
||||
func (cr *containerReference) extractFromImageEnv(env *map[string]string) common.Executor {
|
||||
envMap := *env
|
||||
return func(ctx context.Context) error {
|
||||
logger := common.Logger(ctx)
|
||||
|
||||
inspect, err := cr.cli.ImageInspect(ctx, cr.input.Image)
|
||||
if err != nil {
|
||||
logger.Error(err)
|
||||
return fmt.Errorf("inspect image: %w", err)
|
||||
}
|
||||
|
||||
if inspect.Config == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
imageEnv, err := godotenv.Unmarshal(strings.Join(inspect.Config.Env, "\n"))
|
||||
if err != nil {
|
||||
logger.Error(err)
|
||||
return fmt.Errorf("unmarshal image env: %w", err)
|
||||
}
|
||||
|
||||
for k, v := range imageEnv {
|
||||
if k == "PATH" {
|
||||
if envMap[k] == "" {
|
||||
envMap[k] = v
|
||||
} else {
|
||||
envMap[k] += `:` + v
|
||||
}
|
||||
} else if envMap[k] == "" {
|
||||
envMap[k] = v
|
||||
}
|
||||
}
|
||||
|
||||
env = &envMap
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func (cr *containerReference) exec(cmd []string, env map[string]string, user, workdir string) common.Executor {
|
||||
return func(ctx context.Context) error {
|
||||
if cr.id == "" {
|
||||
@@ -819,11 +782,6 @@ func (cr *containerReference) exec(cmd []string, env map[string]string, user, wo
|
||||
|
||||
logger.Debugf("Exec command '%s'", cmd)
|
||||
isTerminal := cr.input.AllocatePTY
|
||||
envList := make([]string, 0)
|
||||
for k, v := range env {
|
||||
envList = append(envList, fmt.Sprintf("%s=%s", k, v))
|
||||
}
|
||||
|
||||
var wd string
|
||||
if workdir != "" {
|
||||
if strings.HasPrefix(workdir, "/") {
|
||||
@@ -840,7 +798,7 @@ func (cr *containerReference) exec(cmd []string, env map[string]string, user, wo
|
||||
User: user,
|
||||
Cmd: cmd,
|
||||
WorkingDir: wd,
|
||||
Env: envList,
|
||||
Env: getEnvListFromMap(env),
|
||||
TTY: isTerminal,
|
||||
AttachStderr: true,
|
||||
AttachStdout: true,
|
||||
@@ -857,7 +815,7 @@ func (cr *containerReference) exec(cmd []string, env map[string]string, user, wo
|
||||
}
|
||||
defer resp.Close()
|
||||
|
||||
err = cr.waitForCommand(ctx, resp.HijackedResponse, idResp, user, workdir)
|
||||
err = cr.waitForCommand(ctx, resp.HijackedResponse)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -907,15 +865,7 @@ func (cr *containerReference) tryReadID(opt string, cbk func(id int)) common.Exe
|
||||
}
|
||||
}
|
||||
|
||||
func (cr *containerReference) tryReadUID() common.Executor {
|
||||
return cr.tryReadID("-u", func(id int) { cr.UID = id })
|
||||
}
|
||||
|
||||
func (cr *containerReference) tryReadGID() common.Executor {
|
||||
return cr.tryReadID("-g", func(id int) { cr.GID = id })
|
||||
}
|
||||
|
||||
func (cr *containerReference) waitForCommand(ctx context.Context, resp client.HijackedResponse, _ client.ExecCreateResult, _, _ string) error {
|
||||
func (cr *containerReference) waitForCommand(ctx context.Context, resp client.HijackedResponse) error {
|
||||
logger := common.Logger(ctx)
|
||||
|
||||
// Buffered so the copy goroutine never blocks on send if the grace-period
|
||||
@@ -957,6 +907,7 @@ func (cr *containerReference) waitForCommand(ctx context.Context, resp client.Hi
|
||||
}
|
||||
|
||||
func (cr *containerReference) copyDir(dstPath, srcPath string, useGitIgnore, skipGitDir bool) common.Executor {
|
||||
dstPath = path.Clean(dstPath)
|
||||
return func(ctx context.Context) error {
|
||||
if cr.id == "" {
|
||||
return cr.missingContainerError("copy directory to %s", dstPath)
|
||||
@@ -978,7 +929,11 @@ func (cr *containerReference) copyDir(dstPath, srcPath string, useGitIgnore, ski
|
||||
logger.Error(err)
|
||||
}
|
||||
}(tarFile)
|
||||
if err := writeDirTar(ctx, tarFile, dstPath, srcPath, useGitIgnore, skipGitDir, cr.UID, cr.GID); err != nil {
|
||||
extractionRoot := "/"
|
||||
if actPath := cr.GetActPath(); strings.HasPrefix(dstPath+"/", actPath+"/") {
|
||||
extractionRoot = actPath // keeps tar entries from crossing the image's /var/run symlink
|
||||
}
|
||||
if err := writeDirTar(ctx, tarFile, path.Join("/", strings.TrimPrefix(dstPath, extractionRoot)), srcPath, useGitIgnore, skipGitDir, cr.UID, cr.GID); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -987,14 +942,7 @@ func (cr *containerReference) copyDir(dstPath, srcPath string, useGitIgnore, ski
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to seek tar archive: %w", err)
|
||||
}
|
||||
_, err = cr.cli.CopyToContainer(ctx, cr.id, client.CopyToContainerOptions{
|
||||
DestinationPath: "/",
|
||||
Content: tarFile,
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to copy content to container: %w", err)
|
||||
}
|
||||
return nil
|
||||
return cr.upload(ctx, extractionRoot, tarFile)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1009,17 +957,17 @@ func (cr *containerReference) copyContent(dstPath string, files ...*FileEntry) c
|
||||
}
|
||||
|
||||
common.Logger(ctx).Debugf("Extracting content to '%s'", dstPath)
|
||||
_, err := cr.cli.CopyToContainer(ctx, cr.id, client.CopyToContainerOptions{
|
||||
DestinationPath: dstPath,
|
||||
Content: &buf,
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to copy content to container: %w", err)
|
||||
}
|
||||
return nil
|
||||
return cr.upload(ctx, dstPath, &buf)
|
||||
}
|
||||
}
|
||||
|
||||
func (cr *containerReference) upload(ctx context.Context, dstPath string, content io.Reader) error {
|
||||
if _, err := cr.cli.CopyToContainer(ctx, cr.id, client.CopyToContainerOptions{DestinationPath: dstPath, Content: content}); err != nil {
|
||||
return fmt.Errorf("failed to copy content to container: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (cr *containerReference) attach() common.Executor {
|
||||
return func(ctx context.Context) error {
|
||||
out, err := cr.cli.ContainerAttach(ctx, cr.id, client.ContainerAttachOptions{
|
||||
@@ -1098,7 +1046,7 @@ func (cr *containerReference) wait() common.Executor {
|
||||
// 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.
|
||||
// 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, "--ipc", &hostConfig.IpcMode, trusted.IpcMode)
|
||||
resetOption(logger, "--uts", &hostConfig.UTSMode, trusted.UTSMode)
|
||||
@@ -1137,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
|
||||
// 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) {
|
||||
return
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
package container
|
||||
|
||||
import (
|
||||
"archive/tar"
|
||||
"bufio"
|
||||
"bytes"
|
||||
"context"
|
||||
@@ -18,6 +19,8 @@ import (
|
||||
"testing"
|
||||
|
||||
"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"
|
||||
"github.com/moby/moby/api/pkg/stdcopy"
|
||||
@@ -25,50 +28,33 @@ import (
|
||||
"github.com/moby/moby/api/types/mount"
|
||||
"github.com/moby/moby/api/types/network"
|
||||
mobyclient "github.com/moby/moby/client"
|
||||
"github.com/sirupsen/logrus/hooks/test"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/mock"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestDocker(t *testing.T) {
|
||||
requireDocker(t)
|
||||
ctx := context.Background()
|
||||
client, err := GetDockerClient(ctx)
|
||||
require.NoError(t, err)
|
||||
defer client.Close()
|
||||
|
||||
dockerBuild := NewDockerBuildExecutor(NewDockerBuildExecutorInput{
|
||||
ContextDir: "testdata",
|
||||
ImageTag: "envmergetest",
|
||||
})
|
||||
|
||||
err = dockerBuild(ctx)
|
||||
assert.NoError(t, err) //nolint:testifylint // pre-existing issue from nektos/act
|
||||
|
||||
cr := &containerReference{
|
||||
cli: client,
|
||||
input: &NewContainerInput{
|
||||
Image: "envmergetest",
|
||||
},
|
||||
}
|
||||
func TestDockerEnvironmentUsesContainerPath(t *testing.T) {
|
||||
client := &mockDockerClient{}
|
||||
client.On("ContainerInspect", mock.Anything, "amd64-job", mobyclient.ContainerInspectOptions{}).
|
||||
Return(mobyclient.ContainerInspectResult{Container: container.InspectResponse{Config: &container.Config{Env: []string{
|
||||
"PATH=/opt/node/x64/bin:/usr/bin:/bin", "CONFLICT_VAR=container-value", "SOME_RANDOM_VAR=",
|
||||
"ANOTHER_ONE=old-value", "ANOTHER_ONE=BUT_I_HAVE_VALUE", "LITERAL='quoted'=$HOME\nsecond line",
|
||||
}}}}, nil)
|
||||
env := map[string]string{
|
||||
"PATH": "/usr/local/bin:/usr/bin:/usr/sbin:/bin:/sbin",
|
||||
"PATH": "/custom/bin",
|
||||
"RANDOM_VAR": "WITH_VALUE",
|
||||
"ANOTHER_VAR": "",
|
||||
"CONFLICT_VAR": "I_EXIST_IN_MULTIPLE_PLACES",
|
||||
"CONFLICT_VAR": "step-value",
|
||||
}
|
||||
|
||||
envExecutor := cr.extractFromImageEnv(&env)
|
||||
err = envExecutor(ctx)
|
||||
assert.NoError(t, err) //nolint:testifylint // pre-existing issue from nektos/act
|
||||
require.NoError(t, (&containerReference{cli: client, id: "amd64-job"}).UpdateFromImageEnv(&env)(t.Context()))
|
||||
assert.Equal(t, map[string]string{
|
||||
"PATH": "/usr/local/bin:/usr/bin:/usr/sbin:/bin:/sbin:/this/path/does/not/exists/anywhere:/this/either",
|
||||
"PATH": "/custom/bin:/opt/node/x64/bin:/usr/bin:/bin",
|
||||
"RANDOM_VAR": "WITH_VALUE",
|
||||
"ANOTHER_VAR": "",
|
||||
"SOME_RANDOM_VAR": "",
|
||||
"ANOTHER_ONE": "BUT_I_HAVE_VALUE",
|
||||
"CONFLICT_VAR": "I_EXIST_IN_MULTIPLE_PLACES",
|
||||
"CONFLICT_VAR": "step-value",
|
||||
"LITERAL": "'quoted'=$HOME\nsecond line",
|
||||
}, env)
|
||||
}
|
||||
|
||||
@@ -180,7 +166,7 @@ func (m *mockConn) Close() (err error) {
|
||||
}
|
||||
|
||||
func TestDockerExecAbort(t *testing.T) {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
ctx, cancel := context.WithCancel(t.Context())
|
||||
|
||||
reader := &interruptReader{started: make(chan struct{}), interrupted: make(chan struct{}), stopped: make(chan struct{})}
|
||||
conn := &mockConn{}
|
||||
@@ -221,7 +207,7 @@ func TestDockerExecAbort(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestDockerExecFailure(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
ctx := t.Context()
|
||||
|
||||
conn := &mockConn{}
|
||||
|
||||
@@ -268,7 +254,7 @@ func stdcopyFrame(stream stdcopy.StdType, payload string) []byte {
|
||||
// attach() streaming goroutine has drained and flushed the container's output,
|
||||
// so a final line without a trailing newline is not lost.
|
||||
func TestDockerAttachFlushesTrailingLine(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
ctx := t.Context()
|
||||
|
||||
framed := bytes.NewBuffer(stdcopyFrame(stdcopy.Stdout, "line one\nlast line without newline"))
|
||||
|
||||
@@ -317,7 +303,7 @@ func TestDockerAttachFlushesTrailingLine(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestDockerWaitFailure(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
ctx := t.Context()
|
||||
|
||||
statusCh := make(chan container.WaitResponse, 1)
|
||||
statusCh <- container.WaitResponse{StatusCode: 2}
|
||||
@@ -368,8 +354,8 @@ func TestRemoveIgnoresAutoRemoveRace(t *testing.T) {
|
||||
{name: "real failure", err: removeFailure, wantErr: removeFailure},
|
||||
} {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
logger, hook := test.NewNullLogger()
|
||||
ctx := common.WithLogger(context.Background(), logger)
|
||||
logger, hook := logtest.NewLogger()
|
||||
ctx := common.WithLogger(t.Context(), logger)
|
||||
client := &mockDockerClient{}
|
||||
client.On("ContainerKill", ctx, "abc", killOpts).Return(mobyclient.ContainerKillResult{}, nil)
|
||||
client.On("ContainerRemove", ctx, "abc", removeOpts).Return(mobyclient.ContainerRemoveResult{}, tc.err)
|
||||
@@ -392,7 +378,7 @@ func TestRemoveIgnoresAutoRemoveRace(t *testing.T) {
|
||||
} else {
|
||||
assert.Empty(t, cr.id)
|
||||
}
|
||||
assert.Empty(t, hook.AllEntries())
|
||||
assert.Empty(t, hook.AllRecords())
|
||||
client.AssertExpectations(t)
|
||||
})
|
||||
}
|
||||
@@ -402,7 +388,7 @@ func TestRemoveIgnoresAutoRemoveRace(t *testing.T) {
|
||||
// create() lost its reply, must still be removed rather than leaking with its network. It
|
||||
// was never started here, so it is not worth a kill of its own.
|
||||
func TestRemoveWithoutIDUsesName(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
ctx := t.Context()
|
||||
client := &mockDockerClient{}
|
||||
client.On("ContainerRemove", ctx, "job-1", mobyclient.ContainerRemoveOptions{RemoveVolumes: true, Force: true}).
|
||||
Return(mobyclient.ContainerRemoveResult{}, nil)
|
||||
@@ -415,7 +401,7 @@ func TestRemoveWithoutIDUsesName(t *testing.T) {
|
||||
// find() must drop a stale cached id so later Copy/Exec don't hit the
|
||||
// daemon with a torn-down container.
|
||||
func TestFindRevalidatesStaleID(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
ctx := t.Context()
|
||||
notFound := cerrdefs.ErrNotFound.WithMessage("No such container")
|
||||
boom := errors.New("daemon unreachable")
|
||||
newCR := func(id string) (*containerReference, *mockDockerClient) {
|
||||
@@ -473,7 +459,7 @@ func TestFindRevalidatesStaleID(t *testing.T) {
|
||||
// Every daemon entry point fails fast with a clear, container-named
|
||||
// error when no live cr.id is known.
|
||||
func TestRejectsMissingContainer(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
ctx := t.Context()
|
||||
client := &mockDockerClient{}
|
||||
client.On("ContainerList", ctx, mobyclient.ContainerListOptions{All: true}).Return(mobyclient.ContainerListResult{}, nil)
|
||||
cr := &containerReference{cli: client, input: &NewContainerInput{Name: "job-1"}}
|
||||
@@ -498,25 +484,45 @@ func TestRejectsMissingContainer(t *testing.T) {
|
||||
check("Inspect after removal", err)
|
||||
}
|
||||
|
||||
// End-to-end: a stale cr.id is cleared, repopulated from name lookup,
|
||||
// and the Copy completes against the fresh id.
|
||||
func TestPublicCopyPipelineHandlesStaleID(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
client := &mockDockerClient{}
|
||||
client.On("ContainerInspect", ctx, "stale", mobyclient.ContainerInspectOptions{}).
|
||||
Return(mobyclient.ContainerInspectResult{}, cerrdefs.ErrNotFound.WithMessage("gone"))
|
||||
client.On("ContainerList", ctx, mobyclient.ContainerListOptions{All: true}).
|
||||
Return(mobyclient.ContainerListResult{Items: []container.Summary{
|
||||
{ID: "fresh", Names: []string{"/job-1"}},
|
||||
}}, nil)
|
||||
client.On("CopyToContainer", ctx, "fresh", mock.MatchedBy(func(opts mobyclient.CopyToContainerOptions) bool {
|
||||
return opts.DestinationPath == "/var/run/act"
|
||||
})).Return(mobyclient.CopyToContainerResult{}, nil)
|
||||
for _, testcase := range []struct {
|
||||
name, destination, extractionRoot, entry string
|
||||
directory bool
|
||||
}{
|
||||
{name: "files", destination: "/var/run/act", extractionRoot: "/var/run/act", entry: "x"},
|
||||
{name: "action below act volume", directory: true, destination: "/var/run/act/actions/checkout/", extractionRoot: "/var/run/act", entry: "actions/checkout/x"},
|
||||
{name: "sibling prefix", directory: true, destination: "/var/run/actor/action", extractionRoot: "/", entry: "var/run/actor/action/x"},
|
||||
} {
|
||||
t.Run(testcase.name, func(t *testing.T) {
|
||||
ctx := t.Context()
|
||||
client := &mockDockerClient{}
|
||||
client.On("ContainerInspect", ctx, "stale", mobyclient.ContainerInspectOptions{}).
|
||||
Return(mobyclient.ContainerInspectResult{}, cerrdefs.ErrNotFound.WithMessage("gone"))
|
||||
client.On("ContainerList", ctx, mobyclient.ContainerListOptions{All: true}).
|
||||
Return(mobyclient.ContainerListResult{Items: []container.Summary{
|
||||
{ID: "fresh", Names: []string{"/job-1"}},
|
||||
}}, nil)
|
||||
client.On("CopyToContainer", ctx, "fresh", mock.Anything).
|
||||
Run(func(args mock.Arguments) {
|
||||
opts := args.Get(2).(mobyclient.CopyToContainerOptions)
|
||||
assert.Equal(t, testcase.extractionRoot, opts.DestinationPath)
|
||||
header, err := tar.NewReader(opts.Content).Next()
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, testcase.entry, header.Name)
|
||||
}).Return(mobyclient.CopyToContainerResult{}, nil).Once()
|
||||
|
||||
cr := &containerReference{id: "stale", cli: client, input: &NewContainerInput{Name: "job-1"}}
|
||||
require.NoError(t, cr.Copy("/var/run/act", &FileEntry{Name: "x", Mode: 0o644})(ctx))
|
||||
assert.Equal(t, "fresh", cr.id)
|
||||
client.AssertExpectations(t)
|
||||
cr := &containerReference{id: "stale", cli: client, input: &NewContainerInput{Name: "job-1"}}
|
||||
if testcase.directory {
|
||||
source := t.TempDir()
|
||||
require.NoError(t, os.WriteFile(filepath.Join(source, "x"), nil, 0o644))
|
||||
require.NoError(t, cr.CopyDir(testcase.destination, source+string(filepath.Separator), false, true)(ctx))
|
||||
} else {
|
||||
require.NoError(t, cr.Copy(testcase.destination, &FileEntry{Name: "x", Mode: 0o644})(ctx))
|
||||
}
|
||||
assert.Equal(t, "fresh", cr.id)
|
||||
client.AssertExpectations(t)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Type assert containerReference implements ExecutionsEnvironment
|
||||
@@ -609,8 +615,8 @@ func TestCheckVolumes(t *testing.T) {
|
||||
}
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.desc, func(t *testing.T) {
|
||||
logger, hook := test.NewNullLogger()
|
||||
ctx := common.WithLogger(context.Background(), logger)
|
||||
logger, hook := logtest.NewLogger()
|
||||
ctx := common.WithLogger(t.Context(), logger)
|
||||
cr := &containerReference{
|
||||
input: &NewContainerInput{
|
||||
ValidVolumes: tc.validVolumes,
|
||||
@@ -619,7 +625,7 @@ func TestCheckVolumes(t *testing.T) {
|
||||
_, hostConf := cr.sanitizeConfig(ctx, &container.Config{}, &container.HostConfig{Binds: tc.binds, Mounts: tc.mounts})
|
||||
assert.Equal(t, tc.expectedBinds, hostConf.Binds)
|
||||
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
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -647,33 +653,33 @@ func TestMergeContainerConfigsKeepsTheRunnersFilesAndEnvToItself(t *testing.T) {
|
||||
t.Setenv("RUNNER_SECRET", "s3cr3t")
|
||||
|
||||
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}}
|
||||
|
||||
_, _, err := cr.mergeContainerConfigs(common.WithLogger(context.Background(), logger), &container.Config{}, &container.HostConfig{})
|
||||
_, _, err := cr.mergeContainerConfigs(common.WithLogger(t.Context(), logger), &container.Config{}, &container.HostConfig{})
|
||||
require.ErrorContains(t, err, "not allowed in a workflow")
|
||||
|
||||
// the runner reading its own files is what those options are for
|
||||
cr = &containerReference{input: &NewContainerInput{NetworkMode: "bridge", RunnerOptions: option}}
|
||||
_, _, err = cr.mergeContainerConfigs(common.WithLogger(context.Background(), logger), &container.Config{}, &container.HostConfig{})
|
||||
_, _, err = cr.mergeContainerConfigs(common.WithLogger(t.Context(), logger), &container.Config{}, &container.HostConfig{})
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
// 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{
|
||||
NetworkMode: "bridge",
|
||||
RunnerOptions: "--env RUNNER_SECRET",
|
||||
WorkflowOptions: "--env RUNNER_SECRET --env GIVEN=value",
|
||||
}}
|
||||
|
||||
config, _, err := cr.mergeContainerConfigs(common.WithLogger(context.Background(), logger), &container.Config{}, &container.HostConfig{})
|
||||
config, _, err := cr.mergeContainerConfigs(common.WithLogger(t.Context(), logger), &container.Config{}, &container.HostConfig{})
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, []string{"RUNNER_SECRET", "RUNNER_SECRET", "GIVEN=value"}, config.Env)
|
||||
}
|
||||
|
||||
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
|
||||
hostConfig := &container.HostConfig{
|
||||
@@ -706,9 +712,9 @@ func TestSanitizeOptionsHostConfig(t *testing.T) {
|
||||
}
|
||||
|
||||
// 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()
|
||||
logger, hook := test.NewNullLogger()
|
||||
logger, hook := logtest.NewLogger()
|
||||
cr := &containerReference{input: &NewContainerInput{
|
||||
RunnerOptions: runnerOptions,
|
||||
WorkflowOptions: workflowOptions,
|
||||
@@ -717,7 +723,7 @@ func mergeOptions(t *testing.T, runnerOptions, workflowOptions string, privilege
|
||||
ValidVolumes: []string{"**"},
|
||||
}}
|
||||
|
||||
_, hostConfig, err := cr.mergeContainerConfigs(common.WithLogger(context.Background(), logger), &container.Config{}, &container.HostConfig{
|
||||
_, hostConfig, err := cr.mergeContainerConfigs(common.WithLogger(t.Context(), logger), &container.Config{}, &container.HostConfig{
|
||||
Privileged: privileged,
|
||||
UsernsMode: container.UsernsMode("private"),
|
||||
NetworkMode: container.NetworkMode("bridge"),
|
||||
@@ -760,8 +766,8 @@ func TestMergeContainerConfigsStripsDangerousOptionsWhenUnprivileged(t *testing.
|
||||
}
|
||||
|
||||
func TestCheckVolumesRejectsEscapingHostPaths(t *testing.T) {
|
||||
logger, _ := test.NewNullLogger()
|
||||
ctx := common.WithLogger(context.Background(), logger)
|
||||
logger := logging.New(logtest.New())
|
||||
ctx := common.WithLogger(t.Context(), logger)
|
||||
|
||||
base := t.TempDir()
|
||||
allowed := filepath.Join(base, "allowed")
|
||||
@@ -801,14 +807,14 @@ func TestSanitizeConfigKeepsTmpfsMounts(t *testing.T) {
|
||||
require.Len(t, hostConfig.Mounts, 1)
|
||||
|
||||
for _, validVolumes := range [][]string{nil, {"data"}, {"**:ro"}} {
|
||||
logger, hook := test.NewNullLogger()
|
||||
_, sanitized := (&containerReference{input: &NewContainerInput{ValidVolumes: validVolumes}}).sanitizeConfig(common.WithLogger(context.Background(), logger), &container.Config{}, &container.HostConfig{Mounts: []mount.Mount{
|
||||
logger, hook := logtest.NewLogger()
|
||||
_, sanitized := (&containerReference{input: &NewContainerInput{ValidVolumes: validVolumes}}).sanitizeConfig(common.WithLogger(t.Context(), logger), &container.Config{}, &container.HostConfig{Mounts: []mount.Mount{
|
||||
hostConfig.Mounts[0],
|
||||
{Type: mount.TypeVolume, Target: "/anonymous"},
|
||||
{Type: mount.TypeTmpfs, Source: "/etc", Target: "/etc-copy"},
|
||||
}})
|
||||
assert.Equal(t, hostConfig.Mounts, sanitized.Mounts)
|
||||
assert.Len(t, hook.AllEntries(), 2)
|
||||
assert.Len(t, hook.AllRecords(), 2)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -891,8 +897,8 @@ func TestContainerInfoFromInspect(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestMergeContainerConfigsVolumesReplaceRunnerMounts(t *testing.T) {
|
||||
logger, _ := test.NewNullLogger()
|
||||
ctx := common.WithLogger(context.Background(), logger)
|
||||
logger := logging.New(logtest.New())
|
||||
ctx := common.WithLogger(t.Context(), logger)
|
||||
cr := &containerReference{
|
||||
input: &NewContainerInput{
|
||||
NetworkMode: "bridge",
|
||||
@@ -914,7 +920,7 @@ func TestMergeContainerConfigsVolumesReplaceRunnerMounts(t *testing.T) {
|
||||
func TestMergeContainerConfigsWarnsOnlyAboutOptionsThatWereGiven(t *testing.T) {
|
||||
warnings := func(runnerOptions, workflowOptions string) int {
|
||||
_, hook := mergeOptions(t, runnerOptions, workflowOptions, false)
|
||||
return len(hook.AllEntries())
|
||||
return len(hook.AllRecords())
|
||||
}
|
||||
|
||||
assert.Zero(t, warnings("--volume /host/tools:/opt/hostedtoolcache", ""))
|
||||
@@ -923,8 +929,37 @@ func TestMergeContainerConfigsWarnsOnlyAboutOptionsThatWereGiven(t *testing.T) {
|
||||
assert.Equal(t, 1, warnings("", "--privileged"))
|
||||
}
|
||||
|
||||
func TestDockerCreateKeepsInputEntrypoint(t *testing.T) {
|
||||
for _, entrypoint := range [][]string{nil, {"sleep", "3600"}} {
|
||||
for _, optionEntrypoint := range []string{"/sbin/dumb-init", ""} {
|
||||
logger, hook := logtest.NewLogger()
|
||||
cr := &containerReference{
|
||||
input: &NewContainerInput{
|
||||
NetworkMode: "bridge",
|
||||
Entrypoint: entrypoint,
|
||||
RunnerOptions: "--entrypoint=runner-entrypoint",
|
||||
WorkflowOptions: "--entrypoint=" + optionEntrypoint + " --init",
|
||||
},
|
||||
cli: &probeClient{create: func(opts mobyclient.ContainerCreateOptions) (mobyclient.ContainerCreateResult, error) {
|
||||
if len(entrypoint) > 0 {
|
||||
assert.Equal(t, entrypoint, opts.Config.Entrypoint)
|
||||
} else {
|
||||
assert.Equal(t, []string{optionEntrypoint}, opts.Config.Entrypoint)
|
||||
}
|
||||
require.NotNil(t, opts.HostConfig.Init)
|
||||
assert.True(t, *opts.HostConfig.Init)
|
||||
return mobyclient.ContainerCreateResult{ID: "created"}, nil
|
||||
}},
|
||||
}
|
||||
require.NoError(t, cr.create(nil, nil)(common.WithLogger(t.Context(), logger)))
|
||||
assert.Equal(t, "created", cr.id)
|
||||
assert.Equal(t, len(entrypoint) > 0, len(hook.AllRecords()) == 1)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestMergeContainerConfigsKeepsNetworkAliasesFromOptions(t *testing.T) {
|
||||
logger, _ := test.NewNullLogger()
|
||||
logger := logging.New(logtest.New())
|
||||
cr := &containerReference{input: &NewContainerInput{
|
||||
NetworkMode: "job-network", NetworkAliases: []string{"redis"}, WorkflowOptions: "--network-alias redis-primary",
|
||||
}}
|
||||
|
||||
@@ -4,7 +4,6 @@
|
||||
package container
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
mobyclient "github.com/moby/moby/client"
|
||||
@@ -15,7 +14,7 @@ import (
|
||||
// so the daemon has to be pinged explicitly here to decide whether to skip.
|
||||
func requireDocker(t *testing.T) {
|
||||
t.Helper()
|
||||
ctx := context.Background()
|
||||
ctx := t.Context()
|
||||
cli, err := GetDockerClient(ctx)
|
||||
if err != nil {
|
||||
t.Skipf("skipping: docker client unavailable: %v", err)
|
||||
|
||||
@@ -27,9 +27,6 @@ import (
|
||||
"gitea.com/gitea/runner/internal/pkg/process"
|
||||
|
||||
"github.com/creack/pty"
|
||||
"github.com/go-git/go-billy/v5/helper/polyfill"
|
||||
"github.com/go-git/go-billy/v5/osfs"
|
||||
"github.com/go-git/go-git/v5/plumbing/format/gitignore"
|
||||
"golang.org/x/term"
|
||||
)
|
||||
|
||||
@@ -100,25 +97,16 @@ func (e *HostEnvironment) CopyDir(destPath, srcPath string, useGitIgnore, skipGi
|
||||
srcPrefix += string(filepath.Separator)
|
||||
}
|
||||
logger.Debugf("Stripping prefix:%s src:%s", srcPrefix, srcPath)
|
||||
var ignorer gitignore.Matcher
|
||||
if useGitIgnore {
|
||||
ps, err := gitignore.ReadPatterns(polyfill.New(osfs.New(srcPath)), nil)
|
||||
if err != nil {
|
||||
logger.Debugf("Error loading .gitignore: %v", err)
|
||||
}
|
||||
|
||||
ignorer = gitignore.NewMatcher(ps)
|
||||
}
|
||||
fc := &filecollector.FileCollector{
|
||||
Ignorer: ignorer,
|
||||
SrcPath: srcPath,
|
||||
SrcPrefix: srcPrefix,
|
||||
SkipGitDir: skipGitDir,
|
||||
SrcPath: srcPath,
|
||||
SrcPrefix: srcPrefix,
|
||||
SkipGitDir: skipGitDir,
|
||||
UseGitIgnore: useGitIgnore,
|
||||
Handler: &filecollector.CopyCollector{
|
||||
DstDir: destPath,
|
||||
},
|
||||
}
|
||||
return filepath.Walk(srcPath, fc.CollectFiles(ctx, []string{}))
|
||||
return filepath.Walk(srcPath, fc.CollectFiles(ctx))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -152,7 +140,7 @@ func (e *HostEnvironment) GetContainerArchive(ctx context.Context, srcPath strin
|
||||
SrcPrefix: srcPrefix,
|
||||
Handler: tc,
|
||||
}
|
||||
err = filepath.Walk(srcPath, fc.CollectFiles(ctx, []string{}))
|
||||
err = filepath.Walk(srcPath, fc.CollectFiles(ctx))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -18,8 +18,8 @@ import (
|
||||
"time"
|
||||
|
||||
"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/require"
|
||||
)
|
||||
@@ -33,38 +33,25 @@ func TestActionPlatformNames(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestCopyDir(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
ctx := context.Background()
|
||||
e := &HostEnvironment{
|
||||
Path: filepath.Join(dir, "path"),
|
||||
TmpDir: filepath.Join(dir, "tmp"),
|
||||
ToolCache: filepath.Join(dir, "tool_cache"),
|
||||
ActPath: filepath.Join(dir, "act_path"),
|
||||
StdOut: os.Stdout,
|
||||
Workdir: path.Join("testdata", "scratch"),
|
||||
}
|
||||
_ = os.MkdirAll(e.Path, 0o700)
|
||||
_ = os.MkdirAll(e.TmpDir, 0o700)
|
||||
_ = os.MkdirAll(e.ToolCache, 0o700)
|
||||
_ = os.MkdirAll(e.ActPath, 0o700)
|
||||
err := e.CopyDir(e.Workdir, e.Path, true, false)(ctx)
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestCopyDirSkipGitDir(t *testing.T) {
|
||||
for name, skipGitDir := range map[string]bool{"kept for a workspace copy": false, "skipped for an action copy": true} {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
for _, test := range []struct {
|
||||
name string
|
||||
useGitIgnore, skipGitDir bool
|
||||
}{
|
||||
{name: "git dir kept for a workspace copy"},
|
||||
{name: "git dir skipped for an action copy", skipGitDir: true},
|
||||
{name: "unfiltered outside a git repository", useGitIgnore: true},
|
||||
} {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
src := filepath.Join(t.TempDir(), "action")
|
||||
require.NoError(t, os.MkdirAll(filepath.Join(src, ".git", "objects"), 0o700))
|
||||
require.NoError(t, os.WriteFile(filepath.Join(src, ".git", "objects", "pack"), []byte("x"), 0o600))
|
||||
require.NoError(t, os.WriteFile(filepath.Join(src, "action.yml"), []byte("name: x\n"), 0o600))
|
||||
|
||||
dest := filepath.Join(t.TempDir(), "dest")
|
||||
e := &HostEnvironment{StdOut: os.Stdout}
|
||||
require.NoError(t, e.CopyDir(dest, src+string(filepath.Separator), false, skipGitDir)(context.Background()))
|
||||
require.NoError(t, (&HostEnvironment{}).CopyDir(dest, src+string(filepath.Separator), test.useGitIgnore, test.skipGitDir)(t.Context()))
|
||||
|
||||
assert.FileExists(t, filepath.Join(dest, "action.yml"))
|
||||
if skipGitDir {
|
||||
if test.skipGitDir {
|
||||
assert.NoDirExists(t, filepath.Join(dest, ".git"))
|
||||
} else {
|
||||
assert.FileExists(t, filepath.Join(dest, ".git", "objects", "pack"))
|
||||
@@ -75,7 +62,7 @@ func TestCopyDirSkipGitDir(t *testing.T) {
|
||||
|
||||
func TestGetContainerArchive(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
ctx := context.Background()
|
||||
ctx := t.Context()
|
||||
e := &HostEnvironment{
|
||||
Path: filepath.Join(dir, "path"),
|
||||
TmpDir: filepath.Join(dir, "tmp"),
|
||||
@@ -110,7 +97,7 @@ func TestHostEnvironmentExecExitCode(t *testing.T) {
|
||||
t.Skip("uses POSIX shell")
|
||||
}
|
||||
dir := t.TempDir()
|
||||
ctx := context.Background()
|
||||
ctx := t.Context()
|
||||
e := &HostEnvironment{
|
||||
Path: filepath.Join(dir, "path"),
|
||||
TmpDir: filepath.Join(dir, "tmp"),
|
||||
@@ -161,7 +148,7 @@ func TestHostEnvironmentAllocatePTY(t *testing.T) {
|
||||
err := e.Exec(
|
||||
[]string{"sh", "-c", "[ -t 1 ] && printf TTY || printf NOTTY"},
|
||||
map[string]string{"PATH": os.Getenv("PATH")}, "", "",
|
||||
)(context.Background())
|
||||
)(t.Context())
|
||||
require.NoError(t, err)
|
||||
got := strings.TrimSpace(strings.ReplaceAll(buf.String(), "\r", ""))
|
||||
assert.Equal(t, tc.expect, got)
|
||||
@@ -170,8 +157,7 @@ func TestHostEnvironmentAllocatePTY(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestHostEnvironmentRemovePreservesWorkdirByDefault(t *testing.T) {
|
||||
logger := logrus.New()
|
||||
ctx := common.WithLogger(context.Background(), logrus.NewEntry(logger))
|
||||
ctx := common.WithLogger(t.Context(), logging.Default())
|
||||
base := t.TempDir()
|
||||
miscRoot := filepath.Join(base, "misc")
|
||||
path := filepath.Join(miscRoot, "hostexecutor")
|
||||
@@ -193,8 +179,7 @@ func TestHostEnvironmentRemovePreservesWorkdirByDefault(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestHostEnvironmentRemoveCleansWorkdirWhenOwned(t *testing.T) {
|
||||
logger := logrus.New()
|
||||
ctx := common.WithLogger(context.Background(), logrus.NewEntry(logger))
|
||||
ctx := common.WithLogger(t.Context(), logging.Default())
|
||||
base := t.TempDir()
|
||||
miscRoot := filepath.Join(base, "misc")
|
||||
path := filepath.Join(miscRoot, "hostexecutor")
|
||||
@@ -235,7 +220,7 @@ func TestRemoveAllWithContextDoesNotHangOnStuckDelete(t *testing.T) {
|
||||
removeAll = orig
|
||||
})
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 100*time.Millisecond)
|
||||
ctx, cancel := context.WithTimeout(t.Context(), 100*time.Millisecond)
|
||||
defer cancel()
|
||||
|
||||
err := removeAllWithContext(ctx, t.TempDir())
|
||||
@@ -252,8 +237,7 @@ func TestHostEnvironmentRemoveDoesNotHangOnStuckCleanUp(t *testing.T) {
|
||||
hostCleanupTimeout = 100 * time.Millisecond
|
||||
t.Cleanup(func() { hostCleanupTimeout = orig })
|
||||
|
||||
logger := logrus.New()
|
||||
ctx := common.WithLogger(context.Background(), logrus.NewEntry(logger))
|
||||
ctx := common.WithLogger(t.Context(), logging.Default())
|
||||
base := t.TempDir()
|
||||
path := filepath.Join(base, "misc", "hostexecutor")
|
||||
require.NoError(t, os.MkdirAll(path, 0o700))
|
||||
@@ -306,8 +290,7 @@ func TestHostEnvironmentRemoveDoesNotHangOnStuckPathRemoval(t *testing.T) {
|
||||
removeAll = origRemoveAll
|
||||
})
|
||||
|
||||
logger := logrus.New()
|
||||
ctx := common.WithLogger(context.Background(), logrus.NewEntry(logger))
|
||||
ctx := common.WithLogger(t.Context(), logging.Default())
|
||||
base := t.TempDir()
|
||||
path := filepath.Join(base, "misc", "hostexecutor")
|
||||
require.NoError(t, os.MkdirAll(path, 0o700))
|
||||
|
||||
@@ -11,7 +11,7 @@ import (
|
||||
"runtime"
|
||||
"strings"
|
||||
|
||||
log "github.com/sirupsen/logrus"
|
||||
"gitea.com/gitea/runner/act/common/logging"
|
||||
)
|
||||
|
||||
type LinuxContainerEnvironmentExtensions struct{}
|
||||
@@ -21,13 +21,13 @@ type LinuxContainerEnvironmentExtensions struct{}
|
||||
// For use in docker volumes and binds
|
||||
func (*LinuxContainerEnvironmentExtensions) ToContainerPath(path string) string {
|
||||
if runtime.GOOS == "windows" && strings.Contains(path, "/") {
|
||||
log.Error("You cannot specify linux style local paths (/mnt/etc) on Windows as it does not understand them.")
|
||||
logging.Error("You cannot specify linux style local paths (/mnt/etc) on Windows as it does not understand them.")
|
||||
return ""
|
||||
}
|
||||
|
||||
abspath, err := filepath.Abs(path)
|
||||
if err != nil {
|
||||
log.Error(err)
|
||||
logging.Error(err)
|
||||
return ""
|
||||
}
|
||||
|
||||
|
||||
@@ -11,7 +11,8 @@ import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
log "github.com/sirupsen/logrus"
|
||||
"gitea.com/gitea/runner/act/common/logging"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
@@ -27,7 +28,7 @@ func TestContainerPath(t *testing.T) {
|
||||
if runtime.GOOS == "windows" {
|
||||
cwd, err := os.Getwd()
|
||||
if err != nil {
|
||||
log.Error(err)
|
||||
logging.Error(err)
|
||||
}
|
||||
|
||||
rootDrive := os.Getenv("SystemDrive")
|
||||
@@ -49,7 +50,7 @@ func TestContainerPath(t *testing.T) {
|
||||
} else {
|
||||
cwd, err := os.Getwd()
|
||||
if err != nil {
|
||||
log.Error(err)
|
||||
logging.Error(err)
|
||||
}
|
||||
for _, v := range []containerPathJob{
|
||||
{"/home/act/go/src/github.com/nektos/act", "/home/act/go/src/github.com/nektos/act", ""},
|
||||
|
||||
@@ -5,7 +5,6 @@ package container
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"context"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
@@ -28,7 +27,7 @@ func TestParseEnvFileSingleLine(t *testing.T) {
|
||||
require.NoError(t, os.WriteFile(envPath, []byte("FOO=bar\nBAZ=qux\n"), 0o600))
|
||||
|
||||
env := map[string]string{}
|
||||
require.NoError(t, parseEnvFile(e, envPath, &env)(context.Background()))
|
||||
require.NoError(t, parseEnvFile(e, envPath, &env)(t.Context()))
|
||||
assert.Equal(t, "bar", env["FOO"])
|
||||
assert.Equal(t, "qux", env["BAZ"])
|
||||
}
|
||||
@@ -39,7 +38,7 @@ func TestParseEnvFileMultiLine(t *testing.T) {
|
||||
require.NoError(t, os.WriteFile(envPath, []byte(content), 0o600))
|
||||
|
||||
env := map[string]string{}
|
||||
require.NoError(t, parseEnvFile(e, envPath, &env)(context.Background()))
|
||||
require.NoError(t, parseEnvFile(e, envPath, &env)(t.Context()))
|
||||
assert.Equal(t, "line1\nline2", env["FOO"])
|
||||
}
|
||||
|
||||
@@ -50,7 +49,7 @@ func TestParseEnvFileLargeValueWithinLimit(t *testing.T) {
|
||||
require.NoError(t, os.WriteFile(envPath, []byte(content), 0o600))
|
||||
|
||||
env := map[string]string{}
|
||||
require.NoError(t, parseEnvFile(e, envPath, &env)(context.Background()))
|
||||
require.NoError(t, parseEnvFile(e, envPath, &env)(t.Context()))
|
||||
assert.Equal(t, big, env["FOO"])
|
||||
}
|
||||
|
||||
@@ -61,7 +60,7 @@ func TestParseEnvFileLineExceedsBufferReportsScannerError(t *testing.T) {
|
||||
require.NoError(t, os.WriteFile(envPath, []byte(content), 0o600))
|
||||
|
||||
env := map[string]string{}
|
||||
err := parseEnvFile(e, envPath, &env)(context.Background())
|
||||
err := parseEnvFile(e, envPath, &env)(t.Context())
|
||||
require.ErrorIs(t, err, bufio.ErrTooLong)
|
||||
assert.Contains(t, err.Error(), "reading env file")
|
||||
}
|
||||
@@ -73,7 +72,7 @@ func TestParseEnvFileBlankLines(t *testing.T) {
|
||||
require.NoError(t, os.WriteFile(envPath, []byte("\nFOO=bar\n\n \nBAZ=qux\n\n"), 0o600))
|
||||
|
||||
env := map[string]string{}
|
||||
require.NoError(t, parseEnvFile(e, envPath, &env)(context.Background()))
|
||||
require.NoError(t, parseEnvFile(e, envPath, &env)(t.Context()))
|
||||
assert.Equal(t, "bar", env["FOO"])
|
||||
assert.Equal(t, "qux", env["BAZ"])
|
||||
}
|
||||
@@ -84,12 +83,12 @@ func TestParseEnvFileMultiLineKeepsBlankLines(t *testing.T) {
|
||||
require.NoError(t, os.WriteFile(envPath, []byte("FOO<<EOF\nline1\n\nline2\nEOF\n"), 0o600))
|
||||
|
||||
env := map[string]string{}
|
||||
require.NoError(t, parseEnvFile(e, envPath, &env)(context.Background()))
|
||||
require.NoError(t, parseEnvFile(e, envPath, &env)(t.Context()))
|
||||
assert.Equal(t, "line1\n\nline2", env["FOO"])
|
||||
|
||||
require.NoError(t, os.WriteFile(envPath, []byte("FOO<<EOF\n\nline2\nEOF\n"), 0o600))
|
||||
env = map[string]string{}
|
||||
require.NoError(t, parseEnvFile(e, envPath, &env)(context.Background()))
|
||||
require.NoError(t, parseEnvFile(e, envPath, &env)(t.Context()))
|
||||
assert.Equal(t, "\nline2", env["FOO"])
|
||||
}
|
||||
|
||||
@@ -99,7 +98,7 @@ func TestParseEnvFileUTF8BOM(t *testing.T) {
|
||||
require.NoError(t, os.WriteFile(envPath, content, 0o600))
|
||||
|
||||
env := map[string]string{}
|
||||
require.NoError(t, parseEnvFile(e, envPath, &env)(context.Background()))
|
||||
require.NoError(t, parseEnvFile(e, envPath, &env)(t.Context()))
|
||||
assert.Equal(t, "bar", env["FOO"])
|
||||
}
|
||||
|
||||
@@ -121,7 +120,7 @@ func TestParseEnvFileUTF16(t *testing.T) {
|
||||
require.NoError(t, os.WriteFile(envPath, content, 0o600))
|
||||
|
||||
env := map[string]string{}
|
||||
require.NoError(t, parseEnvFile(e, envPath, &env)(context.Background()))
|
||||
require.NoError(t, parseEnvFile(e, envPath, &env)(t.Context()))
|
||||
assert.Equal(t, "bar", env["FOO"])
|
||||
assert.Equal(t, "line1", env["MULTI"])
|
||||
})
|
||||
@@ -133,7 +132,7 @@ func TestParseEnvFileMissingDelimiter(t *testing.T) {
|
||||
require.NoError(t, os.WriteFile(envPath, []byte("FOO<<EOF\nline1\nline2\n"), 0o600))
|
||||
|
||||
env := map[string]string{}
|
||||
err := parseEnvFile(e, envPath, &env)(context.Background())
|
||||
err := parseEnvFile(e, envPath, &env)(t.Context())
|
||||
require.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "delimiter")
|
||||
}
|
||||
+6
-20
@@ -13,10 +13,6 @@ import (
|
||||
|
||||
"gitea.com/gitea/runner/act/common"
|
||||
"gitea.com/gitea/runner/act/filecollector"
|
||||
|
||||
"github.com/go-git/go-billy/v5/helper/polyfill"
|
||||
"github.com/go-git/go-billy/v5/osfs"
|
||||
"github.com/go-git/go-git/v5/plumbing/format/gitignore"
|
||||
)
|
||||
|
||||
func writeFilesTar(ctx context.Context, w io.Writer, uid, gid int, files ...*FileEntry) error {
|
||||
@@ -46,23 +42,13 @@ func writeDirTar(ctx context.Context, w io.Writer, dstPath, srcPath string, useG
|
||||
srcPrefix := strings.TrimSuffix(filepath.Dir(srcPath), string(filepath.Separator)) + string(filepath.Separator)
|
||||
logger.Debugf("Stripping prefix:%s src:%s", srcPrefix, srcPath)
|
||||
|
||||
var ignorer gitignore.Matcher
|
||||
if useGitIgnore {
|
||||
ps, err := gitignore.ReadPatterns(polyfill.New(osfs.New(srcPath)), nil)
|
||||
if err != nil {
|
||||
logger.Debugf("Error loading .gitignore: %v", err)
|
||||
}
|
||||
|
||||
ignorer = gitignore.NewMatcher(ps)
|
||||
}
|
||||
|
||||
if err := filepath.Walk(srcPath, (&filecollector.FileCollector{
|
||||
Ignorer: ignorer,
|
||||
SrcPath: srcPath,
|
||||
SrcPrefix: srcPrefix,
|
||||
SkipGitDir: skipGitDir,
|
||||
Handler: &filecollector.TarCollector{TarWriter: tw, UID: uid, GID: gid, DstDir: dstPath[1:]},
|
||||
}).CollectFiles(ctx, []string{})); err != nil {
|
||||
SrcPath: srcPath,
|
||||
SrcPrefix: srcPrefix,
|
||||
SkipGitDir: skipGitDir,
|
||||
UseGitIgnore: useGitIgnore,
|
||||
Handler: &filecollector.TarCollector{TarWriter: tw, UID: uid, GID: gid, DstDir: dstPath[1:]},
|
||||
}).CollectFiles(ctx)); err != nil {
|
||||
return err
|
||||
}
|
||||
return tw.Close()
|
||||
|
||||
Vendored
-5
@@ -1,5 +0,0 @@
|
||||
FROM scratch
|
||||
ENV PATH="/this/path/does/not/exists/anywhere:/this/either"
|
||||
ENV SOME_RANDOM_VAR=""
|
||||
ENV ANOTHER_ONE="BUT_I_HAVE_VALUE"
|
||||
ENV CONFLICT_VAR="I_EXIST_ONLY_HERE"
|
||||
@@ -16,10 +16,10 @@ import (
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
git "github.com/go-git/go-git/v5"
|
||||
"github.com/go-git/go-git/v5/plumbing/filemode"
|
||||
"github.com/go-git/go-git/v5/plumbing/format/gitignore"
|
||||
"github.com/go-git/go-git/v5/plumbing/format/index"
|
||||
"gitea.com/gitea/runner/act/common"
|
||||
"gitea.com/gitea/runner/act/common/git"
|
||||
|
||||
"golang.org/x/text/unicode/norm"
|
||||
)
|
||||
|
||||
type Handler interface {
|
||||
@@ -94,73 +94,62 @@ func (cc *CopyCollector) WriteFile(fpath string, fi fs.FileInfo, linkName string
|
||||
}
|
||||
|
||||
type FileCollector struct {
|
||||
Ignorer gitignore.Matcher
|
||||
SrcPath string
|
||||
SrcPrefix string
|
||||
SkipGitDir bool
|
||||
Handler Handler
|
||||
SrcPath string
|
||||
SrcPrefix string
|
||||
SkipGitDir bool
|
||||
UseGitIgnore bool
|
||||
Handler Handler
|
||||
}
|
||||
|
||||
func openGitIndex(path string) (*index.Index, error) {
|
||||
repo, err := git.PlainOpen(path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
// foldPath compares git's spelling of a path with the filesystem's, which may differ in case or Unicode normalization.
|
||||
func foldPath(name string) string {
|
||||
return strings.ToLower(norm.NFC.String(name))
|
||||
}
|
||||
|
||||
func (fc *FileCollector) gitFiles(ctx context.Context) map[string]bool {
|
||||
if !fc.UseGitIgnore {
|
||||
return nil
|
||||
}
|
||||
return repo.Storer.Index()
|
||||
files, err := git.ListFiles(ctx, fc.SrcPath)
|
||||
if err != nil {
|
||||
common.Logger(ctx).Debugf("Copying %s without applying .gitignore: %v", fc.SrcPath, err)
|
||||
return nil
|
||||
}
|
||||
keep := map[string]bool{".": true}
|
||||
for _, file := range files {
|
||||
for file = foldPath(file); !keep[file]; file = path.Dir(file) {
|
||||
keep[file] = true
|
||||
}
|
||||
}
|
||||
return keep
|
||||
}
|
||||
|
||||
func (fc *FileCollector) CollectFiles(ctx context.Context, submodulePath []string) filepath.WalkFunc {
|
||||
i, _ := openGitIndex(path.Join(fc.SrcPath, path.Join(submodulePath...)))
|
||||
func (fc *FileCollector) CollectFiles(ctx context.Context) filepath.WalkFunc {
|
||||
keep := fc.gitFiles(ctx)
|
||||
return func(file string, fi os.FileInfo, err error) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if ctx != nil && ctx.Err() != nil {
|
||||
if ctx.Err() != nil {
|
||||
return errors.New("copy cancelled")
|
||||
}
|
||||
|
||||
sansPrefix := strings.TrimPrefix(file, fc.SrcPrefix)
|
||||
split := strings.Split(sansPrefix, string(filepath.Separator))
|
||||
// The root folders should be skipped, submodules only have the last path component set to "." by filepath.Walk
|
||||
if fi.IsDir() && len(split) > 0 && split[len(split)-1] == "." {
|
||||
return nil
|
||||
}
|
||||
if fc.SkipGitDir && split[len(split)-1] == ".git" {
|
||||
if fc.SkipGitDir && fi.Name() == ".git" {
|
||||
if fi.IsDir() {
|
||||
return filepath.SkipDir
|
||||
}
|
||||
return nil
|
||||
}
|
||||
var entry *index.Entry
|
||||
if i != nil {
|
||||
entry, err = i.Entry(strings.Join(split[len(submodulePath):], "/"))
|
||||
} else {
|
||||
err = index.ErrEntryNotFound
|
||||
}
|
||||
if err != nil && fc.Ignorer != nil && fc.Ignorer.Match(split, fi.IsDir()) {
|
||||
if fi.IsDir() {
|
||||
if i != nil {
|
||||
ms, err := i.Glob(strings.Join(append(split[len(submodulePath):], "**"), "/"))
|
||||
if err != nil || len(ms) == 0 {
|
||||
return filepath.SkipDir
|
||||
}
|
||||
} else {
|
||||
if keep != nil {
|
||||
rel, _ := filepath.Rel(fc.SrcPath, file)
|
||||
if rel = filepath.ToSlash(rel); !keep[foldPath(rel)] && !strings.Contains("/"+rel+"/", "/.git/") {
|
||||
if fi.IsDir() {
|
||||
return filepath.SkipDir
|
||||
}
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
if err == nil && entry.Mode == filemode.Submodule {
|
||||
err = filepath.Walk(file, fc.CollectFiles(ctx, split))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return filepath.SkipDir
|
||||
}
|
||||
path := filepath.ToSlash(sansPrefix)
|
||||
path := filepath.ToSlash(strings.TrimPrefix(file, fc.SrcPrefix))
|
||||
|
||||
// return on non-regular files (thanks to [kumo](https://medium.com/@komuw/just-like-you-did-fbdd7df829d3) for this suggested update)
|
||||
if fi.Mode()&os.ModeSymlink == os.ModeSymlink {
|
||||
linkName, err := os.Readlink(file)
|
||||
if err != nil {
|
||||
@@ -171,17 +160,12 @@ func (fc *FileCollector) CollectFiles(ctx context.Context, submodulePath []strin
|
||||
return nil
|
||||
}
|
||||
|
||||
// open file
|
||||
f, err := os.Open(file)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
if ctx != nil {
|
||||
stop := context.AfterFunc(ctx, func() { _ = f.Close() })
|
||||
defer stop()
|
||||
}
|
||||
defer context.AfterFunc(ctx, func() { _ = f.Close() })()
|
||||
|
||||
return fc.Handler.WriteFile(path, fi, "", f)
|
||||
}
|
||||
|
||||
@@ -9,106 +9,78 @@ import (
|
||||
"bytes"
|
||||
"context"
|
||||
"io"
|
||||
"maps"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
git "github.com/go-git/go-git/v5"
|
||||
"github.com/go-git/go-git/v5/plumbing/format/gitignore"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestIgnoredTrackedfile(t *testing.T) {
|
||||
func gitCmd(t *testing.T, args ...string) {
|
||||
t.Helper()
|
||||
out, err := exec.Command("git", args...).CombinedOutput()
|
||||
require.NoError(t, err, string(out))
|
||||
}
|
||||
|
||||
func TestCollectFilesKeepsWhatGitTracksOrDoesNotIgnore(t *testing.T) {
|
||||
repoDir := filepath.Join(t.TempDir(), "mygitrepo")
|
||||
repo, err := git.PlainInit(repoDir, false)
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, os.WriteFile(filepath.Join(repoDir, ".gitignore"), []byte(".*\n"), 0o644))
|
||||
require.NoError(t, os.WriteFile(filepath.Join(repoDir, ".env"), []byte("test=val1\n"), 0o644))
|
||||
worktree, err := repo.Worktree()
|
||||
require.NoError(t, err)
|
||||
_, err = worktree.Add(".gitignore")
|
||||
require.NoError(t, err)
|
||||
for _, dir := range []string{repoDir, filepath.Join(repoDir, "sub"), filepath.Join(repoDir, "nested")} {
|
||||
gitCmd(t, "init", "-q", dir)
|
||||
}
|
||||
for name, body := range map[string]string{
|
||||
".gitignore": ".*\nign/\n",
|
||||
".env": "",
|
||||
"Renamed": "",
|
||||
"ign/tracked": "",
|
||||
"ign/untracked": "",
|
||||
"sub/.gitignore": "dropped\n",
|
||||
"sub/dropped": "",
|
||||
"sub/kept": "",
|
||||
"nested/kept": "",
|
||||
} {
|
||||
require.NoError(t, os.MkdirAll(filepath.Dir(filepath.Join(repoDir, name)), 0o755))
|
||||
require.NoError(t, os.WriteFile(filepath.Join(repoDir, name), []byte(body), 0o644))
|
||||
}
|
||||
require.NoError(t, os.Mkdir(filepath.Join(repoDir, "uninitialized"), 0o755))
|
||||
gitCmd(t, "-C", repoDir, "add", "-f", ".gitignore", "Renamed", "ign/tracked")
|
||||
require.NoError(t, os.Rename(filepath.Join(repoDir, "Renamed"), filepath.Join(repoDir, "renamed")))
|
||||
for _, gitlink := range []string{"sub", "uninitialized"} {
|
||||
gitCmd(t, "-C", repoDir, "update-index", "--add", "--cacheinfo", "160000,4b825dc642cb6eb9a060e54bf8d69288fbee4904,"+gitlink)
|
||||
}
|
||||
expected := map[string]string{".gitignore": "", "ign/tracked": "", "nested/kept": "", "renamed": "", "sub/.gitignore": "", "sub/kept": ""}
|
||||
if runtime.GOOS != "windows" {
|
||||
require.NoError(t, os.Symlink(".env", filepath.Join(repoDir, "test.env")))
|
||||
expected["test.env"] = ".env"
|
||||
}
|
||||
|
||||
var archive bytes.Buffer
|
||||
tw := tar.NewWriter(&archive)
|
||||
patterns, err := gitignore.ReadPatterns(worktree.Filesystem, nil)
|
||||
require.NoError(t, err)
|
||||
ignorer := gitignore.NewMatcher(patterns)
|
||||
fc := &FileCollector{
|
||||
Ignorer: ignorer,
|
||||
SrcPath: repoDir,
|
||||
SrcPrefix: repoDir + string(filepath.Separator),
|
||||
Handler: &TarCollector{
|
||||
TarWriter: tw,
|
||||
},
|
||||
}
|
||||
err = filepath.Walk(repoDir, fc.CollectFiles(context.Background(), nil))
|
||||
assert.NoError(t, err, "successfully collect files")
|
||||
fc := &FileCollector{SrcPath: repoDir, SrcPrefix: repoDir + string(filepath.Separator), UseGitIgnore: true, Handler: &TarCollector{TarWriter: tw}}
|
||||
require.NoError(t, filepath.Walk(repoDir, fc.CollectFiles(t.Context())))
|
||||
require.NoError(t, tw.Close())
|
||||
|
||||
files := map[string]string{}
|
||||
tr := tar.NewReader(&archive)
|
||||
h, err := tr.Next()
|
||||
assert.NoError(t, err, "tar must not be empty") //nolint:testifylint // pre-existing issue from nektos/act
|
||||
assert.Equal(t, ".gitignore", h.Name)
|
||||
_, err = tr.Next()
|
||||
assert.ErrorIs(t, err, io.EOF, "tar must only contain one element")
|
||||
for header, err := tr.Next(); err != io.EOF; header, err = tr.Next() {
|
||||
require.NoError(t, err)
|
||||
files[header.Name] = header.Linkname
|
||||
}
|
||||
assert.Contains(t, files, ".git/HEAD")
|
||||
maps.DeleteFunc(files, func(name, _ string) bool { return strings.Contains("/"+name, "/.git/") })
|
||||
assert.Equal(t, expected, files)
|
||||
}
|
||||
|
||||
func TestSymlinks(t *testing.T) {
|
||||
if runtime.GOOS == "windows" {
|
||||
t.Skip("creating symlinks requires elevated privileges on Windows")
|
||||
}
|
||||
repoDir := filepath.Join(t.TempDir(), "mygitrepo")
|
||||
repo, err := git.PlainInit(repoDir, false)
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, os.WriteFile(filepath.Join(repoDir, ".env"), []byte("test=val1\n"), 0o644))
|
||||
require.NoError(t, os.Symlink(".env", filepath.Join(repoDir, "test.env")))
|
||||
worktree, err := repo.Worktree()
|
||||
require.NoError(t, err)
|
||||
_, err = worktree.Add("test.env")
|
||||
require.NoError(t, err)
|
||||
|
||||
var archive bytes.Buffer
|
||||
tw := tar.NewWriter(&archive)
|
||||
fc := &FileCollector{
|
||||
SrcPath: repoDir,
|
||||
SrcPrefix: repoDir + string(filepath.Separator),
|
||||
Handler: &TarCollector{
|
||||
TarWriter: tw,
|
||||
},
|
||||
}
|
||||
err = filepath.Walk(repoDir, fc.CollectFiles(context.Background(), nil))
|
||||
assert.NoError(t, err, "successfully collect files")
|
||||
require.NoError(t, tw.Close())
|
||||
tr := tar.NewReader(&archive)
|
||||
h, err := tr.Next()
|
||||
files := map[string]tar.Header{}
|
||||
for err == nil {
|
||||
files[h.Name] = *h
|
||||
h, err = tr.Next()
|
||||
}
|
||||
|
||||
assert.Equal(t, ".env", files[".env"].Name)
|
||||
assert.Equal(t, "test.env", files["test.env"].Name)
|
||||
assert.Equal(t, ".env", files["test.env"].Linkname)
|
||||
assert.ErrorIs(t, err, io.EOF, "tar must be read cleanly to EOF")
|
||||
}
|
||||
|
||||
// Regression for https://gitea.com/gitea/runner/issues/876 and /941:
|
||||
// re-copying an action directory must overwrite a pre-existing read-only
|
||||
// file (e.g. a git pack .idx at mode 0444) instead of failing with EACCES
|
||||
// on macOS or "Access is denied" on Windows.
|
||||
func TestCopyCollectorWriteFileOverwritesReadOnlyFile(t *testing.T) {
|
||||
dst := t.TempDir()
|
||||
target := filepath.Join(dst, "sub", "pack.idx")
|
||||
require.NoError(t, os.MkdirAll(filepath.Dir(target), 0o755))
|
||||
require.NoError(t, os.WriteFile(target, []byte("old"), 0o444))
|
||||
|
||||
src := filepath.Join(t.TempDir(), "pack.idx")
|
||||
require.NoError(t, os.WriteFile(src, []byte("new"), 0o444))
|
||||
fi, err := os.Stat(src)
|
||||
fi, err := os.Stat(target)
|
||||
require.NoError(t, err)
|
||||
|
||||
cc := &CopyCollector{DstDir: dst}
|
||||
@@ -119,8 +91,6 @@ func TestCopyCollectorWriteFileOverwritesReadOnlyFile(t *testing.T) {
|
||||
assert.Equal(t, "new", string(got))
|
||||
}
|
||||
|
||||
// Without the destination removal, os.Symlink fails with EEXIST when the
|
||||
// path already holds a regular file from an earlier copy of the action.
|
||||
func TestCopyCollectorWriteFileOverwritesFileWithSymlink(t *testing.T) {
|
||||
if runtime.GOOS == "windows" {
|
||||
t.Skip("creating symlinks requires elevated privileges on Windows")
|
||||
@@ -129,11 +99,8 @@ func TestCopyCollectorWriteFileOverwritesFileWithSymlink(t *testing.T) {
|
||||
target := filepath.Join(dst, "link")
|
||||
require.NoError(t, os.WriteFile(target, []byte("stale"), 0o644))
|
||||
|
||||
fi, err := os.Lstat(target)
|
||||
require.NoError(t, err)
|
||||
|
||||
cc := &CopyCollector{DstDir: dst}
|
||||
require.NoError(t, cc.WriteFile("link", fi, "target", nil))
|
||||
require.NoError(t, cc.WriteFile("link", nil, "target", nil))
|
||||
|
||||
resolved, err := os.Readlink(target)
|
||||
require.NoError(t, err)
|
||||
@@ -143,7 +110,7 @@ func TestCopyCollectorWriteFileOverwritesFileWithSymlink(t *testing.T) {
|
||||
func TestFileCollectorCancellationAndWalkError(t *testing.T) {
|
||||
ctx, cancel := context.WithCancel(t.Context())
|
||||
cancel()
|
||||
walk := (&FileCollector{}).CollectFiles(ctx, nil)
|
||||
walk := (&FileCollector{}).CollectFiles(ctx)
|
||||
|
||||
err := walk("file", nil, nil)
|
||||
require.EqualError(t, err, "copy cancelled")
|
||||
|
||||
@@ -74,7 +74,7 @@ func SetRef(ctx context.Context, ghc *model.GithubContext, repoPath string) {
|
||||
if ghc.Ref == "" {
|
||||
ref, err := pinned(ctx, "ref:"+repoPath, func() (string, error) { return findGitRef(ctx, repoPath) })
|
||||
if err != nil {
|
||||
logger.Warningf("unable to get git ref: %v", err)
|
||||
logger.Warnf("unable to get git ref: %v", err)
|
||||
} else {
|
||||
logger.Debugf("using github ref: %s", ref)
|
||||
ghc.Ref = ref
|
||||
@@ -121,7 +121,7 @@ func SetSha(ctx context.Context, ghc *model.GithubContext, repoPath string) {
|
||||
return revision, err
|
||||
})
|
||||
if err != nil {
|
||||
logger.Warningf("unable to get git revision: %v", err)
|
||||
logger.Warnf("unable to get git revision: %v", err)
|
||||
} else {
|
||||
ghc.Sha = sha
|
||||
}
|
||||
@@ -130,11 +130,11 @@ func SetSha(ctx context.Context, ghc *model.GithubContext, repoPath string) {
|
||||
|
||||
// SetRepositoryAndOwner resolves the repository of the context from the git
|
||||
// remote in repoPath when it is not set yet, and derives its owner.
|
||||
func SetRepositoryAndOwner(ctx context.Context, ghc *model.GithubContext, githubInstance, repoPath string) {
|
||||
func SetRepositoryAndOwner(ctx context.Context, ghc *model.GithubContext, repoPath string) {
|
||||
if ghc.Repository == "" {
|
||||
repo, err := findGithubRepo(ctx, repoPath, githubInstance)
|
||||
repo, err := findGithubRepo(ctx, repoPath)
|
||||
if err != nil {
|
||||
common.Logger(ctx).Warningf("unable to get git repo (githubInstance: %v, repoPath: %v): %v", githubInstance, repoPath, err)
|
||||
common.Logger(ctx).Warnf("unable to get git repo (repoPath: %v): %v", repoPath, err)
|
||||
return
|
||||
}
|
||||
ghc.Repository = repo
|
||||
|
||||
@@ -9,13 +9,14 @@ import (
|
||||
"errors"
|
||||
"testing"
|
||||
|
||||
"gitea.com/gitea/runner/act/common/logging"
|
||||
|
||||
"gitea.dev/actionslib/pkg/model"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestSetRef(t *testing.T) {
|
||||
log.SetLevel(log.DebugLevel)
|
||||
logging.SetLevel(logging.LevelDebug)
|
||||
|
||||
oldFindGitRef := findGitRef
|
||||
oldFindGitRevision := findGitRevision
|
||||
@@ -104,7 +105,7 @@ func TestSetRef(t *testing.T) {
|
||||
Event: table.event,
|
||||
}
|
||||
|
||||
SetRef(context.Background(), ghc, "/some/dir")
|
||||
SetRef(t.Context(), ghc, "/some/dir")
|
||||
ghc.SetRefTypeAndName()
|
||||
|
||||
assert.Equal(t, table.ref, ghc.Ref)
|
||||
@@ -122,14 +123,14 @@ func TestSetRef(t *testing.T) {
|
||||
Event: map[string]any{},
|
||||
}
|
||||
|
||||
SetRef(context.Background(), ghc, "/some/dir")
|
||||
SetRef(t.Context(), ghc, "/some/dir")
|
||||
|
||||
assert.Equal(t, "refs/heads/master", ghc.Ref)
|
||||
})
|
||||
}
|
||||
|
||||
func TestSetSha(t *testing.T) {
|
||||
log.SetLevel(log.DebugLevel)
|
||||
logging.SetLevel(logging.LevelDebug)
|
||||
|
||||
oldFindGitRef := findGitRef
|
||||
oldFindGitRevision := findGitRevision
|
||||
@@ -209,14 +210,14 @@ func TestSetSha(t *testing.T) {
|
||||
Event: table.event,
|
||||
}
|
||||
|
||||
SetSha(context.Background(), ghc, "/some/dir")
|
||||
SetSha(t.Context(), ghc, "/some/dir")
|
||||
|
||||
assert.Equal(t, table.sha, ghc.Sha)
|
||||
})
|
||||
}
|
||||
|
||||
t.Run("pinned checkout keeps the first sha and ref", func(t *testing.T) {
|
||||
ctx := WithPinnedCheckout(context.Background())
|
||||
ctx := WithPinnedCheckout(t.Context())
|
||||
for _, head := range []string{"first", "moved"} {
|
||||
findGitRevision = func(context.Context, string) (string, string, error) { return "", head, nil }
|
||||
findGitRef = func(context.Context, string) (string, error) { return "refs/heads/" + head, nil }
|
||||
|
||||
+13
-26
@@ -145,17 +145,17 @@ func maybeCopyToActionDir(ctx context.Context, step actionStep, actionDir, actio
|
||||
containerActionDirCopy += `/`
|
||||
}
|
||||
|
||||
defer git.AcquireCloneLock(actionDir)()
|
||||
unlock, err := git.AcquireCloneLock(ctx, actionDir)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer unlock()
|
||||
|
||||
if !rc.Config.NoActionPatch {
|
||||
// A concurrent job's prepare resets this directory, so patch under the copy's lock.
|
||||
defer patchActions(ctx, actionScriptPaths(filepath.Join(actionDir, actionPath), step.getActionModel()))()
|
||||
}
|
||||
|
||||
if err := removeGitIgnore(ctx, actionDir); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return rc.JobContainer.CopyDir(containerActionDirCopy, actionDir+"/", rc.Config.UseGitIgnore, true)(ctx)
|
||||
}
|
||||
|
||||
@@ -241,23 +241,6 @@ func nodeActionCommand(script string) []string {
|
||||
return []string{"node", "--preserve-symlinks-main", script}
|
||||
}
|
||||
|
||||
// https://github.com/nektos/act/issues/228#issuecomment-629709055
|
||||
// files in .gitignore are not copied in a Docker container
|
||||
// this causes issues with actions that ignore other important resources
|
||||
// such as `node_modules` for example
|
||||
func removeGitIgnore(ctx context.Context, directory string) error {
|
||||
gitIgnorePath := path.Join(directory, ".gitignore")
|
||||
if _, err := os.Stat(gitIgnorePath); err == nil {
|
||||
// .gitignore exists
|
||||
common.Logger(ctx).Debugf("Removing %s before docker cp", gitIgnorePath)
|
||||
err := os.Remove(gitIgnorePath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// dockerActionImageTag derives the local docker image tag used when an action
|
||||
// is built from a Dockerfile.
|
||||
//
|
||||
@@ -366,7 +349,11 @@ func execAsDocker(ctx context.Context, step actionStep, actionName, actionDir, b
|
||||
// Held across the whole build: the daemon drains contextDir lazily.
|
||||
inner := prepImage
|
||||
prepImage = func(ctx context.Context) error {
|
||||
defer git.AcquireCloneLock(actionDir)()
|
||||
unlock, err := git.AcquireCloneLock(ctx, actionDir)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer unlock()
|
||||
return inner(ctx)
|
||||
}
|
||||
}
|
||||
@@ -681,17 +668,17 @@ func shouldRunPostStep(step actionStep) common.Conditional {
|
||||
stepResult := stepResults[step.getStepModel().ID]
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
|
||||
@@ -132,7 +132,7 @@ runs:
|
||||
return nil
|
||||
}
|
||||
|
||||
action, err := readActionImpl(context.Background(), tt.step, "actionDir", "actionPath", readFile, writeFile)
|
||||
action, err := readActionImpl(t.Context(), tt.step, "actionDir", "actionPath", readFile, writeFile)
|
||||
|
||||
assert.NoError(t, err) //nolint:testifylint // pre-existing issue from nektos/act
|
||||
assert.Equal(t, tt.expected, action)
|
||||
@@ -143,6 +143,7 @@ runs:
|
||||
}
|
||||
|
||||
func TestActionRunner(t *testing.T) {
|
||||
t.Chdir(t.TempDir())
|
||||
table := []struct {
|
||||
name string
|
||||
step actionStep
|
||||
@@ -224,7 +225,7 @@ func TestActionRunner(t *testing.T) {
|
||||
|
||||
for _, tt := range table {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
ctx := t.Context()
|
||||
|
||||
cm := &containerMock{}
|
||||
cm.On("CopyDir", "/var/run/act/actions/dir/", "dir/", false, true).Return(func(ctx context.Context) error { return nil })
|
||||
@@ -289,7 +290,7 @@ func TestNewStepContainerDoesNotUseDockerSecrets(t *testing.T) {
|
||||
ContainerNewContainer = origContainerNewContainer
|
||||
}()
|
||||
|
||||
ctx := context.Background()
|
||||
ctx := t.Context()
|
||||
rc := &RunContext{
|
||||
Name: "job",
|
||||
Config: &Config{
|
||||
@@ -326,7 +327,7 @@ func TestNewStepContainerDoesNotUseDockerSecrets(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestMaybeCopyToActionDirHoldsCloneLock(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
ctx := t.Context()
|
||||
|
||||
actionDir := t.TempDir()
|
||||
|
||||
@@ -366,7 +367,10 @@ func TestMaybeCopyToActionDirHoldsCloneLock(t *testing.T) {
|
||||
|
||||
peerAcquired := make(chan struct{})
|
||||
go func() {
|
||||
unlock := git.AcquireCloneLock(actionDir)
|
||||
unlock, err := git.AcquireCloneLock(t.Context(), actionDir)
|
||||
if !assert.NoError(t, err) {
|
||||
return
|
||||
}
|
||||
close(peerAcquired)
|
||||
unlock()
|
||||
}()
|
||||
@@ -400,7 +404,9 @@ func TestMaybeCopyToActionDirHoldsCloneLock(t *testing.T) {
|
||||
func TestExecAsDockerHoldsCloneLockForRemoteUncached(t *testing.T) {
|
||||
actionDir := t.TempDir()
|
||||
|
||||
unlockOnce := sync.OnceFunc(git.AcquireCloneLock(actionDir))
|
||||
unlock, err := git.AcquireCloneLock(t.Context(), actionDir)
|
||||
require.NoError(t, err)
|
||||
unlockOnce := sync.OnceFunc(unlock)
|
||||
defer unlockOnce()
|
||||
|
||||
innerEntered := make(chan struct{})
|
||||
@@ -441,7 +447,7 @@ func TestExecAsDockerHoldsCloneLockForRemoteUncached(t *testing.T) {
|
||||
env: map[string]string{},
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
ctx, cancel := context.WithCancel(t.Context())
|
||||
defer cancel()
|
||||
|
||||
done := make(chan error, 1)
|
||||
@@ -613,7 +619,7 @@ func TestExecAsDockerStageEntrypoint(t *testing.T) {
|
||||
cm.On("Start", true).Return(func(context.Context) error { return nil })
|
||||
cm.On("Close").Return(func(context.Context) error { return nil })
|
||||
|
||||
require.NoError(t, execAsDocker(context.Background(), step, "action", t.TempDir(), t.TempDir(), false, tc.stage))
|
||||
require.NoError(t, execAsDocker(t.Context(), step, "action", t.TempDir(), t.TempDir(), false, tc.stage))
|
||||
require.NotNil(t, input)
|
||||
assert.Equal(t, tc.wantCmd, input.Cmd)
|
||||
assert.Equal(t, tc.wantEntrypoint, input.Entrypoint)
|
||||
@@ -628,7 +634,7 @@ func TestDockerActionHasPreAndPostStep(t *testing.T) {
|
||||
newStep := func(runs model.ActionRuns) actionStep {
|
||||
return &stepActionRemote{action: &model.Action{Runs: runs}}
|
||||
}
|
||||
ctx := context.Background()
|
||||
ctx := t.Context()
|
||||
|
||||
assert.False(t, hasPreStep(newStep(model.ActionRuns{Using: "docker", Image: "Dockerfile"}))(ctx))
|
||||
assert.False(t, hasPostStep(newStep(model.ActionRuns{Using: "docker", Image: "Dockerfile"}))(ctx))
|
||||
|
||||
@@ -62,7 +62,7 @@ func TestCancelledJobStatusEnablesAlwaysAndCancelledSteps(t *testing.T) {
|
||||
enabled, err := interp.Evaluate("always()", exprparser.DefaultStatusCheckSuccess)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, true, enabled, "`if: always()` step must run on a cancelled job")
|
||||
setJobResult(context.Background(), rc, rc, true)
|
||||
setJobResult(t.Context(), rc, rc, true)
|
||||
assert.Equal(t, "cancelled", rc.Run.Job().Result)
|
||||
}
|
||||
|
||||
@@ -76,7 +76,7 @@ func TestMainStepsExecutorRunsAlwaysStepsAfterCancel(t *testing.T) {
|
||||
"job1": createJob(t, `runs-on: ubuntu-latest`, ""),
|
||||
})
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
ctx, cancel := context.WithCancel(t.Context())
|
||||
defer cancel()
|
||||
|
||||
var ran []string
|
||||
@@ -110,7 +110,7 @@ func TestMainStepsExecutorMarksFailedOnTimeoutBetweenSteps(t *testing.T) {
|
||||
"job1": createJob(t, `runs-on: ubuntu-latest`, ""),
|
||||
})
|
||||
|
||||
ctx := newControllableDeadlineContext(context.Background())
|
||||
ctx := newControllableDeadlineContext(t.Context())
|
||||
|
||||
var ran []string
|
||||
var laterStepCtxErr error
|
||||
@@ -149,7 +149,7 @@ func TestStepsExecutorRunsMainStepsAfterPreCancel(t *testing.T) {
|
||||
"job1": createJob(t, `runs-on: ubuntu-latest`, ""),
|
||||
})
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
ctx, cancel := context.WithCancel(t.Context())
|
||||
defer cancel()
|
||||
|
||||
var ran []string
|
||||
@@ -200,7 +200,7 @@ func TestStepsExecutorRunsMainStepsAfterPreFailure(t *testing.T) {
|
||||
},
|
||||
}
|
||||
|
||||
err := newStepsExecutor(rc, preSteps, steps)(context.Background())
|
||||
err := newStepsExecutor(rc, preSteps, steps)(t.Context())
|
||||
|
||||
require.ErrorIs(t, err, assert.AnError, "the pre-step error is propagated")
|
||||
assert.Equal(t, []string{"pre1", "always-step"}, ran, "the main always() step runs after a pre-step failure")
|
||||
@@ -215,7 +215,7 @@ func TestPreStepFailureAffectsMainStepIfStatus(t *testing.T) {
|
||||
rc := createIfTestRunContext(map[string]*model.Job{
|
||||
"job1": createJob(t, `runs-on: ubuntu-latest`, ""),
|
||||
})
|
||||
ctx := common.WithJobErrorContainer(context.Background())
|
||||
ctx := common.WithJobErrorContainer(t.Context())
|
||||
|
||||
reportStepError(ctx, rc, assert.AnError)
|
||||
|
||||
@@ -251,7 +251,7 @@ func TestPreStepFailureAffectsMainStepIfStatus(t *testing.T) {
|
||||
// from cancellation (so the steps run) yet still carry a usable error container.
|
||||
func TestPostStepsContextCancelledIsUsableForFailingStep(t *testing.T) {
|
||||
jobSpan := trace.NewSpanContext(trace.SpanContextConfig{TraceID: trace.TraceID{1}, SpanID: trace.SpanID{1}})
|
||||
cancelled, cancel := context.WithCancel(trace.ContextWithSpanContext(common.WithJobErrorContainer(context.Background()), jobSpan))
|
||||
cancelled, cancel := context.WithCancel(trace.ContextWithSpanContext(common.WithJobErrorContainer(t.Context()), jobSpan))
|
||||
cancel()
|
||||
require.ErrorIs(t, cancelled.Err(), context.Canceled)
|
||||
|
||||
@@ -273,7 +273,7 @@ func TestPostStepsContextCancelledIsUsableForFailingStep(t *testing.T) {
|
||||
// job-error container (via context.WithoutCancel), so the timeout failure and any post-step error
|
||||
// survive into the post phase and the job is still reported as failed.
|
||||
func TestPostStepsContextDeadlinePreservesJobError(t *testing.T) {
|
||||
base := common.WithJobErrorContainer(context.Background())
|
||||
base := common.WithJobErrorContainer(t.Context())
|
||||
common.SetJobError(base, assert.AnError)
|
||||
expired, cancel := context.WithDeadline(base, time.Now().Add(-time.Hour))
|
||||
defer cancel()
|
||||
@@ -292,14 +292,14 @@ func TestReportStepErrorTreatsCancelAsInterruption(t *testing.T) {
|
||||
rc := &RunContext{}
|
||||
|
||||
// stray read cancellation while the job context is live: ignored, not a failure
|
||||
live := common.WithJobErrorContainer(context.Background())
|
||||
live := common.WithJobErrorContainer(t.Context())
|
||||
reportStepError(live, rc, context.Canceled)
|
||||
require.NoError(t, common.JobError(live))
|
||||
assert.False(t, rc.jobFailed)
|
||||
assert.False(t, rc.jobCancelled)
|
||||
|
||||
// genuine job cancellation: recorded as cancelled, still not a failure
|
||||
cancelled, cancel := context.WithCancel(common.WithJobErrorContainer(context.Background()))
|
||||
cancelled, cancel := context.WithCancel(common.WithJobErrorContainer(t.Context()))
|
||||
cancel()
|
||||
reportStepError(cancelled, rc, context.Canceled)
|
||||
require.NoError(t, common.JobError(cancelled))
|
||||
@@ -307,7 +307,7 @@ func TestReportStepErrorTreatsCancelAsInterruption(t *testing.T) {
|
||||
assert.True(t, rc.jobCancelled)
|
||||
|
||||
// a real error still fails the job
|
||||
failed := common.WithJobErrorContainer(context.Background())
|
||||
failed := common.WithJobErrorContainer(t.Context())
|
||||
reportStepError(failed, rc, assert.AnError)
|
||||
require.ErrorIs(t, common.JobError(failed), assert.AnError)
|
||||
assert.True(t, rc.jobFailed)
|
||||
|
||||
@@ -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.
|
||||
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. "+
|
||||
"Write to $GITHUB_ENV or $GITHUB_PATH instead, or set ACTIONS_ALLOW_UNSECURE_COMMANDS to allow it",
|
||||
command)))
|
||||
@@ -159,7 +159,7 @@ func (rc *RunContext) setEnv(ctx context.Context, kvPairs map[string]string, arg
|
||||
if fromCommand {
|
||||
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
|
||||
}
|
||||
common.Logger(ctx).Infof("::set-env:: %s=%s", name, arg)
|
||||
|
||||
+18
-19
@@ -6,15 +6,14 @@ package runner
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"io"
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"gitea.com/gitea/runner/act/common"
|
||||
"gitea.com/gitea/runner/act/common/logging/logtest"
|
||||
|
||||
"gitea.dev/actionslib/pkg/model"
|
||||
"github.com/sirupsen/logrus/hooks/test"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
@@ -26,8 +25,8 @@ func unsecureRC() *RunContext {
|
||||
|
||||
func TestSetEnv(t *testing.T) {
|
||||
a := assert.New(t)
|
||||
logger, hook := test.NewNullLogger()
|
||||
ctx := common.WithLogger(context.Background(), logger)
|
||||
logger, hook := logtest.NewLogger()
|
||||
ctx := common.WithLogger(t.Context(), logger)
|
||||
rc := unsecureRC()
|
||||
handler := rc.commandHandler(ctx)
|
||||
|
||||
@@ -37,7 +36,7 @@ func TestSetEnv(t *testing.T) {
|
||||
rc.setEnvFile(ctx, map[string]string{"name": "node_options"}, "--require env.js")
|
||||
a.NotContains(rc.Env, "NODE_OPTIONS")
|
||||
a.NotContains(rc.Env, "node_options")
|
||||
entries := hook.AllEntries()
|
||||
entries := hook.AllRecords()
|
||||
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 store NODE_OPTIONS output parameter using '$GITHUB_ENV' command.", entries[2].Message)
|
||||
@@ -45,7 +44,7 @@ func TestSetEnv(t *testing.T) {
|
||||
|
||||
func TestStopCommandsKeepsSuppressedLinesInLog(t *testing.T) {
|
||||
a := assert.New(t)
|
||||
ctx := context.Background()
|
||||
ctx := t.Context()
|
||||
rc := unsecureRC()
|
||||
handler := rc.commandHandler(ctx)
|
||||
|
||||
@@ -68,7 +67,7 @@ func TestStopCommandsKeepsSuppressedLinesInLog(t *testing.T) {
|
||||
|
||||
func TestSetOutput(t *testing.T) {
|
||||
a := assert.New(t)
|
||||
ctx := context.Background()
|
||||
ctx := t.Context()
|
||||
rc := new(RunContext)
|
||||
rc.StepResults = make(map[string]*model.StepResult)
|
||||
handler := rc.commandHandler(ctx)
|
||||
@@ -108,7 +107,7 @@ func TestSetOutput(t *testing.T) {
|
||||
|
||||
func TestAddpath(t *testing.T) {
|
||||
a := assert.New(t)
|
||||
ctx := context.Background()
|
||||
ctx := t.Context()
|
||||
rc := unsecureRC()
|
||||
handler := rc.commandHandler(ctx)
|
||||
|
||||
@@ -120,10 +119,10 @@ func TestAddpath(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestStopCommands(t *testing.T) {
|
||||
logger, hook := test.NewNullLogger()
|
||||
logger, hook := logtest.NewLogger()
|
||||
|
||||
a := assert.New(t)
|
||||
ctx := common.WithLogger(context.Background(), logger)
|
||||
ctx := common.WithLogger(t.Context(), logger)
|
||||
rc := unsecureRC()
|
||||
handler := rc.commandHandler(ctx)
|
||||
|
||||
@@ -137,7 +136,7 @@ func TestStopCommands(t *testing.T) {
|
||||
a.Equal("abcd", rc.Env["x"])
|
||||
|
||||
messages := make([]string, 0)
|
||||
for _, entry := range hook.AllEntries() {
|
||||
for _, entry := range hook.AllRecords() {
|
||||
messages = append(messages, entry.Message)
|
||||
}
|
||||
|
||||
@@ -149,7 +148,7 @@ func TestStopCommands(t *testing.T) {
|
||||
func TestStopCommandsResumesOnCommandNamedToken(t *testing.T) {
|
||||
a := assert.New(t)
|
||||
rc := unsecureRC()
|
||||
handler := rc.commandHandler(context.Background())
|
||||
handler := rc.commandHandler(t.Context())
|
||||
|
||||
handler("::stop-commands::add-mask\n")
|
||||
handler("::set-env name=x::suppressed\n")
|
||||
@@ -162,7 +161,7 @@ func TestStopCommandsResumesOnCommandNamedToken(t *testing.T) {
|
||||
|
||||
func TestAddpathADO(t *testing.T) {
|
||||
a := assert.New(t)
|
||||
ctx := context.Background()
|
||||
ctx := t.Context()
|
||||
rc := unsecureRC()
|
||||
handler := rc.commandHandler(ctx)
|
||||
|
||||
@@ -174,17 +173,17 @@ func TestAddpathADO(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestAddmask(t *testing.T) {
|
||||
logger, hook := test.NewNullLogger()
|
||||
logger, hook := logtest.NewLogger()
|
||||
|
||||
a := assert.New(t)
|
||||
ctx := context.Background()
|
||||
ctx := t.Context()
|
||||
loggerCtx := common.WithLogger(ctx, logger)
|
||||
|
||||
rc := new(RunContext)
|
||||
handler := rc.commandHandler(loggerCtx)
|
||||
handler("::ADD-MASK::my::secret")
|
||||
|
||||
a.Equal("***", hook.LastEntry().Message)
|
||||
a.Equal("***", hook.LastRecord().Message)
|
||||
a.Equal([]string{"my::secret"}, rc.Masks)
|
||||
}
|
||||
|
||||
@@ -231,7 +230,7 @@ func TestAddmaskUsemask(t *testing.T) {
|
||||
}
|
||||
|
||||
re := captureOutput(t, func() {
|
||||
ctx := context.Background()
|
||||
ctx := t.Context()
|
||||
ctx = WithJobLogger(ctx, "0", "testjob", config, &rc.Masks, map[string]any{})
|
||||
|
||||
handler := rc.commandHandler(ctx)
|
||||
@@ -248,7 +247,7 @@ func TestSaveState(t *testing.T) {
|
||||
StepResults: map[string]*model.StepResult{},
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
ctx := t.Context()
|
||||
|
||||
handler := rc.commandHandler(ctx)
|
||||
handler("::save-state name=state-name::state-value\n")
|
||||
@@ -281,7 +280,7 @@ func TestUnsecureCommands(t *testing.T) {
|
||||
a := assert.New(t)
|
||||
rc := &RunContext{Env: tt.jobEnv}
|
||||
rc.setCurrentStepEnv(tt.stepEnv)
|
||||
handler := rc.commandHandler(context.Background())
|
||||
handler := rc.commandHandler(t.Context())
|
||||
|
||||
handler("::set-env name=x::valz\n")
|
||||
handler("::add-path::/opt/bin\n")
|
||||
|
||||
@@ -15,12 +15,12 @@ import (
|
||||
"time"
|
||||
|
||||
"gitea.com/gitea/runner/act/common"
|
||||
"gitea.com/gitea/runner/act/common/logging/logtest"
|
||||
"gitea.com/gitea/runner/act/container"
|
||||
|
||||
"gitea.dev/actionslib/pkg/model"
|
||||
cerrdefs "github.com/containerd/errdefs"
|
||||
"github.com/moby/moby/client"
|
||||
"github.com/sirupsen/logrus/hooks/test"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
@@ -83,10 +83,10 @@ func TestDockerProxyMountedJob(t *testing.T) {
|
||||
}
|
||||
})
|
||||
|
||||
hook := &test.Hook{}
|
||||
require.NoError(t, runner.NewPlanExecutor(plan)(common.WithLoggerHook(ctx, hook)))
|
||||
hook := logtest.New()
|
||||
require.NoError(t, runner.NewPlanExecutor(plan)(common.WithLoggerHandler(ctx, hook)))
|
||||
var messages []string
|
||||
for _, entry := range hook.AllEntries() {
|
||||
for _, entry := range hook.AllRecords() {
|
||||
messages = append(messages, strings.TrimSpace(entry.Message))
|
||||
}
|
||||
require.Contains(t, messages, "docker proxy post verified")
|
||||
|
||||
@@ -5,7 +5,6 @@
|
||||
package runner
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
@@ -82,7 +81,7 @@ func createRunContext(t *testing.T) *RunContext {
|
||||
|
||||
func TestEvaluateRunContext(t *testing.T) {
|
||||
rc := createRunContext(t)
|
||||
ee := rc.NewExpressionEvaluator(context.Background())
|
||||
ee := rc.NewExpressionEvaluator(t.Context())
|
||||
|
||||
tables := []struct {
|
||||
in string
|
||||
@@ -142,7 +141,7 @@ func TestEvaluateRunContext(t *testing.T) {
|
||||
for _, table := range tables {
|
||||
t.Run(table.in, func(t *testing.T) {
|
||||
assertObject := assert.New(t)
|
||||
out, err := ee.evaluate(context.Background(), table.in, exprparser.DefaultStatusCheckNone)
|
||||
out, err := ee.evaluate(t.Context(), table.in, exprparser.DefaultStatusCheckNone)
|
||||
if table.errMesg == "" {
|
||||
assertObject.NoError(err, table.in) //nolint:testifylint // pre-existing issue from nektos/act
|
||||
assertObject.Equal(table.out, out, table.in)
|
||||
@@ -162,7 +161,7 @@ func TestEvaluateStep(t *testing.T) {
|
||||
env: map[string]string{"INPUT_FORGED": "leaked"},
|
||||
}
|
||||
|
||||
ee := rc.NewStepExpressionEvaluator(context.Background(), step)
|
||||
ee := rc.NewStepExpressionEvaluator(t.Context(), step)
|
||||
|
||||
tables := []struct {
|
||||
in string
|
||||
@@ -184,7 +183,7 @@ func TestEvaluateStep(t *testing.T) {
|
||||
for _, table := range tables {
|
||||
t.Run(table.in, func(t *testing.T) {
|
||||
assertObject := assert.New(t)
|
||||
out, err := ee.evaluate(context.Background(), table.in, exprparser.DefaultStatusCheckNone)
|
||||
out, err := ee.evaluate(t.Context(), table.in, exprparser.DefaultStatusCheckNone)
|
||||
if table.errMesg == "" {
|
||||
assertObject.NoError(err, table.in) //nolint:testifylint // pre-existing issue from nektos/act
|
||||
assertObject.Equal(table.out, out, table.in)
|
||||
@@ -224,7 +223,7 @@ func TestInterpolate(t *testing.T) {
|
||||
},
|
||||
},
|
||||
}
|
||||
ee := rc.NewExpressionEvaluator(context.Background())
|
||||
ee := rc.NewExpressionEvaluator(t.Context())
|
||||
tables := []struct {
|
||||
in string
|
||||
out string
|
||||
@@ -270,14 +269,14 @@ func TestInterpolate(t *testing.T) {
|
||||
|
||||
for _, table := range tables {
|
||||
t.Run("interpolate", func(t *testing.T) {
|
||||
out, err := ee.Interpolate(context.Background(), table.in)
|
||||
out, err := ee.Interpolate(t.Context(), table.in)
|
||||
require.NoError(t, err, table.in)
|
||||
assert.Equal(t, table.out, out, table.in)
|
||||
})
|
||||
}
|
||||
|
||||
for _, in := range []string{"${{ 1) && (2 }}", "run ${{ 1) && (2 }} now", "${{ 1"} {
|
||||
_, err := ee.Interpolate(context.Background(), in)
|
||||
_, err := ee.Interpolate(t.Context(), in)
|
||||
assert.Error(t, err, in)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,7 +4,6 @@
|
||||
package runner
|
||||
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
"os/exec"
|
||||
"runtime"
|
||||
@@ -30,7 +29,7 @@ func requireLinuxDocker(t *testing.T) {
|
||||
// so the daemon has to be pinged explicitly here to decide whether to skip.
|
||||
func requireDocker(t *testing.T) {
|
||||
t.Helper()
|
||||
ctx := context.Background()
|
||||
ctx := t.Context()
|
||||
cli, err := container.GetDockerClient(ctx)
|
||||
if err != nil {
|
||||
t.Skipf("skipping: docker client unavailable: %v", err)
|
||||
|
||||
+37
-20
@@ -29,7 +29,10 @@ import (
|
||||
"gitea.dev/actionslib/pkg/model"
|
||||
)
|
||||
|
||||
const maxJobSummaryBytes = 1024 * 1024
|
||||
const (
|
||||
jobCleanupTimeout = time.Minute
|
||||
maxJobSummaryBytes = 1024 * 1024
|
||||
)
|
||||
|
||||
// jobSummaryTruncationMarker is appended to a summary that exceeded the size limit
|
||||
// so the rendered output makes the truncation visible instead of silently cutting off.
|
||||
@@ -89,14 +92,14 @@ func printPrepareActions(rc *RunContext, preparers []actionPreparer) common.Exec
|
||||
return nil
|
||||
}
|
||||
|
||||
rawLogger := common.Logger(ctx).WithField(rawOutputField, true)
|
||||
rawLogger := common.Logger(ctx).With(rawOutputField, true)
|
||||
rawLogger.Infof("Prepare all required actions")
|
||||
|
||||
for _, preparer := range preparers {
|
||||
if err := preparer.prepareActionExecutor()(ctx); err != nil {
|
||||
// No step has run yet, so the failure belongs to the job.
|
||||
reportStepError(ctx, rc, err)
|
||||
return err
|
||||
return setupError{err}
|
||||
}
|
||||
reference, sha, ok := preparer.actionDownloadInfo()
|
||||
if !ok {
|
||||
@@ -112,6 +115,11 @@ func printPrepareActions(rc *RunContext, preparers []actionPreparer) common.Exec
|
||||
}
|
||||
}
|
||||
|
||||
// setupError is a failure of the job setup, such as an action that cannot be downloaded.
|
||||
type setupError struct{ error }
|
||||
|
||||
func (e setupError) Unwrap() error { return e.error }
|
||||
|
||||
// printCompleteJobName closes the setup section the way actions/runner ends its "Set up job" step.
|
||||
func printCompleteJobName(rc *RunContext) common.Executor {
|
||||
return func(ctx context.Context) error {
|
||||
@@ -123,7 +131,7 @@ func printCompleteJobName(rc *RunContext) common.Executor {
|
||||
if name == "" && rc.Run != nil {
|
||||
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
|
||||
}
|
||||
}
|
||||
@@ -136,20 +144,18 @@ func newJobExecutor(info jobInfo, sf stepFactory, rc *RunContext) common.Executo
|
||||
preparers := make([]actionPreparer, 0)
|
||||
var postExecutor common.Executor
|
||||
|
||||
steps = append(steps, func(ctx context.Context) error {
|
||||
logger := common.Logger(ctx)
|
||||
if len(info.matrix()) > 0 {
|
||||
logger.Infof("Matrix: %v", info.matrix())
|
||||
}
|
||||
return nil
|
||||
})
|
||||
|
||||
infoSteps := info.steps()
|
||||
|
||||
if len(infoSteps) == 0 {
|
||||
return common.NewDebugExecutor("No steps found")
|
||||
}
|
||||
|
||||
preSteps = append(preSteps, func(ctx context.Context) error {
|
||||
if len(info.matrix()) > 0 {
|
||||
common.Logger(ctx).Infof("Matrix: %v", info.matrix())
|
||||
}
|
||||
return nil
|
||||
})
|
||||
preSteps = append(preSteps, func(ctx context.Context) error {
|
||||
// Have to be skipped for some Tests
|
||||
if rc.Run == nil {
|
||||
@@ -228,14 +234,15 @@ func newJobExecutor(info jobInfo, sf stepFactory, rc *RunContext) common.Executo
|
||||
|
||||
// The setup section of the job log. The started hook goes first, so what it sets up is
|
||||
// in place for the first action download and the first step.
|
||||
// The pre steps come after it, as steps of their own.
|
||||
preSteps = append(preSteps, rc.runJobStartedHook)
|
||||
preSteps = append(preSteps, printPrepareActions(rc, preparers))
|
||||
preSteps = append(preSteps, stepPreSteps...)
|
||||
preSteps = append(preSteps, printCompleteJobName(rc))
|
||||
preSteps = append(preSteps, stepPreSteps...)
|
||||
|
||||
// Ahead of the teardown below, while the job environment is still up.
|
||||
postExecutor = postExecutor.Finally(rc.runJobCompletedHook)
|
||||
postExecutor = postExecutor.Finally(func(ctx context.Context) error {
|
||||
postExecutor = postExecutor.Finally(useJobStageLogger(StageComplete, rc.runJobCompletedHook))
|
||||
postExecutor = postExecutor.Finally(useJobStageLogger(StageComplete, func(ctx context.Context) error {
|
||||
// swallowed: a bad output fails this job, it must not abandon the rest of the plan
|
||||
if err := info.interpolateOutputs()(ctx); err != nil {
|
||||
reportStepError(ctx, rc, err)
|
||||
@@ -243,13 +250,13 @@ func newJobExecutor(info jobInfo, sf stepFactory, rc *RunContext) common.Executo
|
||||
reportStepError(ctx, rc, err)
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}))
|
||||
|
||||
postExecutor = postExecutor.Finally(func(ctx context.Context) error {
|
||||
postExecutor = postExecutor.Finally(useJobStageLogger(StageComplete, func(ctx context.Context) error {
|
||||
jobError := common.JobError(ctx)
|
||||
var err error
|
||||
// always allow 1 min for stopping and removing the runner, even if we were cancelled
|
||||
ctx, cancel := context.WithTimeout(common.WithLogger(context.Background(), common.Logger(ctx)), time.Minute)
|
||||
ctx, cancel := context.WithTimeout(common.WithLogger(context.Background(), common.Logger(ctx)), jobCleanupTimeout)
|
||||
defer cancel()
|
||||
|
||||
logger := common.Logger(ctx)
|
||||
@@ -262,7 +269,7 @@ func newJobExecutor(info jobInfo, sf stepFactory, rc *RunContext) common.Executo
|
||||
rc.removeDockerActionImages(ctx)
|
||||
|
||||
return err
|
||||
})
|
||||
}))
|
||||
|
||||
return common.Executor(func(ctx context.Context) error {
|
||||
if err := info.startContainer()(ctx); err != nil {
|
||||
@@ -316,6 +323,9 @@ func newStepsExecutor(rc *RunContext, preSteps, steps []common.Executor) common.
|
||||
mainExecutor := newMainStepsExecutor(rc, steps)
|
||||
return func(ctx context.Context) error {
|
||||
preErr := preExecutor(ctx)
|
||||
if errors.As(preErr, new(setupError)) {
|
||||
return preErr // as on GitHub, no step runs after the job setup failed
|
||||
}
|
||||
mainErr := mainExecutor(ctx)
|
||||
if preErr != nil {
|
||||
return preErr
|
||||
@@ -409,7 +419,7 @@ func setJobResult(ctx context.Context, info jobInfo, rc *RunContext, success boo
|
||||
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.
|
||||
@@ -681,6 +691,13 @@ func archiveEntryMatchesPath(entryName, requestedPath string) bool {
|
||||
return entryName == requestedPath || entryName == path.Base(requestedPath)
|
||||
}
|
||||
|
||||
// useJobStageLogger tags the log entries of a job level executor with the stage they belong to.
|
||||
func useJobStageLogger(stage string, executor common.Executor) common.Executor {
|
||||
return func(ctx context.Context) error {
|
||||
return executor(common.WithLogger(ctx, common.Logger(ctx).With("stage", stage)))
|
||||
}
|
||||
}
|
||||
|
||||
func useStepLogger(rc *RunContext, stepModel *model.Step, stage stepStage, executor common.Executor) common.Executor {
|
||||
return func(ctx context.Context) error {
|
||||
ctx = withStepLogger(ctx, stepModel.Number, stepModel.ID, rc.ExprEval.InterpolateName(ctx, stepModel.String()), stage.String())
|
||||
|
||||
@@ -21,11 +21,11 @@ import (
|
||||
"time"
|
||||
|
||||
"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.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/mock"
|
||||
"github.com/stretchr/testify/require"
|
||||
@@ -44,7 +44,7 @@ func TestJobExecutor(t *testing.T) {
|
||||
{workdir, "job-nil-step", "push", "invalid Step 0: missing run or uses key", platforms, secrets},
|
||||
}
|
||||
// These tests are sufficient to only check syntax.
|
||||
ctx := common.WithDryrun(context.Background(), true)
|
||||
ctx := common.WithDryrun(t.Context(), true)
|
||||
for _, table := range tables {
|
||||
t.Run(table.workflowPath, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
@@ -136,11 +136,8 @@ func (apm *actionPreparerMock) actionDownloadInfo() (string, string, bool) {
|
||||
|
||||
func TestPrintPrepareActionsGolden(t *testing.T) {
|
||||
buf := &bytes.Buffer{}
|
||||
logger := log.New()
|
||||
logger.SetOutput(buf)
|
||||
logger.SetLevel(log.InfoLevel)
|
||||
logger.SetFormatter(&jobLogFormatter{color: cyan})
|
||||
ctx := common.WithLogger(context.Background(), logger.WithFields(log.Fields{"job": "j1"}))
|
||||
logger := logging.New(logging.NewLevelHandler(newJobLogHandler(buf, cyan), logging.LevelInfo))
|
||||
ctx := common.WithLogger(t.Context(), logger.With("job", "j1"))
|
||||
|
||||
preparers := []actionPreparer{
|
||||
&actionPreparerMock{reference: "actions/checkout@v7", sha: "9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0", ok: true},
|
||||
@@ -162,10 +159,8 @@ func TestPrintPrepareActionsGolden(t *testing.T) {
|
||||
|
||||
func TestPrintPrepareActionsSkipsWithoutActions(t *testing.T) {
|
||||
buf := &bytes.Buffer{}
|
||||
logger := log.New()
|
||||
logger.SetOutput(buf)
|
||||
logger.SetFormatter(&jobLogFormatter{color: cyan})
|
||||
ctx := common.WithLogger(context.Background(), logger.WithFields(log.Fields{"job": "j1"}))
|
||||
logger := logging.New(logging.NewLevelHandler(newJobLogHandler(buf, cyan), logging.LevelInfo))
|
||||
ctx := common.WithLogger(t.Context(), logger.With("job", "j1"))
|
||||
|
||||
require.NoError(t, printPrepareActions(&RunContext{}, nil)(ctx))
|
||||
|
||||
@@ -173,8 +168,8 @@ func TestPrintPrepareActionsSkipsWithoutActions(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestPrintPrepareActionsFailsJobOnDownloadError(t *testing.T) {
|
||||
logger, _ := logrustest.NewNullLogger()
|
||||
ctx := common.WithJobErrorContainer(common.WithLogger(context.Background(), logger.WithField("job", "j1")))
|
||||
logger := logging.New(logtest.New())
|
||||
ctx := common.WithJobErrorContainer(common.WithLogger(t.Context(), logger.With("job", "j1")))
|
||||
|
||||
downloadErr := errors.New("failed to fetch \"actions/checkout\"")
|
||||
rc := &RunContext{}
|
||||
@@ -192,6 +187,27 @@ func TestPrintPrepareActionsFailsJobOnDownloadError(t *testing.T) {
|
||||
assert.Zero(t, remaining.prepared)
|
||||
}
|
||||
|
||||
func TestNewStepsExecutorSkipsStepsAfterSetupFailure(t *testing.T) {
|
||||
for name, tt := range map[string]struct {
|
||||
preErr error
|
||||
mainRan bool
|
||||
}{
|
||||
"setup failure": {preErr: setupError{assert.AnError}},
|
||||
"pre step failure": {preErr: assert.AnError, mainRan: true}, // always() steps still run, as on GitHub
|
||||
} {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
ctx := common.WithJobErrorContainer(common.WithLogger(context.Background(), logging.New(logtest.New())))
|
||||
mainRan := false
|
||||
err := newStepsExecutor(&RunContext{},
|
||||
[]common.Executor{func(context.Context) error { return tt.preErr }},
|
||||
[]common.Executor{func(context.Context) error { mainRan = true; return nil }},
|
||||
)(ctx)
|
||||
require.ErrorIs(t, err, assert.AnError)
|
||||
assert.Equal(t, tt.mainRan, mainRan)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestPrintCompleteJobName(t *testing.T) {
|
||||
for name, tt := range map[string]struct {
|
||||
rc *RunContext
|
||||
@@ -203,10 +219,8 @@ func TestPrintCompleteJobName(t *testing.T) {
|
||||
} {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
buf := &bytes.Buffer{}
|
||||
logger := log.New()
|
||||
logger.SetOutput(buf)
|
||||
logger.SetFormatter(&jobLogFormatter{color: cyan})
|
||||
ctx := common.WithLogger(context.Background(), logger.WithFields(log.Fields{"job": "j1"}))
|
||||
logger := logging.New(logging.NewLevelHandler(newJobLogHandler(buf, cyan), logging.LevelInfo))
|
||||
ctx := common.WithLogger(t.Context(), logger.With("job", "j1"))
|
||||
|
||||
require.NoError(t, printCompleteJobName(tt.rc)(ctx))
|
||||
|
||||
@@ -225,7 +239,7 @@ type actionStepMock struct {
|
||||
// every action is downloaded before any step runs, and the job name closes the section. A pre
|
||||
// step that downloaded its own action would leave the log interleaved with the downloads.
|
||||
func TestNewJobExecutorDownloadsAllActionsBeforeTheFirstStep(t *testing.T) {
|
||||
ctx := common.WithJobErrorContainer(context.Background())
|
||||
ctx := common.WithJobErrorContainer(t.Context())
|
||||
jim := &jobInfoMock{}
|
||||
sfm := &stepFactoryMock{}
|
||||
rc := &RunContext{
|
||||
@@ -256,8 +270,9 @@ func TestNewJobExecutorDownloadsAllActionsBeforeTheFirstStep(t *testing.T) {
|
||||
apm := &actionPreparerMock{reference: "actions/checkout@v" + stepModel.ID, ok: true}
|
||||
sfm.On("newStep", stepModel, rc).Return(&actionStepMock{stepMock: sm, actionPreparerMock: apm}, nil)
|
||||
|
||||
sm.On("pre").Return(func(context.Context) error {
|
||||
sm.On("pre").Return(func(ctx context.Context) error {
|
||||
executorOrder = append(executorOrder, "pre"+stepModel.ID)
|
||||
common.Logger(ctx).Infof("Run Pre %s", stepModel.ID)
|
||||
return nil
|
||||
})
|
||||
sm.On("main").Return(func(context.Context) error {
|
||||
@@ -269,24 +284,31 @@ func TestNewJobExecutorDownloadsAllActionsBeforeTheFirstStep(t *testing.T) {
|
||||
defer sm.AssertExpectations(t)
|
||||
}
|
||||
|
||||
logger, hook := logrustest.NewNullLogger()
|
||||
err := newJobExecutor(jim, sfm, rc)(common.WithLogger(ctx, logger.WithField("job", "test")))
|
||||
logger, hook := logtest.NewLogger()
|
||||
err := newJobExecutor(jim, sfm, rc)(common.WithLogger(ctx, logger.With("job", "test")))
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Equal(t, []string{"pre1", "pre2", "step1", "step2"}, executorOrder)
|
||||
|
||||
// the setup is logged ahead of the pre steps and the cleanup is tagged, so the reporter can show both as steps of their own
|
||||
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") ||
|
||||
strings.HasPrefix(entry.Message, "Complete job name") {
|
||||
strings.HasPrefix(entry.Message, "Complete job name") || strings.HasPrefix(entry.Message, "Run Pre") {
|
||||
setup = append(setup, entry.Message)
|
||||
}
|
||||
if _, ok := entry.Attr("jobResult"); ok || strings.HasPrefix(entry.Message, "Cleaning up container") {
|
||||
stage, _ := entry.Attr("stage")
|
||||
assert.Equal(t, StageComplete, stage, entry.Message)
|
||||
}
|
||||
}
|
||||
assert.Equal(t, []string{
|
||||
"Prepare all required actions",
|
||||
"Download action repository 'actions/checkout@v1'",
|
||||
"Download action repository 'actions/checkout@v2'",
|
||||
"Complete job name: test",
|
||||
"Run Pre 1",
|
||||
"Run Pre 2",
|
||||
}, setup)
|
||||
}
|
||||
|
||||
@@ -462,7 +484,7 @@ func TestNewJobExecutor(t *testing.T) {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
fmt.Printf("::group::%s\n", tt.name) //nolint:forbidigo // pre-existing issue from nektos/act
|
||||
|
||||
ctx, cancel := context.WithCancel(common.WithJobErrorContainer(context.Background()))
|
||||
ctx, cancel := context.WithCancel(common.WithJobErrorContainer(t.Context()))
|
||||
defer cancel()
|
||||
jim := &jobInfoMock{}
|
||||
sfm := &stepFactoryMock{}
|
||||
@@ -610,7 +632,7 @@ func (ctx *controllableDeadlineContext) expire() {
|
||||
// still run against a fresh, non-expired context, and the job must still be
|
||||
// reported as failed.
|
||||
func TestNewJobExecutorRunsPostStepsAfterTimeout(t *testing.T) {
|
||||
ctx := newControllableDeadlineContext(common.WithJobErrorContainer(context.Background()))
|
||||
ctx := newControllableDeadlineContext(common.WithJobErrorContainer(t.Context()))
|
||||
|
||||
jim := &jobInfoMock{}
|
||||
sfm := &stepFactoryMock{}
|
||||
@@ -691,7 +713,7 @@ func TestSetJobResultMatrixContinueOnError(t *testing.T) {
|
||||
return &model.Plan{Stages: []*model.Stage{{Runs: []*model.Run{{Workflow: wf, JobID: "job1"}}}}}
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
ctx := t.Context()
|
||||
|
||||
// fail drives a single matrix combination through the failure path; each
|
||||
// RunContext is its own jobInfo (rc implements jobInfo) and shares the job.
|
||||
@@ -786,7 +808,7 @@ func TestTryUploadJobSummaryRetriesTransientFailure(t *testing.T) {
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
ctx := context.Background()
|
||||
ctx := t.Context()
|
||||
cm := &containerMock{}
|
||||
cm.On("GetContainerArchive", mock.Anything, "/var/run/act/workflow/step-summary-0.md").Return(
|
||||
io.NopCloser(bytes.NewReader(tarArchive(t, tarEntry{name: "step-summary-0.md", body: "# summary"}))),
|
||||
@@ -830,7 +852,7 @@ func TestTryUploadJobSummaryStopsAtPhaseTimeout(t *testing.T) {
|
||||
defer server.Close()
|
||||
defer close(release)
|
||||
|
||||
ctx := context.Background()
|
||||
ctx := t.Context()
|
||||
cm := &containerMock{}
|
||||
cm.On("GetContainerArchive", mock.Anything, "/var/run/act/workflow/step-summary-0.md").Return(
|
||||
io.NopCloser(bytes.NewReader(tarArchive(t, tarEntry{name: "step-summary-0.md", body: "# summary"}))),
|
||||
@@ -874,7 +896,7 @@ func TestTryUploadJobSummaryUploadsEachStepIndependentlyWithInstanceTransport(t
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
ctx := context.Background()
|
||||
ctx := t.Context()
|
||||
cm := &containerMock{}
|
||||
// Three steps: 0 has content, 1 has empty content (skipped), 2 has content.
|
||||
cm.On("GetContainerArchive", mock.Anything, "/var/run/act/workflow/step-summary-0.md").Return(
|
||||
@@ -922,7 +944,7 @@ func TestTryUploadJobSummaryRequiresExactCapability(t *testing.T) {
|
||||
"GITEA_RUN_ID": "12",
|
||||
}, &containerMock{}, 1)
|
||||
|
||||
tryUploadJobSummary(context.Background(), rc)
|
||||
tryUploadJobSummary(t.Context(), rc)
|
||||
|
||||
assert.Equal(t, 0, requests)
|
||||
}
|
||||
@@ -942,7 +964,7 @@ func TestTryUploadJobSummarySkipsWhenJobIDMissingFromToken(t *testing.T) {
|
||||
"GITEA_RUN_ID": "12",
|
||||
}, &containerMock{}, 1)
|
||||
|
||||
tryUploadJobSummary(context.Background(), rc)
|
||||
tryUploadJobSummary(t.Context(), rc)
|
||||
|
||||
assert.Equal(t, 0, requests)
|
||||
}
|
||||
@@ -955,7 +977,7 @@ func TestExtractJobIDFromRuntimeToken(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestReadSingleFileFromContainerArchiveFindsMatchingRegularFile(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
ctx := t.Context()
|
||||
cm := &containerMock{}
|
||||
cm.On("GetContainerArchive", ctx, "/var/run/act/workflow/SUMMARY.md").Return(
|
||||
io.NopCloser(bytes.NewReader(tarArchive(t,
|
||||
@@ -974,8 +996,8 @@ func TestReadSingleFileFromContainerArchiveFindsMatchingRegularFile(t *testing.T
|
||||
}
|
||||
|
||||
func TestReadSingleFileFromContainerArchiveTruncatesWhenTooLarge(t *testing.T) {
|
||||
logger, hook := logrustest.NewNullLogger()
|
||||
ctx := common.WithLogger(context.Background(), logger)
|
||||
logger, hook := logtest.NewLogger()
|
||||
ctx := common.WithLogger(t.Context(), logger)
|
||||
cm := &containerMock{}
|
||||
content := strings.Repeat("a", 300)
|
||||
cm.On("GetContainerArchive", ctx, "/var/run/act/workflow/SUMMARY.md").Return(
|
||||
@@ -992,15 +1014,15 @@ func TestReadSingleFileFromContainerArchiveTruncatesWhenTooLarge(t *testing.T) {
|
||||
assert.LessOrEqual(t, len(body), maxBytes)
|
||||
keep := maxBytes - len(jobSummaryTruncationMarker)
|
||||
assert.Equal(t, []byte(content[:keep]+jobSummaryTruncationMarker), body)
|
||||
if assert.Len(t, hook.Entries, 1) {
|
||||
assert.Contains(t, hook.Entries[0].Message, "job summary truncated")
|
||||
if assert.Len(t, hook.AllRecords(), 1) {
|
||||
assert.Contains(t, hook.AllRecords()[0].Message, "job summary truncated")
|
||||
}
|
||||
cm.AssertExpectations(t)
|
||||
}
|
||||
|
||||
func TestReadSingleFileFromContainerArchiveKeepsExactLimitWithoutWarning(t *testing.T) {
|
||||
logger, hook := logrustest.NewNullLogger()
|
||||
ctx := common.WithLogger(context.Background(), logger)
|
||||
logger, hook := logtest.NewLogger()
|
||||
ctx := common.WithLogger(t.Context(), logger)
|
||||
cm := &containerMock{}
|
||||
cm.On("GetContainerArchive", ctx, "/var/run/act/workflow/SUMMARY.md").Return(
|
||||
io.NopCloser(bytes.NewReader(tarArchive(t, tarEntry{name: "SUMMARY.md", body: "abc"}))),
|
||||
@@ -1012,7 +1034,7 @@ func TestReadSingleFileFromContainerArchiveKeepsExactLimitWithoutWarning(t *test
|
||||
// A summary that is exactly at the limit is kept whole and not flagged as truncated.
|
||||
assert.True(t, ok)
|
||||
assert.Equal(t, []byte("abc"), body)
|
||||
assert.Empty(t, hook.Entries)
|
||||
assert.Empty(t, hook.AllRecords())
|
||||
cm.AssertExpectations(t)
|
||||
}
|
||||
|
||||
@@ -1070,7 +1092,7 @@ func makeTestRC(t *testing.T, jobYAML string) *RunContext {
|
||||
Name: "workflow1",
|
||||
Jobs: map[string]*model.Job{"job1": job},
|
||||
}, nil)
|
||||
rc.ExprEval = rc.NewExpressionEvaluator(context.Background())
|
||||
rc.ExprEval = rc.NewExpressionEvaluator(t.Context())
|
||||
return rc
|
||||
}
|
||||
|
||||
@@ -1088,7 +1110,7 @@ func TestApplyJobTimeout(t *testing.T) {
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
rc := makeTestRC(t, tc.yaml)
|
||||
ctx := context.Background()
|
||||
ctx := t.Context()
|
||||
newCtx, cancel := applyJobTimeout(ctx, rc, rc.Run.Job())
|
||||
defer cancel()
|
||||
_, hasDeadline := newCtx.Deadline()
|
||||
@@ -1112,7 +1134,7 @@ func TestEvaluateJobContinueOnError(t *testing.T) {
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
rc := makeTestRC(t, tc.yaml)
|
||||
got := evaluateJobContinueOnError(context.Background(), rc, rc.Run.Job())
|
||||
got := evaluateJobContinueOnError(t.Context(), rc, rc.Run.Job())
|
||||
assert.Equal(t, tc.want, got)
|
||||
})
|
||||
}
|
||||
@@ -1177,7 +1199,7 @@ func TestTryUploadJobSummaryMasksSecrets(t *testing.T) {
|
||||
rc.Config.ExtraMasks = []string{"pr0xypw"}
|
||||
rc.Masks = []string{"runtime-added"}
|
||||
|
||||
tryUploadJobSummary(context.Background(), rc)
|
||||
tryUploadJobSummary(t.Context(), rc)
|
||||
|
||||
assert.Equal(t, "deployed true with *** and *** via ***", got)
|
||||
cm.AssertExpectations(t)
|
||||
|
||||
@@ -42,7 +42,7 @@ func (rc *RunContext) runJobHook(ctx context.Context, hookPath, name string) err
|
||||
}
|
||||
|
||||
cmd, shell := hookCommand(hookPath)
|
||||
rawLogger := common.Logger(ctx).WithField(rawOutputField, true)
|
||||
rawLogger := common.Logger(ctx).With(rawOutputField, true)
|
||||
defer rawLogger.Infof("::endgroup::")
|
||||
rawLogger.Infof("::group::Run '%s'", EscapeCommandData(hookPath))
|
||||
rawLogger.Infof("A %s hook has been configured by the runner administrator", name)
|
||||
|
||||
@@ -12,9 +12,10 @@ import (
|
||||
"testing"
|
||||
|
||||
"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"
|
||||
"github.com/sirupsen/logrus/hooks/test"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"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": {}}}},
|
||||
JobContainer: jobContainer,
|
||||
}
|
||||
logger, _ := test.NewNullLogger()
|
||||
ctx := common.WithJobErrorContainer(common.WithLogger(context.Background(), logger.WithField("test", true)))
|
||||
logger := logging.New(logtest.New())
|
||||
ctx := common.WithJobErrorContainer(common.WithLogger(context.Background(), logger.With("test", true)))
|
||||
rc.ExprEval = rc.NewExpressionEvaluator(ctx)
|
||||
return rc, ctx
|
||||
}
|
||||
|
||||
+136
-99
@@ -19,8 +19,8 @@ import (
|
||||
"sync"
|
||||
|
||||
"gitea.com/gitea/runner/act/common"
|
||||
"gitea.com/gitea/runner/act/common/logging"
|
||||
|
||||
"github.com/sirupsen/logrus"
|
||||
"golang.org/x/term"
|
||||
)
|
||||
|
||||
@@ -35,6 +35,16 @@ const (
|
||||
gray = 37
|
||||
)
|
||||
|
||||
const (
|
||||
// The "stage" of the log entries of a step, and of the job level entries that finalize the job after its steps.
|
||||
StagePre = "Pre"
|
||||
StageMain = "Main"
|
||||
StagePost = "Post"
|
||||
StageComplete = "Complete"
|
||||
// StepStartedField marks the log entry of a step that is about to run, unlike one that is skipped.
|
||||
StepStartedField = "stepStarted"
|
||||
)
|
||||
|
||||
const (
|
||||
rawOutputField = "raw_output"
|
||||
scriptLineCyanField = "script_line_cyan"
|
||||
@@ -73,8 +83,9 @@ func WithMasks(ctx context.Context, masks *[]string) context.Context {
|
||||
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 {
|
||||
WithJobLogger() *logrus.Logger
|
||||
WithJobLogger() logging.Handler
|
||||
}
|
||||
|
||||
type jobLoggerFactoryContextKey string
|
||||
@@ -89,84 +100,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 {
|
||||
ctx = WithMasks(ctx, masks)
|
||||
|
||||
var logger *logrus.Logger
|
||||
var output logging.Handler
|
||||
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 {
|
||||
var formatter logrus.Formatter
|
||||
if config.JSONLogger {
|
||||
formatter = &logrus.JSONFormatter{}
|
||||
} else {
|
||||
mux.Lock()
|
||||
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)
|
||||
mux.Lock()
|
||||
nextColor++
|
||||
color := colors[nextColor%len(colors)]
|
||||
mux.Unlock()
|
||||
output = newJobLogHandler(os.Stdout, color)
|
||||
}
|
||||
|
||||
{ // Adapt to Gitea
|
||||
if hook := common.LoggerHook(ctx); hook != nil {
|
||||
logger.AddHook(hook)
|
||||
}
|
||||
if config.JobLoggerLevel != nil {
|
||||
logger.SetLevel(*config.JobLoggerLevel)
|
||||
} else {
|
||||
logger.SetLevel(logrus.TraceLevel)
|
||||
}
|
||||
var handlers []logging.Handler
|
||||
if h := common.LoggerHandler(ctx); h != nil { // Adapt to Gitea
|
||||
handlers = append(handlers, h)
|
||||
}
|
||||
|
||||
logger.SetFormatter(&maskedFormatter{
|
||||
Formatter: logger.Formatter,
|
||||
masker: valueMasker(config.InsecureSecrets, config.maskers()),
|
||||
handlers = append(handlers, &maskedHandler{
|
||||
Handler: output,
|
||||
masker: valueMasker(config.InsecureSecrets, config.maskers()),
|
||||
})
|
||||
rtn := logger.WithFields(logrus.Fields{
|
||||
"job": jobName,
|
||||
"jobID": jobID,
|
||||
"dryrun": common.Dryrun(ctx),
|
||||
"matrix": matrix,
|
||||
}).WithContext(ctx)
|
||||
|
||||
level := logging.LevelTrace
|
||||
if config.JobLoggerLevel != nil {
|
||||
level = *config.JobLoggerLevel
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
func WithCompositeLogger(ctx context.Context, masks *[]string) context.Context {
|
||||
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 {
|
||||
val := common.Logger(ctx)
|
||||
stepIDs := make([]string, 0)
|
||||
stepIDs := append(slices.Clone(common.StepIDs(ctx)), stepID)
|
||||
ctx = common.WithStepIDs(ctx, stepIDs)
|
||||
|
||||
if logger, ok := val.(*logrus.Entry); ok {
|
||||
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))
|
||||
return common.WithLogger(ctx, common.Logger(ctx).With("stepID", stepIDs).WithContext(ctx))
|
||||
}
|
||||
|
||||
func withStepLogger(ctx context.Context, stepNumber int, stepID, stepName, stageName string) context.Context {
|
||||
rtn := common.Logger(ctx).WithFields(logrus.Fields{
|
||||
"stepNumber": stepNumber,
|
||||
"step": stepName,
|
||||
"stepID": []string{stepID},
|
||||
"stage": stageName,
|
||||
})
|
||||
stepIDs := []string{stepID}
|
||||
ctx = common.WithStepIDs(ctx, stepIDs)
|
||||
|
||||
rtn := common.Logger(ctx).With(
|
||||
"stepNumber", stepNumber,
|
||||
"step", stepName,
|
||||
"stepID", stepIDs,
|
||||
"stage", stageName,
|
||||
)
|
||||
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{
|
||||
func(v string) string { return base64.StdEncoding.EncodeToString([]byte(v)) },
|
||||
@@ -321,16 +318,16 @@ func valueMasker(insecureSecrets bool, oldnew []string) entryProcessor {
|
||||
replacer *strings.Replacer
|
||||
)
|
||||
|
||||
return func(entry *logrus.Entry) *logrus.Entry {
|
||||
return func(ctx context.Context, r logging.Record) logging.Record {
|
||||
if insecureSecrets {
|
||||
return entry
|
||||
return r
|
||||
}
|
||||
|
||||
masks := Masks(entry.Context)
|
||||
masks := Masks(ctx)
|
||||
|
||||
if len(*masks) == 0 {
|
||||
entry.Message = defReplacer.Replace(entry.Message)
|
||||
return entry
|
||||
r.Message = defReplacer.Replace(r.Message)
|
||||
return r
|
||||
}
|
||||
|
||||
mu.Lock()
|
||||
@@ -349,89 +346,129 @@ func valueMasker(insecureSecrets bool, oldnew []string) entryProcessor {
|
||||
cmasker := replacer
|
||||
mu.Unlock()
|
||||
|
||||
entry.Message = cmasker.Replace(entry.Message)
|
||||
r.Message = cmasker.Replace(r.Message)
|
||||
|
||||
return entry
|
||||
return r
|
||||
}
|
||||
}
|
||||
|
||||
type maskedFormatter struct {
|
||||
logrus.Formatter
|
||||
type maskedHandler struct {
|
||||
logging.Handler
|
||||
masker entryProcessor
|
||||
}
|
||||
|
||||
func (f *maskedFormatter) Format(entry *logrus.Entry) ([]byte, error) {
|
||||
return f.Formatter.Format(f.masker(entry))
|
||||
func (m *maskedHandler) Handle(ctx context.Context, r logging.Record) error {
|
||||
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
|
||||
}
|
||||
|
||||
func (f *jobLogFormatter) Format(entry *logrus.Entry) ([]byte, error) {
|
||||
func (f textFormatter) format(r logging.Record) ([]byte, error) {
|
||||
b := &bytes.Buffer{}
|
||||
|
||||
// the web renderer decodes command data, so this local view has to as well
|
||||
if _, _, _, ok := tryParseRawActionCommand(entry.Message + "\n"); ok {
|
||||
entry.Message = UnescapeCommandData(entry.Message)
|
||||
if _, _, _, ok := tryParseRawActionCommand(r.Message + "\n"); ok {
|
||||
r.Message = UnescapeCommandData(r.Message)
|
||||
}
|
||||
r.Message = strings.TrimSuffix(r.Message, "\n")
|
||||
|
||||
if f.isColored(entry) {
|
||||
f.printColored(b, entry)
|
||||
if f.isColored() {
|
||||
f.printColored(b, r)
|
||||
} else {
|
||||
f.print(b, entry)
|
||||
f.print(b, r)
|
||||
}
|
||||
|
||||
b.WriteByte('\n')
|
||||
return b.Bytes(), nil
|
||||
}
|
||||
|
||||
func (f *jobLogFormatter) printColored(b *bytes.Buffer, entry *logrus.Entry) {
|
||||
entry.Message = strings.TrimSuffix(entry.Message, "\n")
|
||||
func attrIsTrue(r logging.Record, key string) bool {
|
||||
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 := ""
|
||||
if entry.Level == logrus.DebugLevel {
|
||||
if r.Level == logging.LevelDebug {
|
||||
debugFlag = "[DEBUG] "
|
||||
}
|
||||
|
||||
switch {
|
||||
case entry.Data[rawOutputField] == true:
|
||||
if entry.Data[scriptLineCyanField] == true {
|
||||
fmt.Fprintf(b, "\x1b[%dm|\x1b[0m \x1b[36;1m%s\x1b[0m", f.color, entry.Message)
|
||||
case attrIsTrue(r, rawOutputField):
|
||||
if attrIsTrue(r, scriptLineCyanField) {
|
||||
fmt.Fprintf(b, "\x1b[%dm|\x1b[0m \x1b[36;1m%s\x1b[0m", f.color, r.Message)
|
||||
} 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:
|
||||
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)
|
||||
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, r.Message)
|
||||
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) {
|
||||
entry.Message = strings.TrimSuffix(entry.Message, "\n")
|
||||
|
||||
job := entry.Data["job"]
|
||||
func (f textFormatter) print(b *bytes.Buffer, r logging.Record) {
|
||||
job, _ := r.Attr("job")
|
||||
|
||||
debugFlag := ""
|
||||
if entry.Level == logrus.DebugLevel {
|
||||
if r.Level == logging.LevelDebug {
|
||||
debugFlag = "[DEBUG] "
|
||||
}
|
||||
|
||||
switch {
|
||||
case entry.Data[rawOutputField] == true:
|
||||
fmt.Fprintf(b, "[%s] | %s", job, entry.Message)
|
||||
case entry.Data["dryrun"] == true:
|
||||
fmt.Fprintf(b, "*DRYRUN* [%s] %s%s", job, debugFlag, entry.Message)
|
||||
case attrIsTrue(r, rawOutputField):
|
||||
fmt.Fprintf(b, "[%s] | %s", job, r.Message)
|
||||
case attrIsTrue(r, "dryrun"):
|
||||
fmt.Fprintf(b, "*DRYRUN* [%s] %s%s", job, debugFlag, r.Message)
|
||||
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 {
|
||||
isColored := checkIfTerminal(entry.Logger.Out)
|
||||
func (f textFormatter) isColored() bool {
|
||||
isColored := checkIfTerminal(f.w)
|
||||
|
||||
if force, ok := os.LookupEnv("CLICOLOR_FORCE"); ok && force != "0" {
|
||||
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
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"io"
|
||||
"net/url"
|
||||
"strings"
|
||||
"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/require"
|
||||
)
|
||||
@@ -49,10 +55,7 @@ func TestValueMasker(t *testing.T) {
|
||||
ctx := WithMasks(t.Context(), &entry.masks)
|
||||
masker := valueMasker(false, AppendSecretMaskers(nil, entry.secrets))
|
||||
for line := range strings.SplitSeq(entry.lines, "\n") {
|
||||
lentry := masker(&logrus.Entry{
|
||||
Context: ctx,
|
||||
Message: line,
|
||||
})
|
||||
lentry := masker(ctx, logging.Record{Message: line})
|
||||
for _, line := range entry.disallowed {
|
||||
assert.NotContains(t, lentry.Message, line)
|
||||
}
|
||||
@@ -79,7 +82,7 @@ func TestValueMaskerEncodedSecrets(t *testing.T) {
|
||||
} {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
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, "***")
|
||||
for _, disallowed := range tc.encoded {
|
||||
@@ -105,7 +108,7 @@ func TestValueMaskerJSONEscapesBothWays(t *testing.T) {
|
||||
{"literal (JS JSON.stringify / .NET)", jsonStringEscapeNoHTML(secret)},
|
||||
} {
|
||||
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.NotContains(t, entry.Message, tc.form)
|
||||
@@ -118,7 +121,7 @@ func TestValueMaskerJSONEscapesBothWays(t *testing.T) {
|
||||
func TestValueMaskerHidesExtraMasks(t *testing.T) {
|
||||
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)
|
||||
}
|
||||
@@ -137,7 +140,7 @@ func TestValueMaskerEncodedMasks(t *testing.T) {
|
||||
{"encoded: " + base64.StdEncoding.EncodeToString([]byte("s3cr3t")), "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)
|
||||
}
|
||||
}
|
||||
@@ -154,7 +157,7 @@ func TestValueMaskerBase64Alignments(t *testing.T) {
|
||||
for _, prefix := range []string{"x-access-token:", "user:", "ab:"} {
|
||||
t.Run(prefix, func(t *testing.T) {
|
||||
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.NotEqual(t, "Authorization: Basic "+encoded, entry.Message)
|
||||
@@ -172,7 +175,7 @@ func TestValueMaskerBase64Alignments(t *testing.T) {
|
||||
func TestValueMaskerCachedReplacerSeesNewMasks(t *testing.T) {
|
||||
masker := valueMasker(false, AppendSecretMaskers(nil, map[string]string{"TOKEN": "secret-token"}))
|
||||
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"}
|
||||
@@ -206,15 +209,61 @@ func TestAppendSecretMaskerSkipsUselessEncodings(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestJobLogFormatterDecodesCommandData(t *testing.T) {
|
||||
logger := logrus.New()
|
||||
logger.Out = io.Discard
|
||||
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)
|
||||
return string(out)
|
||||
return string(line)
|
||||
}
|
||||
|
||||
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
|
||||
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"])
|
||||
}
|
||||
@@ -10,7 +10,6 @@ import (
|
||||
"net/url"
|
||||
"path"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"gitea.com/gitea/runner/act/common"
|
||||
"gitea.com/gitea/runner/act/common/git"
|
||||
@@ -27,7 +26,7 @@ func newLocalReusableWorkflowExecutor(rc *RunContext) common.Executor {
|
||||
if !rc.Config.NoSkipCheckout {
|
||||
// resolve the local workflow against the workspace root, not the process
|
||||
// working directory, so it is found regardless of where the runner is invoked
|
||||
return newReusableWorkflowExecutor(rc, rc.Config.Workdir, localWorkflow.Path)
|
||||
return newReusableWorkflowExecutor(rc, rc.Config.Workdir, localWorkflow.Path, false)
|
||||
}
|
||||
|
||||
uses := fmt.Sprintf("%s/%s@%s", rc.Config.PresetGitHubContext.Repository, localWorkflow.Path, rc.Config.PresetGitHubContext.Sha)
|
||||
@@ -41,7 +40,7 @@ func newLocalReusableWorkflowExecutor(rc *RunContext) common.Executor {
|
||||
|
||||
return common.NewPipelineExecutor(
|
||||
cloneRemoteReusableWorkflow(rc, remoteReusableWorkflow.CloneURL(), remoteReusableWorkflow.Ref, workflowDir),
|
||||
newReusableWorkflowExecutor(rc, workflowDir, remoteReusableWorkflow.Path),
|
||||
newReusableWorkflowExecutor(rc, workflowDir, remoteReusableWorkflow.Path, true),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -59,7 +58,7 @@ func newRemoteReusableWorkflowExecutor(rc *RunContext) common.Executor {
|
||||
|
||||
return common.NewPipelineExecutor(
|
||||
cloneRemoteReusableWorkflow(rc, remoteReusableWorkflow.CloneURL(), remoteReusableWorkflow.Ref, workflowDir),
|
||||
newReusableWorkflowExecutor(rc, workflowDir, remoteReusableWorkflow.Path),
|
||||
newReusableWorkflowExecutor(rc, workflowDir, remoteReusableWorkflow.Path, true),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -87,11 +86,16 @@ func cloneRemoteReusableWorkflow(rc *RunContext, cloneURL, ref, targetDirectory
|
||||
}
|
||||
}
|
||||
|
||||
func newReusableWorkflowExecutor(rc *RunContext, directory, workflow string) common.Executor {
|
||||
func newReusableWorkflowExecutor(rc *RunContext, directory, workflow string, cached bool) common.Executor {
|
||||
return func(ctx context.Context) error {
|
||||
// Serialize workflow reads with cache updates.
|
||||
planner, err := func() (model.WorkflowPlanner, error) {
|
||||
defer git.AcquireCloneLock(directory)()
|
||||
if cached {
|
||||
unlock, err := git.AcquireCloneLock(ctx, directory)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer unlock()
|
||||
}
|
||||
return model.NewWorkflowPlanner(path.Join(directory, workflow), true)
|
||||
}()
|
||||
if err != nil {
|
||||
@@ -135,11 +139,7 @@ type remoteReusableWorkflow struct {
|
||||
}
|
||||
|
||||
func (r *remoteReusableWorkflow) CloneURL() string {
|
||||
// In Gitea, r.URL always has the protocol prefix, we don't need to add extra prefix in this case.
|
||||
if strings.HasPrefix(r.URL, "http://") || strings.HasPrefix(r.URL, "https://") {
|
||||
return fmt.Sprintf("%s/%s/%s", r.URL, r.Owner, r.Repo)
|
||||
}
|
||||
return fmt.Sprintf("https://%s/%s/%s", r.URL, r.Owner, r.Repo)
|
||||
return fmt.Sprintf("%s/%s/%s", withHTTPS(r.URL), r.Owner, r.Repo)
|
||||
}
|
||||
|
||||
var absoluteReusableWorkflowURLRegex = regexp.MustCompile(`^(https?://.*)/([^/]+/[^/]+/\.[^/]+/workflows/[^@]+@.*)$`)
|
||||
@@ -193,7 +193,7 @@ func setReusedWorkflowCallerResult(rc *RunContext, runner *runnerImpl) common.Ex
|
||||
unlock := lockJob(rc.Run.Job())
|
||||
rc.result(reusedWorkflowJobResult)
|
||||
unlock()
|
||||
common.Logger(ctx).WithField("jobResult", reusedWorkflowJobResult).Infof("Job %s", reusedWorkflowJobResultMessage)
|
||||
common.Logger(ctx).With("jobResult", reusedWorkflowJobResult).Infof("Job %s", reusedWorkflowJobResultMessage)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -233,10 +233,7 @@ func onInstanceHost(host string, instanceURLs ...string) bool {
|
||||
if candidate == "" {
|
||||
continue
|
||||
}
|
||||
if !strings.HasPrefix(candidate, "http://") && !strings.HasPrefix(candidate, "https://") {
|
||||
candidate = "https://" + candidate
|
||||
}
|
||||
if u, err := url.Parse(candidate); err == nil && u.Host == host {
|
||||
if u, err := url.Parse(withHTTPS(candidate)); err == nil && u.Host == host {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,7 +8,6 @@ import (
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
"encoding/pem"
|
||||
"io/fs"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"os"
|
||||
@@ -64,7 +63,7 @@ func TestReusableWorkflowCachedBranchRefRefreshes(t *testing.T) {
|
||||
}
|
||||
cacheDir := t.TempDir()
|
||||
|
||||
require.NoError(t, cloneRemoteReusableWorkflow(rc, remoteDir, "master", cacheDir)(context.Background()))
|
||||
require.NoError(t, cloneRemoteReusableWorkflow(rc, remoteDir, "master", cacheDir)(t.Context()))
|
||||
got, err := os.ReadFile(filepath.Join(cacheDir, workflowPath))
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, tmpl("v1"), string(got))
|
||||
@@ -74,7 +73,7 @@ func TestReusableWorkflowCachedBranchRefRefreshes(t *testing.T) {
|
||||
gitMust(t, workDir, "commit", "-am", "v2")
|
||||
gitMust(t, workDir, "push", "origin", "master")
|
||||
|
||||
require.NoError(t, cloneRemoteReusableWorkflow(rc, remoteDir, "master", cacheDir)(context.Background()))
|
||||
require.NoError(t, cloneRemoteReusableWorkflow(rc, remoteDir, "master", cacheDir)(t.Context()))
|
||||
got, err = os.ReadFile(filepath.Join(cacheDir, workflowPath))
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, tmpl("v2"), string(got), "cached workflow file must reflect the updated branch tip")
|
||||
@@ -84,14 +83,16 @@ func TestNewReusableWorkflowExecutorHoldsCloneLock(t *testing.T) {
|
||||
workflowDir := t.TempDir()
|
||||
require.NoError(t, os.WriteFile(filepath.Join(workflowDir, "reusable.yml"), []byte(":"), 0o644))
|
||||
|
||||
unlockOnce := sync.OnceFunc(git.AcquireCloneLock(workflowDir))
|
||||
unlock, err := git.AcquireCloneLock(t.Context(), workflowDir)
|
||||
require.NoError(t, err)
|
||||
unlockOnce := sync.OnceFunc(unlock)
|
||||
defer unlockOnce()
|
||||
|
||||
rc := &RunContext{
|
||||
Config: &Config{},
|
||||
Run: &model.Run{Workflow: &model.Workflow{Jobs: map[string]*model.Job{}}},
|
||||
}
|
||||
exec := newReusableWorkflowExecutor(rc, workflowDir, "reusable.yml")
|
||||
exec := newReusableWorkflowExecutor(rc, workflowDir, "reusable.yml", true)
|
||||
|
||||
done := make(chan error, 1)
|
||||
go func() { done <- exec(context.Background()) }()
|
||||
@@ -113,8 +114,9 @@ func TestNewReusableWorkflowExecutorHoldsCloneLock(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestNewLocalReusableWorkflowExecutorFindsSameRepositoryPaths(t *testing.T) {
|
||||
workdir := t.TempDir()
|
||||
workdir := filepath.Join(t.TempDir(), "workspace")
|
||||
require.NoError(t, os.MkdirAll(filepath.Join(workdir, ".gitea", "workflows"), 0o755))
|
||||
require.NoError(t, os.Mkdir(workdir+".lock", 0o755))
|
||||
require.NoError(t, os.WriteFile(filepath.Join(workdir, ".gitea", "workflows", "reusable.yml"), []byte(":"), 0o644))
|
||||
|
||||
for _, uses := range []string{"./.gitea/workflows/reusable.yml", "$/.gitea/workflows/reusable.yml"} {
|
||||
@@ -123,8 +125,7 @@ func TestNewLocalReusableWorkflowExecutorFindsSameRepositoryPaths(t *testing.T)
|
||||
Run: &model.Run{JobID: "job", Workflow: &model.Workflow{Jobs: map[string]*model.Job{"job": {Uses: uses}}}},
|
||||
}
|
||||
err := newLocalReusableWorkflowExecutor(rc)(t.Context())
|
||||
require.Error(t, err)
|
||||
require.NotErrorIs(t, err, fs.ErrNotExist)
|
||||
require.ErrorContains(t, err, "workflow is not valid")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+29
-45
@@ -511,7 +511,7 @@ func (rc *RunContext) startKubernetesPod() common.Executor {
|
||||
// printStartJobContainerGroup mirrors actions/runner's "Starting job container"
|
||||
// section: emit the group header and summary, return a closer for ::endgroup::.
|
||||
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("image: %s", image)
|
||||
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 {
|
||||
rawLogger := common.Logger(ctx).WithField(rawOutputField, true)
|
||||
rawLogger := common.Logger(ctx).With(rawOutputField, true)
|
||||
return func(line string) bool {
|
||||
rawLogger.Infof("%s", line)
|
||||
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
|
||||
// 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 {
|
||||
rawLogger := common.Logger(ctx).WithField(rawOutputField, true)
|
||||
rawLogger := common.Logger(ctx).With(rawOutputField, true)
|
||||
loggedStarting := false
|
||||
|
||||
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) {
|
||||
ctx, cancel := context.WithTimeout(ctx, serviceLogsTimeout)
|
||||
defer cancel()
|
||||
rawLogger := common.Logger(ctx).WithField(rawOutputField, true)
|
||||
rawLogger := common.Logger(ctx).With(rawOutputField, true)
|
||||
for _, svc := range rc.serviceContainers {
|
||||
if !svc.created || svc.logsDumped {
|
||||
continue
|
||||
@@ -1338,7 +1338,7 @@ func (rc *RunContext) isEnabled(ctx context.Context) (bool, error) {
|
||||
rc.caller.setReusedWorkflowJobResult(rc.Run.JobID, "skipped")
|
||||
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
|
||||
}
|
||||
|
||||
@@ -1541,14 +1541,8 @@ func (rc *RunContext) getGithubContext(ctx context.Context) *model.GithubContext
|
||||
merged.RunnerPerflog, merged.RunnerTrackingID = ghc.RunnerPerflog, ghc.RunnerTrackingID
|
||||
*ghc = merged
|
||||
|
||||
instance := rc.Config.GitHubInstance
|
||||
if !strings.HasPrefix(instance, "http://") &&
|
||||
!strings.HasPrefix(instance, "https://") {
|
||||
instance = "https://" + instance
|
||||
}
|
||||
ghc.ServerURL = instance
|
||||
ghc.APIURL = instance + "/api/v1" // the version of Gitea is v1
|
||||
ghc.GraphQLURL = "" // Gitea doesn't support graphql
|
||||
rc.setInstanceURLs(ghc)
|
||||
ghc.GraphQLURL = "" // Gitea doesn't support graphql
|
||||
return ghc
|
||||
}
|
||||
}
|
||||
@@ -1562,7 +1556,7 @@ func (rc *RunContext) getGithubContext(ctx context.Context) *model.GithubContext
|
||||
|
||||
ghc.SetBaseAndHeadRef()
|
||||
repoPath := rc.Config.Workdir
|
||||
ghcontext.SetRepositoryAndOwner(ctx, ghc, rc.Config.GitHubInstance, repoPath)
|
||||
ghcontext.SetRepositoryAndOwner(ctx, ghc, repoPath)
|
||||
if ghc.Ref == "" {
|
||||
ghcontext.SetRef(ctx, ghc, repoPath)
|
||||
}
|
||||
@@ -1572,27 +1566,7 @@ func (rc *RunContext) getGithubContext(ctx context.Context) *model.GithubContext
|
||||
|
||||
ghc.SetRefTypeAndName()
|
||||
|
||||
// defaults
|
||||
ghc.ServerURL = "https://github.com"
|
||||
ghc.APIURL = "https://api.github.com"
|
||||
ghc.GraphQLURL = "https://api.github.com/graphql"
|
||||
// per GHES
|
||||
if rc.Config.GitHubInstance != "github.com" {
|
||||
ghc.ServerURL = "https://" + rc.Config.GitHubInstance
|
||||
ghc.APIURL = fmt.Sprintf("https://%s/api/v3", rc.Config.GitHubInstance)
|
||||
ghc.GraphQLURL = fmt.Sprintf("https://%s/api/graphql", rc.Config.GitHubInstance)
|
||||
}
|
||||
|
||||
{ // Adapt to Gitea
|
||||
instance := rc.Config.GitHubInstance
|
||||
if !strings.HasPrefix(instance, "http://") &&
|
||||
!strings.HasPrefix(instance, "https://") {
|
||||
instance = "https://" + instance
|
||||
}
|
||||
ghc.ServerURL = instance
|
||||
ghc.APIURL = instance + "/api/v1" // the version of Gitea is v1
|
||||
ghc.GraphQLURL = "" // Gitea doesn't support graphql
|
||||
}
|
||||
rc.setInstanceURLs(ghc)
|
||||
|
||||
// allow to be overridden by user
|
||||
if rc.Config.Env["GITHUB_SERVER_URL"] != "" {
|
||||
@@ -1608,6 +1582,20 @@ func (rc *RunContext) getGithubContext(ctx context.Context) *model.GithubContext
|
||||
return ghc
|
||||
}
|
||||
|
||||
func (rc *RunContext) setInstanceURLs(ghc *model.GithubContext) {
|
||||
if instance := strings.TrimRight(rc.Config.GitHubInstance, "/"); instance != "" {
|
||||
ghc.ServerURL = withHTTPS(instance)
|
||||
ghc.APIURL = ghc.ServerURL + "/api/v1"
|
||||
}
|
||||
}
|
||||
|
||||
func withHTTPS(u string) string {
|
||||
if strings.HasPrefix(u, "http://") || strings.HasPrefix(u, "https://") {
|
||||
return u
|
||||
}
|
||||
return "https://" + u
|
||||
}
|
||||
|
||||
func isLocalCheckout(ghc *model.GithubContext, step *model.Step) bool {
|
||||
if step.Type() == model.StepTypeInvalid {
|
||||
// This will be errored out by the executor later, we need this here to avoid a null panic though
|
||||
@@ -1657,20 +1645,16 @@ func (rc *RunContext) withGithubEnv(ctx context.Context, github *model.GithubCon
|
||||
env["GITHUB_HEAD_REF"] = github.HeadRef
|
||||
env["GITHUB_SERVER_URL"] = github.ServerURL
|
||||
env["GITHUB_API_URL"] = github.APIURL
|
||||
env["GITHUB_GRAPHQL_URL"] = github.GraphQLURL
|
||||
|
||||
{ // Adapt to Gitea
|
||||
instance := rc.Config.GitHubInstance
|
||||
if !strings.HasPrefix(instance, "http://") &&
|
||||
!strings.HasPrefix(instance, "https://") {
|
||||
instance = "https://" + instance
|
||||
for _, name := range []string{"GITHUB_SERVER_URL", "GITHUB_API_URL"} {
|
||||
if env[name] == "" {
|
||||
delete(env, name)
|
||||
}
|
||||
env["GITHUB_SERVER_URL"] = instance
|
||||
env["GITHUB_API_URL"] = instance + "/api/v1" // the version of Gitea is v1
|
||||
env["GITHUB_GRAPHQL_URL"] = "" // Gitea doesn't support graphql
|
||||
env["GITHUB_RUN_ATTEMPT"] = github.RunAttempt
|
||||
}
|
||||
|
||||
env["GITHUB_GRAPHQL_URL"] = ""
|
||||
env["GITHUB_RUN_ATTEMPT"] = github.RunAttempt
|
||||
|
||||
env["RUNNER_NAME"] = rc.Config.RunnerName
|
||||
env["RUNNER_ENVIRONMENT"] = "self-hosted"
|
||||
if workspace := parentDir(github.Workspace); workspace != "" {
|
||||
|
||||
@@ -25,14 +25,14 @@ import (
|
||||
"time"
|
||||
|
||||
"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.dev/actionslib/pkg/exprparser"
|
||||
"gitea.dev/actionslib/pkg/model"
|
||||
"github.com/docker/cli/cli/compose/loader"
|
||||
"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"
|
||||
"github.com/stretchr/testify/mock"
|
||||
require "github.com/stretchr/testify/require"
|
||||
@@ -83,7 +83,7 @@ func TestRunContext_EvalBool(t *testing.T) {
|
||||
},
|
||||
},
|
||||
}
|
||||
rc.ExprEval = rc.NewExpressionEvaluator(context.Background())
|
||||
rc.ExprEval = rc.NewExpressionEvaluator(t.Context())
|
||||
|
||||
tables := []struct {
|
||||
in string
|
||||
@@ -176,7 +176,7 @@ func TestRunContext_EvalBool(t *testing.T) {
|
||||
for _, table := range tables {
|
||||
t.Run(table.in, func(t *testing.T) {
|
||||
assertObject := assert.New(t)
|
||||
b, err := EvalBool(context.Background(), rc.ExprEval, table.in, exprparser.DefaultStatusCheckSuccess)
|
||||
b, err := EvalBool(t.Context(), rc.ExprEval, table.in, exprparser.DefaultStatusCheckSuccess)
|
||||
if table.wantErr {
|
||||
assertObject.Error(err) //nolint:testifylint // pre-existing issue from nektos/act
|
||||
}
|
||||
@@ -563,8 +563,8 @@ func TestRunContext_GetBindsAndMounts(t *testing.T) {
|
||||
}
|
||||
rc.Run.JobID = "job1"
|
||||
rc.Run.Workflow.Jobs = map[string]*model.Job{"job1": job}
|
||||
rc.ExprEval = rc.NewExpressionEvaluator(context.Background())
|
||||
require.NoError(t, rc.resolvePlatformImage(context.Background()))
|
||||
rc.ExprEval = rc.NewExpressionEvaluator(t.Context())
|
||||
require.NoError(t, rc.resolvePlatformImage(t.Context()))
|
||||
|
||||
gotbind, gotmount := rc.GetBindsAndMounts(t.Context())
|
||||
assert.Contains(t, gotbind, "/host/mame/roms:/root/.mame/roms:ro")
|
||||
@@ -864,7 +864,7 @@ func TestInterpolateOutputsIsPerMatrixComboKeepingNeedsOutputs(t *testing.T) {
|
||||
needed := &model.Job{Outputs: map[string]string{"o": "from-gitea"}}
|
||||
run := &model.Run{JobID: "j", Workflow: &model.Workflow{Name: "w", Jobs: map[string]*model.Job{"j": job, "needed": needed}}}
|
||||
r := &runnerImpl{config: &Config{}}
|
||||
ctx := context.Background()
|
||||
ctx := t.Context()
|
||||
|
||||
_, err := r.newRunContext(ctx, &model.Run{JobID: "needed", Workflow: run.Workflow}, nil)
|
||||
require.NoError(t, err)
|
||||
@@ -899,7 +899,7 @@ func TestInterpolateOutputsFromExpression(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestGetGitHubContext(t *testing.T) {
|
||||
log.SetLevel(log.DebugLevel)
|
||||
logging.SetLevel(logging.LevelDebug)
|
||||
|
||||
cwd, err := os.Getwd()
|
||||
assert.NoError(t, err) //nolint:testifylint // pre-existing issue from nektos/act
|
||||
@@ -915,6 +915,7 @@ func TestGetGitHubContext(t *testing.T) {
|
||||
Run: &model.Run{
|
||||
Workflow: &model.Workflow{
|
||||
Name: "GitHubContextTest",
|
||||
Jobs: map[string]*model.Job{"job1": {}},
|
||||
},
|
||||
},
|
||||
Name: "GitHubContextTest",
|
||||
@@ -926,9 +927,9 @@ func TestGetGitHubContext(t *testing.T) {
|
||||
}
|
||||
rc.Run.JobID = "job1"
|
||||
|
||||
ghc := rc.getGithubContext(context.Background())
|
||||
ghc := rc.getGithubContext(t.Context())
|
||||
|
||||
log.Debugf("%v", ghc)
|
||||
logging.Debugf("%v", ghc)
|
||||
|
||||
actor := "nektos/act"
|
||||
if a := os.Getenv("ACT_ACTOR"); a != "" {
|
||||
@@ -956,10 +957,40 @@ func TestGetGitHubContext(t *testing.T) {
|
||||
assert.Equal(t, ghc.Job, "job1") //nolint:testifylint // pre-existing issue from nektos/act
|
||||
|
||||
rc.Config.PresetGitHubContext = &model.GithubContext{Actor: "preset-actor", TriggeringActor: "triggerer", Job: "preset-job"}
|
||||
ghc = rc.getGithubContext(context.Background())
|
||||
ghc = rc.getGithubContext(t.Context())
|
||||
assert.Equal(t, "preset-actor", ghc.Actor)
|
||||
assert.Equal(t, "triggerer", ghc.TriggeringActor)
|
||||
assert.Equal(t, "job1", ghc.Job)
|
||||
|
||||
preset := &model.GithubContext{ServerURL: "https://preset.test", APIURL: "https://preset.test/api/v1"}
|
||||
for _, testcase := range []struct {
|
||||
name, instance, serverURL, apiURL string
|
||||
preset *model.GithubContext
|
||||
env map[string]string
|
||||
}{
|
||||
{name: "unknown instance"},
|
||||
{name: "bare host with trailing slash", instance: "gitea.test/", serverURL: "https://gitea.test", apiURL: "https://gitea.test/api/v1"},
|
||||
{name: "scheme port and base path", instance: "http://gitea.test:3000/gitea/", serverURL: "http://gitea.test:3000/gitea", apiURL: "http://gitea.test:3000/gitea/api/v1"},
|
||||
{name: "daemon instance replaces preset endpoints", instance: "gitea.test", preset: preset, serverURL: "https://gitea.test", apiURL: "https://gitea.test/api/v1"},
|
||||
{name: "daemon keeps preset endpoints without instance", preset: preset, serverURL: "https://preset.test", apiURL: "https://preset.test/api/v1"},
|
||||
{name: "environment overrides", env: map[string]string{"GITHUB_SERVER_URL": "https://override.test", "GITHUB_API_URL": "https://override.test/api"}, serverURL: "https://override.test", apiURL: "https://override.test/api"},
|
||||
} {
|
||||
t.Run(testcase.name, func(t *testing.T) {
|
||||
rc.Config.GitHubInstance = testcase.instance
|
||||
rc.Config.PresetGitHubContext = testcase.preset
|
||||
rc.Config.Env = map[string]string{"GITHUB_REPOSITORY": "owner/repo", "GITHUB_REF": "refs/heads/main", "SHA_REF": "1234567890"}
|
||||
maps.Copy(rc.Config.Env, testcase.env)
|
||||
github := rc.getGithubContext(t.Context())
|
||||
assert.Equal(t, testcase.serverURL, github.ServerURL)
|
||||
assert.Equal(t, testcase.apiURL, github.APIURL)
|
||||
env := map[string]string{}
|
||||
rc.withGithubEnv(t.Context(), github, env)
|
||||
_, exported := env["GITHUB_SERVER_URL"]
|
||||
assert.Equal(t, testcase.serverURL != "", exported)
|
||||
assert.Equal(t, testcase.serverURL, env["GITHUB_SERVER_URL"])
|
||||
assert.Equal(t, testcase.apiURL, env["GITHUB_API_URL"])
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetGithubContextRef(t *testing.T) {
|
||||
@@ -996,7 +1027,7 @@ func TestGetGithubContextRef(t *testing.T) {
|
||||
},
|
||||
}
|
||||
|
||||
ghc := rc.getGithubContext(context.Background())
|
||||
ghc := rc.getGithubContext(t.Context())
|
||||
|
||||
assert.Equal(t, data.ref, ghc.Ref)
|
||||
})
|
||||
@@ -1029,54 +1060,54 @@ func createJob(t *testing.T, input, result string) *model.Job {
|
||||
}
|
||||
|
||||
func TestRunContextRunsOnPlatformNames(t *testing.T) {
|
||||
log.SetLevel(log.DebugLevel)
|
||||
logging.SetLevel(logging.LevelDebug)
|
||||
assertObject := assert.New(t)
|
||||
|
||||
rc := createIfTestRunContext(map[string]*model.Job{
|
||||
"job1": createJob(t, `runs-on: ubuntu-latest`, ""),
|
||||
})
|
||||
assertObject.Equal([]string{"ubuntu-latest"}, rc.runsOnPlatformNames(context.Background()))
|
||||
assertObject.Equal([]string{"ubuntu-latest"}, rc.runsOnPlatformNames(t.Context()))
|
||||
|
||||
rc = createIfTestRunContext(map[string]*model.Job{
|
||||
"job1": createJob(t, `runs-on: ${{ 'ubuntu-latest' }}`, ""),
|
||||
})
|
||||
assertObject.Equal([]string{"ubuntu-latest"}, rc.runsOnPlatformNames(context.Background()))
|
||||
assertObject.Equal([]string{"ubuntu-latest"}, rc.runsOnPlatformNames(t.Context()))
|
||||
|
||||
rc = createIfTestRunContext(map[string]*model.Job{
|
||||
"job1": createJob(t, `runs-on: [self-hosted, my-runner]`, ""),
|
||||
})
|
||||
assertObject.Equal([]string{"self-hosted", "my-runner"}, rc.runsOnPlatformNames(context.Background()))
|
||||
assertObject.Equal([]string{"self-hosted", "my-runner"}, rc.runsOnPlatformNames(t.Context()))
|
||||
|
||||
rc = createIfTestRunContext(map[string]*model.Job{
|
||||
"job1": createJob(t, `runs-on: [self-hosted, "${{ 'my-runner' }}"]`, ""),
|
||||
})
|
||||
assertObject.Equal([]string{"self-hosted", "my-runner"}, rc.runsOnPlatformNames(context.Background()))
|
||||
assertObject.Equal([]string{"self-hosted", "my-runner"}, rc.runsOnPlatformNames(t.Context()))
|
||||
|
||||
rc = createIfTestRunContext(map[string]*model.Job{
|
||||
"job1": createJob(t, `runs-on: ${{ fromJSON('["ubuntu-latest"]') }}`, ""),
|
||||
})
|
||||
assertObject.Equal([]string{"ubuntu-latest"}, rc.runsOnPlatformNames(context.Background()))
|
||||
assertObject.Equal([]string{"ubuntu-latest"}, rc.runsOnPlatformNames(t.Context()))
|
||||
|
||||
// test missing / invalid runs-on
|
||||
rc = createIfTestRunContext(map[string]*model.Job{
|
||||
"job1": createJob(t, `name: something`, ""),
|
||||
})
|
||||
assertObject.Equal([]string{}, rc.runsOnPlatformNames(context.Background()))
|
||||
assertObject.Equal([]string{}, rc.runsOnPlatformNames(t.Context()))
|
||||
|
||||
rc = createIfTestRunContext(map[string]*model.Job{
|
||||
"job1": createJob(t, `runs-on:
|
||||
mapping: value`, ""),
|
||||
})
|
||||
assertObject.Equal([]string{}, rc.runsOnPlatformNames(context.Background()))
|
||||
assertObject.Equal([]string{}, rc.runsOnPlatformNames(t.Context()))
|
||||
|
||||
rc = createIfTestRunContext(map[string]*model.Job{
|
||||
"job1": createJob(t, `runs-on: ${{ invalid expression }}`, ""),
|
||||
})
|
||||
assertObject.Equal([]string{}, rc.runsOnPlatformNames(context.Background()))
|
||||
assertObject.Equal([]string{}, rc.runsOnPlatformNames(t.Context()))
|
||||
}
|
||||
|
||||
func TestRunContextIsEnabled(t *testing.T) {
|
||||
log.SetLevel(log.DebugLevel)
|
||||
logging.SetLevel(logging.LevelDebug)
|
||||
assertObject := assert.New(t)
|
||||
|
||||
// success()
|
||||
@@ -1086,8 +1117,8 @@ env:
|
||||
BROKEN: ${{ fromJSON('invalid') }}
|
||||
if: success()`, ""),
|
||||
})
|
||||
assertObject.True(rc.isEnabled(context.Background()))
|
||||
require.Error(t, evaluateJobEnvAndDefaults(context.Background(), rc))
|
||||
assertObject.True(rc.isEnabled(t.Context()))
|
||||
require.Error(t, evaluateJobEnvAndDefaults(t.Context(), rc))
|
||||
|
||||
rc = createIfTestRunContext(map[string]*model.Job{
|
||||
"job1": createJob(t, `runs-on: ubuntu-latest`, "failure"),
|
||||
@@ -1096,7 +1127,7 @@ needs: [job1]
|
||||
if: success()`, ""),
|
||||
})
|
||||
rc.Run.JobID = "job2"
|
||||
assertObject.False(rc.isEnabled(context.Background()))
|
||||
assertObject.False(rc.isEnabled(t.Context()))
|
||||
|
||||
rc = createIfTestRunContext(map[string]*model.Job{
|
||||
"job1": createJob(t, `runs-on: ubuntu-latest`, "success"),
|
||||
@@ -1105,7 +1136,7 @@ needs: [job1]
|
||||
if: success()`, ""),
|
||||
})
|
||||
rc.Run.JobID = "job2"
|
||||
assertObject.True(rc.isEnabled(context.Background()))
|
||||
assertObject.True(rc.isEnabled(t.Context()))
|
||||
|
||||
rc = createIfTestRunContext(map[string]*model.Job{
|
||||
"job1": createJob(t, `runs-on: ubuntu-latest`, "failure"),
|
||||
@@ -1113,14 +1144,14 @@ if: success()`, ""),
|
||||
if: success()`, ""),
|
||||
})
|
||||
rc.Run.JobID = "job2"
|
||||
assertObject.True(rc.isEnabled(context.Background()))
|
||||
assertObject.True(rc.isEnabled(t.Context()))
|
||||
|
||||
// failure()
|
||||
rc = createIfTestRunContext(map[string]*model.Job{
|
||||
"job1": createJob(t, `runs-on: ubuntu-latest
|
||||
if: failure()`, ""),
|
||||
})
|
||||
assertObject.False(rc.isEnabled(context.Background()))
|
||||
assertObject.False(rc.isEnabled(t.Context()))
|
||||
|
||||
rc = createIfTestRunContext(map[string]*model.Job{
|
||||
"job1": createJob(t, `runs-on: ubuntu-latest`, "failure"),
|
||||
@@ -1129,7 +1160,7 @@ needs: [job1]
|
||||
if: failure()`, ""),
|
||||
})
|
||||
rc.Run.JobID = "job2"
|
||||
assertObject.True(rc.isEnabled(context.Background()))
|
||||
assertObject.True(rc.isEnabled(t.Context()))
|
||||
|
||||
rc = createIfTestRunContext(map[string]*model.Job{
|
||||
"job1": createJob(t, `runs-on: ubuntu-latest`, "success"),
|
||||
@@ -1138,7 +1169,7 @@ needs: [job1]
|
||||
if: failure()`, ""),
|
||||
})
|
||||
rc.Run.JobID = "job2"
|
||||
assertObject.False(rc.isEnabled(context.Background()))
|
||||
assertObject.False(rc.isEnabled(t.Context()))
|
||||
|
||||
rc = createIfTestRunContext(map[string]*model.Job{
|
||||
"job1": createJob(t, `runs-on: ubuntu-latest`, "failure"),
|
||||
@@ -1146,14 +1177,14 @@ if: failure()`, ""),
|
||||
if: failure()`, ""),
|
||||
})
|
||||
rc.Run.JobID = "job2"
|
||||
assertObject.False(rc.isEnabled(context.Background()))
|
||||
assertObject.False(rc.isEnabled(t.Context()))
|
||||
|
||||
// always()
|
||||
rc = createIfTestRunContext(map[string]*model.Job{
|
||||
"job1": createJob(t, `runs-on: ubuntu-latest
|
||||
if: always()`, ""),
|
||||
})
|
||||
assertObject.True(rc.isEnabled(context.Background()))
|
||||
assertObject.True(rc.isEnabled(t.Context()))
|
||||
|
||||
rc = createIfTestRunContext(map[string]*model.Job{
|
||||
"job1": createJob(t, `runs-on: ubuntu-latest`, "failure"),
|
||||
@@ -1162,7 +1193,7 @@ needs: [job1]
|
||||
if: always()`, ""),
|
||||
})
|
||||
rc.Run.JobID = "job2"
|
||||
assertObject.True(rc.isEnabled(context.Background()))
|
||||
assertObject.True(rc.isEnabled(t.Context()))
|
||||
|
||||
rc = createIfTestRunContext(map[string]*model.Job{
|
||||
"job1": createJob(t, `runs-on: ubuntu-latest`, "success"),
|
||||
@@ -1171,7 +1202,7 @@ needs: [job1]
|
||||
if: always()`, ""),
|
||||
})
|
||||
rc.Run.JobID = "job2"
|
||||
assertObject.True(rc.isEnabled(context.Background()))
|
||||
assertObject.True(rc.isEnabled(t.Context()))
|
||||
|
||||
rc = createIfTestRunContext(map[string]*model.Job{
|
||||
"job1": createJob(t, `runs-on: ubuntu-latest`, "success"),
|
||||
@@ -1179,23 +1210,23 @@ if: always()`, ""),
|
||||
if: always()`, ""),
|
||||
})
|
||||
rc.Run.JobID = "job2"
|
||||
assertObject.True(rc.isEnabled(context.Background()))
|
||||
assertObject.True(rc.isEnabled(t.Context()))
|
||||
|
||||
rc = createIfTestRunContext(map[string]*model.Job{
|
||||
"job1": createJob(t, `uses: ./.github/workflows/reusable.yml`, ""),
|
||||
})
|
||||
assertObject.True(rc.isEnabled(context.Background()))
|
||||
assertObject.True(rc.isEnabled(t.Context()))
|
||||
|
||||
rc = createIfTestRunContext(map[string]*model.Job{
|
||||
"job1": createJob(t, `uses: ./.github/workflows/reusable.yml
|
||||
if: false`, ""),
|
||||
})
|
||||
assertObject.False(rc.isEnabled(context.Background()))
|
||||
assertObject.False(rc.isEnabled(t.Context()))
|
||||
|
||||
rc = createIfTestRunContext(map[string]*model.Job{
|
||||
"job1": createJob(t, `if: ${{ fromJSON('not-json') }}`, ""),
|
||||
})
|
||||
enabled, err := rc.isEnabled(context.Background())
|
||||
enabled, err := rc.isEnabled(t.Context())
|
||||
require.NoError(t, err)
|
||||
assertObject.False(enabled)
|
||||
}
|
||||
@@ -1275,12 +1306,9 @@ func TestCreateContainerNameBoundedForLongMatrixInput(t *testing.T) {
|
||||
|
||||
func TestPrintStartJobContainerGroupGolden(t *testing.T) {
|
||||
buf := &bytes.Buffer{}
|
||||
logger := log.New()
|
||||
logger.SetOutput(buf)
|
||||
logger.SetLevel(log.InfoLevel)
|
||||
logger.SetFormatter(&jobLogFormatter{color: cyan})
|
||||
entry := logger.WithFields(log.Fields{"job": "j1"})
|
||||
ctx := common.WithLogger(context.Background(), entry)
|
||||
logger := logging.New(logging.NewLevelHandler(newJobLogHandler(buf, cyan), logging.LevelInfo))
|
||||
entry := logger.With("job", "j1")
|
||||
ctx := common.WithLogger(t.Context(), entry)
|
||||
|
||||
printStartJobContainerGroup(ctx, "node:20", "GITEA-WORKFLOW-build-JOB-test", "gitea-runner-network")()
|
||||
|
||||
@@ -1371,7 +1399,7 @@ func TestRunContext_cleanupFailedStart(t *testing.T) {
|
||||
Run: &model.Run{JobID: "job", Workflow: workflow},
|
||||
platformImage: "node:20",
|
||||
}
|
||||
logger, hook := logrustest.NewNullLogger()
|
||||
logger, hook := logtest.NewLogger()
|
||||
ctx := common.WithLogger(common.WithDryrun(t.Context(), true), logger)
|
||||
rc.ExprEval = rc.NewExpressionEvaluator(ctx)
|
||||
err = rc.startContainer().Then(rc.stopContainer()).Finally(rc.closeContainer())(ctx)
|
||||
@@ -1392,7 +1420,7 @@ func TestRunContext_cleanupFailedStart(t *testing.T) {
|
||||
}
|
||||
assert.Equal(t, strings.Fields(want+" job.Remove service.Remove service.Close job.Close"), operations)
|
||||
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")
|
||||
}))
|
||||
})
|
||||
@@ -1415,7 +1443,7 @@ func TestWaitForServiceContainers(t *testing.T) {
|
||||
Return(&container.Info{ID: "id", State: "running", Health: container.HealthNone}, nil).Once()
|
||||
|
||||
rc := newRunContext(0, &serviceContainer{name: "redis", container: service})
|
||||
require.NoError(t, rc.waitForServiceContainers()(context.Background()))
|
||||
require.NoError(t, rc.waitForServiceContainers()(t.Context()))
|
||||
service.AssertExpectations(t)
|
||||
})
|
||||
|
||||
@@ -1432,9 +1460,8 @@ func TestWaitForServiceContainers(t *testing.T) {
|
||||
Return(&container.Info{ID: "id", State: "running", Health: container.HealthHealthy}, nil).Once()
|
||||
|
||||
var output bytes.Buffer
|
||||
logger := log.New()
|
||||
logger.SetOutput(&output)
|
||||
require.NoError(t, newRunContext(0, &serviceContainer{name: "postgres", container: service}).waitForServiceContainers()(common.WithLogger(t.Context(), logger.WithFields(nil))))
|
||||
logger := logging.New(logging.NewLevelHandler(newJobLogHandler(&output, cyan), logging.LevelInfo))
|
||||
require.NoError(t, newRunContext(0, &serviceContainer{name: "postgres", container: service}).waitForServiceContainers()(common.WithLogger(t.Context(), logger)))
|
||||
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.Contains(t, output.String(), "postgres service is healthy.")
|
||||
@@ -1451,7 +1478,7 @@ func TestWaitForServiceContainers(t *testing.T) {
|
||||
service.On("DumpLogs", mock.Anything).Return(nil).Once()
|
||||
|
||||
rc := newRunContext(0, &serviceContainer{name: "postgres", container: service})
|
||||
err := rc.waitForServiceContainers()(context.Background())
|
||||
err := rc.waitForServiceContainers()(t.Context())
|
||||
require.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "the service 'postgres' is unhealthy: connection refused")
|
||||
service.AssertExpectations(t)
|
||||
@@ -1463,7 +1490,7 @@ func TestWaitForServiceContainers(t *testing.T) {
|
||||
Return(&container.Info{State: "exited", ExitCode: 2, Health: container.HealthNone}, nil).Once()
|
||||
|
||||
rc := newRunContext(0, &serviceContainer{name: "postgres", container: service})
|
||||
require.NoError(t, rc.waitForServiceContainers()(context.Background()))
|
||||
require.NoError(t, rc.waitForServiceContainers()(t.Context()))
|
||||
})
|
||||
|
||||
t.Run("proceeds when the container is gone", func(t *testing.T) {
|
||||
@@ -1472,7 +1499,7 @@ func TestWaitForServiceContainers(t *testing.T) {
|
||||
Return((*container.Info)(nil), container.ErrContainerNotFound).Once()
|
||||
|
||||
rc := newRunContext(0, &serviceContainer{name: "postgres", container: service})
|
||||
require.NoError(t, rc.waitForServiceContainers()(context.Background()))
|
||||
require.NoError(t, rc.waitForServiceContainers()(t.Context()))
|
||||
})
|
||||
|
||||
t.Run("fails right away when one service fails while another is still starting", func(t *testing.T) {
|
||||
@@ -1506,7 +1533,7 @@ func TestWaitForServiceContainers(t *testing.T) {
|
||||
Return(&container.Info{State: "running", Health: container.HealthStarting}, nil)
|
||||
|
||||
rc := newRunContext(20*time.Millisecond, &serviceContainer{name: "postgres", container: service})
|
||||
err := rc.waitForServiceContainers()(context.Background())
|
||||
err := rc.waitForServiceContainers()(t.Context())
|
||||
require.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "did not become healthy within")
|
||||
})
|
||||
@@ -1518,7 +1545,7 @@ func TestWaitForServiceContainers(t *testing.T) {
|
||||
Return((*container.Info)(nil), errors.New("inspect aborted"))
|
||||
|
||||
rc := newRunContext(20*time.Millisecond, &serviceContainer{name: "postgres", container: service})
|
||||
err := rc.waitForServiceContainers()(context.Background())
|
||||
err := rc.waitForServiceContainers()(t.Context())
|
||||
require.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "did not become healthy within")
|
||||
})
|
||||
@@ -1531,7 +1558,7 @@ func TestWaitForServiceContainers(t *testing.T) {
|
||||
|
||||
svc := &serviceContainer{name: "postgres", container: service}
|
||||
rc := newRunContext(-1, svc)
|
||||
require.NoError(t, rc.waitForServiceContainers()(context.Background()))
|
||||
require.NoError(t, rc.waitForServiceContainers()(t.Context()))
|
||||
service.AssertExpectations(t)
|
||||
assert.Equal(t, "id", svc.info.ID)
|
||||
})
|
||||
@@ -1541,13 +1568,13 @@ func TestWaitForServiceContainers(t *testing.T) {
|
||||
service.On("Inspect", mock.Anything).Return((*container.Info)(nil), errors.New("daemon is gone")).Once()
|
||||
|
||||
rc := newRunContext(-1, &serviceContainer{name: "postgres", container: service})
|
||||
err := rc.waitForServiceContainers()(context.Background())
|
||||
err := rc.waitForServiceContainers()(t.Context())
|
||||
require.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "failed to inspect service 'postgres'")
|
||||
})
|
||||
|
||||
t.Run("is a no-op without services", func(t *testing.T) {
|
||||
require.NoError(t, newRunContext(0).waitForServiceContainers()(context.Background()))
|
||||
require.NoError(t, newRunContext(0).waitForServiceContainers()(t.Context()))
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1563,7 +1590,7 @@ func TestReportUnstartedServices(t *testing.T) {
|
||||
{name: "redis", container: running},
|
||||
}}
|
||||
|
||||
require.NoError(t, rc.reportUnstartedServices()(context.Background()))
|
||||
require.NoError(t, rc.reportUnstartedServices()(t.Context()))
|
||||
dead.AssertExpectations(t)
|
||||
running.AssertExpectations(t)
|
||||
}
|
||||
@@ -1603,14 +1630,14 @@ func TestCaptureJobContainerInfoExportsDockerWorkspace(t *testing.T) {
|
||||
JobContainer: job,
|
||||
}
|
||||
|
||||
require.NoError(t, rc.captureJobContainerInfo()(context.Background()))
|
||||
require.NoError(t, rc.captureJobContainerInfo()(t.Context()))
|
||||
|
||||
assert.Equal(t, "job-container-id", rc.jobContainerID)
|
||||
assert.Equal(t, "/var/lib/docker/volumes/job/_data/owner/repo", rc.Env["GITEA_DOCKER_WORKSPACE"])
|
||||
|
||||
rc.Config.Workdir = "/elsewhere"
|
||||
rc.Env = map[string]string{}
|
||||
require.NoError(t, rc.captureJobContainerInfo()(context.Background()))
|
||||
require.NoError(t, rc.captureJobContainerInfo()(t.Context()))
|
||||
assert.NotContains(t, rc.Env, "GITEA_DOCKER_WORKSPACE")
|
||||
}
|
||||
|
||||
@@ -1687,7 +1714,7 @@ func TestRunContextContainerImageCannotSelectBackend(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestRunContextImageOS(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
ctx := t.Context()
|
||||
|
||||
t.Run("prefers the release in the resolved image tag", func(t *testing.T) {
|
||||
rc := createRunsOnRunContext(t, "ubuntu-latest")
|
||||
@@ -1714,7 +1741,7 @@ func TestRunContextImageOS(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestRunContextGetRunnerContext(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
ctx := t.Context()
|
||||
|
||||
t.Run("adds the runner values the container cannot know", func(t *testing.T) {
|
||||
rc := createRunsOnRunContext(t, "ubuntu-latest")
|
||||
@@ -1766,8 +1793,7 @@ func TestRunContextUpdateExtraPath(t *testing.T) {
|
||||
{name: "over 16 MiB", content: []byte(strings.Repeat("x", 16*1024*1024+1)), wantErr: true},
|
||||
} {
|
||||
t.Run(testcase.name, func(t *testing.T) {
|
||||
logger := log.New()
|
||||
logger.SetOutput(io.Discard)
|
||||
logger := logging.New(logging.DiscardHandler)
|
||||
ctx := common.WithLogger(t.Context(), logger)
|
||||
jobContainer := &containerMock{}
|
||||
jobContainer.On("GetContainerArchive", mock.Anything, "/github/path").
|
||||
@@ -1795,7 +1821,7 @@ func TestParentDir(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestRunContextWithGithubEnvRunnerValues(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
ctx := t.Context()
|
||||
rc := createRunsOnRunContext(t, "ubuntu-latest")
|
||||
rc.Config.RunnerName = "runner-1"
|
||||
rc.Config.Secrets = map[string]string{"ACTIONS_STEP_DEBUG": "true"}
|
||||
|
||||
+47
-52
@@ -5,6 +5,7 @@
|
||||
package runner
|
||||
|
||||
import (
|
||||
"cmp"
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
@@ -15,12 +16,12 @@ import (
|
||||
"time"
|
||||
|
||||
"gitea.com/gitea/runner/act/common"
|
||||
"gitea.com/gitea/runner/act/common/logging"
|
||||
"gitea.com/gitea/runner/act/container"
|
||||
"gitea.com/gitea/runner/act/ghcontext"
|
||||
|
||||
"gitea.dev/actionslib/pkg/model"
|
||||
docker_container "github.com/moby/moby/api/types/container"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"go.yaml.in/yaml/v4"
|
||||
)
|
||||
|
||||
@@ -49,7 +50,7 @@ type Config struct {
|
||||
ContainerDaemonSocket string // Path to Docker daemon socket
|
||||
ContainerOptions string // Options for the job container
|
||||
UseGitIgnore bool // controls if paths in .gitignore should not be copied into container, default true
|
||||
GitHubInstance string // GitHub instance to use, default "github.com"
|
||||
GitHubInstance string // instance hostname or URL, empty when unknown
|
||||
ContainerCapAdd []string // list of kernel capabilities to add to the containers
|
||||
ContainerCapDrop []string // list of kernel capabilities to remove from the containers
|
||||
ArtifactServerPath string // the path where the artifact server stores uploads
|
||||
@@ -79,18 +80,18 @@ type Config struct {
|
||||
PlatformPicker func(labels []string) string
|
||||
KubernetesPicker func(labels []string) container.KubernetesOptions
|
||||
InstanceTransport http.RoundTripper
|
||||
JobLoggerLevel *log.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
|
||||
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
|
||||
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)
|
||||
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)
|
||||
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
|
||||
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
|
||||
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
|
||||
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
|
||||
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)
|
||||
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)
|
||||
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
|
||||
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
|
||||
}
|
||||
|
||||
// RunnerDebug reports whether debug logging is on, exposed as `runner.debug` and
|
||||
@@ -111,13 +112,7 @@ func (c Config) GetToken() string {
|
||||
|
||||
// DefaultActionURL returns the host used for implicit remote actions.
|
||||
func (c Config) DefaultActionURL() string {
|
||||
if c.DefaultActionInstance != "" {
|
||||
return c.DefaultActionInstance
|
||||
}
|
||||
if c.GitHubInstance != "" {
|
||||
return c.GitHubInstance
|
||||
}
|
||||
return "github.com"
|
||||
return cmp.Or(c.DefaultActionInstance, c.GitHubInstance, "github.com")
|
||||
}
|
||||
|
||||
type caller struct {
|
||||
@@ -154,7 +149,7 @@ func (runner *runnerImpl) configure() (*runnerImpl, error) {
|
||||
if runner.config.EventJSON != "" {
|
||||
runner.eventJSON = runner.config.EventJSON
|
||||
} else if runner.config.EventPath != "" {
|
||||
log.Debugf("Reading event.json from %s", runner.config.EventPath)
|
||||
logging.Debugf("Reading event.json from %s", runner.config.EventPath)
|
||||
eventJSONBytes, err := os.ReadFile(runner.config.EventPath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -168,7 +163,7 @@ func maxParallelFor(strategy *model.Strategy, combinations int) int {
|
||||
maxParallel := 4 // actionslib has no default for an undeclared max-parallel
|
||||
if strategy != nil {
|
||||
if limit, err := strategy.MaxParallel(); err != nil {
|
||||
log.Errorf("Ignoring invalid max-parallel: %v", err)
|
||||
logging.Errorf("Ignoring invalid max-parallel: %v", err)
|
||||
} else if limit > 0 {
|
||||
maxParallel = limit
|
||||
}
|
||||
@@ -190,36 +185,36 @@ func (runner *runnerImpl) NewPlanExecutor(plan *model.Plan) common.Executor {
|
||||
maxJobNameLen := 0
|
||||
|
||||
stagePipeline := make([]common.Executor, 0)
|
||||
log.Debugf("Plan Stages: %v", plan.Stages)
|
||||
logging.Debugf("Plan Stages: %v", plan.Stages)
|
||||
|
||||
for i := range plan.Stages {
|
||||
stage := plan.Stages[i]
|
||||
stagePipeline = append(stagePipeline, func(ctx context.Context) error {
|
||||
pipeline := make([]common.Executor, 0)
|
||||
for _, run := range stage.Runs {
|
||||
log.Debugf("Stages Runs: %v", stage.Runs)
|
||||
logging.Debugf("Stages Runs: %v", stage.Runs)
|
||||
stageExecutor := make([]common.Executor, 0)
|
||||
job := run.Job()
|
||||
log.Debugf("Job.Name: %v", job.Name)
|
||||
log.Debugf("Job.RawNeeds: %v", job.RawNeeds)
|
||||
log.Debugf("Job.RawRunsOn: %v", job.RawRunsOn)
|
||||
log.Debugf("Job.Env: %v", job.Env)
|
||||
log.Debugf("Job.If: %v", job.If)
|
||||
logging.Debugf("Job.Name: %v", job.Name)
|
||||
logging.Debugf("Job.RawNeeds: %v", job.RawNeeds)
|
||||
logging.Debugf("Job.RawRunsOn: %v", job.RawRunsOn)
|
||||
logging.Debugf("Job.Env: %v", job.Env)
|
||||
logging.Debugf("Job.If: %v", job.If)
|
||||
for step := range job.Steps {
|
||||
if nil != job.Steps[step] {
|
||||
log.Debugf("Job.Steps: %v", job.Steps[step].String())
|
||||
logging.Debugf("Job.Steps: %v", job.Steps[step].String())
|
||||
}
|
||||
}
|
||||
log.Debugf("Job.TimeoutMinutes: %v", job.TimeoutMinutes)
|
||||
log.Debugf("Job.Services: %v", job.Services)
|
||||
log.Debugf("Job.Strategy: %v", job.Strategy)
|
||||
log.Debugf("Job.RawContainer: %v", job.RawContainer)
|
||||
log.Debugf("Job.Defaults.Run.Shell: %v", job.Defaults.Run.Shell)
|
||||
log.Debugf("Job.Defaults.Run.WorkingDirectory: %v", job.Defaults.Run.WorkingDirectory)
|
||||
log.Debugf("Job.Outputs: %v", job.Outputs)
|
||||
log.Debugf("Job.Uses: %v", job.Uses)
|
||||
log.Debugf("Job.With: %v", job.With)
|
||||
log.Debugf("Job.Result: %v", job.Result)
|
||||
logging.Debugf("Job.TimeoutMinutes: %v", job.TimeoutMinutes)
|
||||
logging.Debugf("Job.Services: %v", job.Services)
|
||||
logging.Debugf("Job.Strategy: %v", job.Strategy)
|
||||
logging.Debugf("Job.RawContainer: %v", job.RawContainer)
|
||||
logging.Debugf("Job.Defaults.Run.Shell: %v", job.Defaults.Run.Shell)
|
||||
logging.Debugf("Job.Defaults.Run.WorkingDirectory: %v", job.Defaults.Run.WorkingDirectory)
|
||||
logging.Debugf("Job.Outputs: %v", job.Outputs)
|
||||
logging.Debugf("Job.Uses: %v", job.Uses)
|
||||
logging.Debugf("Job.With: %v", job.With)
|
||||
logging.Debugf("Job.Result: %v", job.Result)
|
||||
|
||||
if job.Strategy != nil || job.RawStrategy.Kind == yaml.ScalarNode {
|
||||
strategyRc, err := runner.newRunContext(ctx, run, nil)
|
||||
@@ -231,13 +226,13 @@ func (runner *runnerImpl) NewPlanExecutor(plan *model.Plan) common.Executor {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
log.Debugf("Job.Strategy.FailFast: %v", job.Strategy.GetFailFast())
|
||||
log.Debugf("Job.Strategy.FailFastString: %v", job.Strategy.FailFastString)
|
||||
log.Debugf("Job.Strategy.MaxParallelString: %v", job.Strategy.MaxParallelString)
|
||||
log.Debugf("Job.Strategy.RawMatrix: %v", job.Strategy.RawMatrix)
|
||||
logging.Debugf("Job.Strategy.FailFast: %v", job.Strategy.GetFailFast())
|
||||
logging.Debugf("Job.Strategy.FailFastString: %v", job.Strategy.FailFastString)
|
||||
logging.Debugf("Job.Strategy.MaxParallelString: %v", job.Strategy.MaxParallelString)
|
||||
logging.Debugf("Job.Strategy.RawMatrix: %v", job.Strategy.RawMatrix)
|
||||
// An unevaluated expression is left in place, which GetMatrixes below rejects.
|
||||
if err := strategyRc.NewExpressionEvaluator(ctx).EvaluateYamlNode(ctx, &job.Strategy.RawMatrix); err != nil {
|
||||
log.Errorf("Error while evaluating matrix: %v", err)
|
||||
logging.Errorf("Error while evaluating matrix: %v", err)
|
||||
}
|
||||
for _, value := range []*string{&job.Strategy.FailFastString, &job.Strategy.MaxParallelString} {
|
||||
if evaluated, err := strategyRc.ExprEval.Interpolate(ctx, *value); err == nil {
|
||||
@@ -251,11 +246,11 @@ func (runner *runnerImpl) NewPlanExecutor(plan *model.Plan) common.Executor {
|
||||
if err != nil {
|
||||
return fmt.Errorf("could not get job matrix: %w", err)
|
||||
}
|
||||
log.Debugf("Job Matrices: %v", matrixes)
|
||||
logging.Debugf("Job Matrices: %v", matrixes)
|
||||
|
||||
maxParallel := maxParallelFor(job.Strategy, len(matrixes))
|
||||
|
||||
log.Infof("Running job with maxParallel=%d for %d matrix combinations", maxParallel, len(matrixes))
|
||||
logging.Infof("Running job with maxParallel=%d for %d matrix combinations", maxParallel, len(matrixes))
|
||||
|
||||
for i, matrix := range matrixes {
|
||||
index, total := i, len(matrixes)
|
||||
@@ -314,7 +309,7 @@ func (runner *runnerImpl) NewPlanExecutor(plan *model.Plan) common.Executor {
|
||||
if len(pipeline) == 1 {
|
||||
// Single run/job: execute directly without additional parallelization wrapper
|
||||
// This ensures max-parallel is the only limiting factor
|
||||
log.Debugf("Single pipeline element, executing directly")
|
||||
logging.Debugf("Single pipeline element, executing directly")
|
||||
return pipeline[0](ctx)
|
||||
}
|
||||
|
||||
@@ -324,9 +319,9 @@ func (runner *runnerImpl) NewPlanExecutor(plan *model.Plan) common.Executor {
|
||||
// If MaxParallel is set in config, use it
|
||||
if runner.config.MaxParallel > 0 {
|
||||
parallelism = runner.config.MaxParallel
|
||||
log.Debugf("Using configured max-parallel: %d", parallelism)
|
||||
logging.Debugf("Using configured max-parallel: %d", parallelism)
|
||||
} else {
|
||||
log.Debugf("Using CPU count for parallelism: %d", parallelism)
|
||||
logging.Debugf("Using CPU count for parallelism: %d", parallelism)
|
||||
}
|
||||
|
||||
// Don't exceed the number of pipeline elements
|
||||
@@ -334,7 +329,7 @@ func (runner *runnerImpl) NewPlanExecutor(plan *model.Plan) common.Executor {
|
||||
parallelism = len(pipeline)
|
||||
}
|
||||
|
||||
log.Infof("Executing %d pipeline elements with parallelism %d", len(pipeline), parallelism)
|
||||
logging.Infof("Executing %d pipeline elements with parallelism %d", len(pipeline), parallelism)
|
||||
return common.NewParallelExecutor(parallelism, pipeline...)(ctx)
|
||||
})
|
||||
}
|
||||
|
||||
+28
-28
@@ -17,10 +17,10 @@ import (
|
||||
"time"
|
||||
|
||||
"gitea.com/gitea/runner/act/common"
|
||||
"gitea.com/gitea/runner/act/common/logging"
|
||||
|
||||
"gitea.dev/actionslib/pkg/model"
|
||||
"github.com/joho/godotenv"
|
||||
log "github.com/sirupsen/logrus"
|
||||
assert "github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
@@ -28,7 +28,7 @@ import (
|
||||
var (
|
||||
baseImage = "node:24-bookworm-slim"
|
||||
platforms map[string]string
|
||||
logLevel = log.DebugLevel
|
||||
logLevel = logging.LevelDebug
|
||||
workdir = "testdata"
|
||||
secrets map[string]string
|
||||
testRunID = strings.ToLower(rand.Text()[:8]) // keeps docker names apart from concurrent test processes on one daemon
|
||||
@@ -55,7 +55,7 @@ func init() {
|
||||
}
|
||||
|
||||
if l := os.Getenv("ACT_TEST_LOG_LEVEL"); l != "" {
|
||||
if lvl, err := log.ParseLevel(l); err == nil {
|
||||
if lvl, err := logging.ParseLevel(l); err == nil {
|
||||
logLevel = lvl
|
||||
}
|
||||
}
|
||||
@@ -71,10 +71,11 @@ func TestNoWorkflowsFoundByPlanner(t *testing.T) {
|
||||
planner, err := model.NewWorkflowPlanner("res", true)
|
||||
assert.NoError(t, err) //nolint:testifylint // pre-existing issue from nektos/act
|
||||
|
||||
out := log.StandardLogger().Out
|
||||
out := logging.Output()
|
||||
t.Cleanup(func() { logging.SetOutput(out) })
|
||||
var buf bytes.Buffer
|
||||
log.SetOutput(&buf)
|
||||
log.SetLevel(log.DebugLevel)
|
||||
logging.SetOutput(&buf)
|
||||
logging.SetLevel(logging.LevelDebug)
|
||||
plan, err := planner.PlanEvent("pull_request")
|
||||
assert.NotNil(t, plan)
|
||||
assert.NoError(t, err) //nolint:testifylint // pre-existing issue from nektos/act
|
||||
@@ -84,25 +85,24 @@ func TestNoWorkflowsFoundByPlanner(t *testing.T) {
|
||||
assert.NotNil(t, plan)
|
||||
assert.NoError(t, err) //nolint:testifylint // pre-existing issue from nektos/act
|
||||
assert.Contains(t, buf.String(), "no workflows found by planner")
|
||||
log.SetOutput(out)
|
||||
}
|
||||
|
||||
func TestGraphMissingEvent(t *testing.T) {
|
||||
planner, err := model.NewWorkflowPlanner("testdata/issue-1595/no-event.yml", true)
|
||||
assert.NoError(t, err) //nolint:testifylint // pre-existing issue from nektos/act
|
||||
|
||||
out := log.StandardLogger().Out
|
||||
out := logging.Output()
|
||||
t.Cleanup(func() { logging.SetOutput(out) })
|
||||
var buf bytes.Buffer
|
||||
log.SetOutput(&buf)
|
||||
log.SetLevel(log.DebugLevel)
|
||||
logging.SetOutput(&buf)
|
||||
logging.SetLevel(logging.LevelDebug)
|
||||
|
||||
plan, err := planner.PlanEvent("push")
|
||||
assert.NoError(t, err) //nolint:testifylint // pre-existing issue from nektos/act
|
||||
assert.NotNil(t, plan)
|
||||
assert.Empty(t, plan.Stages)
|
||||
|
||||
assert.Contains(t, buf.String(), "no events found for workflow: no-event.yml")
|
||||
log.SetOutput(out)
|
||||
assert.Contains(t, buf.String(), `msg="no events found for workflow" file=no-event.yml`)
|
||||
}
|
||||
|
||||
func TestGraphMissingFirst(t *testing.T) {
|
||||
@@ -119,29 +119,30 @@ func TestGraphWithMissing(t *testing.T) {
|
||||
planner, err := model.NewWorkflowPlanner("testdata/issue-1595/missing.yml", true)
|
||||
assert.NoError(t, err) //nolint:testifylint // pre-existing issue from nektos/act
|
||||
|
||||
out := log.StandardLogger().Out
|
||||
out := logging.Output()
|
||||
t.Cleanup(func() { logging.SetOutput(out) })
|
||||
var buf bytes.Buffer
|
||||
log.SetOutput(&buf)
|
||||
log.SetLevel(log.DebugLevel)
|
||||
logging.SetOutput(&buf)
|
||||
logging.SetLevel(logging.LevelDebug)
|
||||
|
||||
plan, err := planner.PlanEvent("push")
|
||||
assert.NotNil(t, plan)
|
||||
assert.Empty(t, plan.Stages)
|
||||
assert.EqualError(t, err, "unable to build dependency graph for missing (missing.yml)") //nolint:testifylint // pre-existing issue from nektos/act
|
||||
assert.Contains(t, buf.String(), "unable to build dependency graph for missing (missing.yml)")
|
||||
log.SetOutput(out)
|
||||
}
|
||||
|
||||
func TestGraphWithSomeMissing(t *testing.T) {
|
||||
log.SetLevel(log.DebugLevel)
|
||||
logging.SetLevel(logging.LevelDebug)
|
||||
|
||||
planner, err := model.NewWorkflowPlanner("testdata/issue-1595/", true)
|
||||
assert.NoError(t, err) //nolint:testifylint // pre-existing issue from nektos/act
|
||||
|
||||
out := log.StandardLogger().Out
|
||||
out := logging.Output()
|
||||
t.Cleanup(func() { logging.SetOutput(out) })
|
||||
var buf bytes.Buffer
|
||||
log.SetOutput(&buf)
|
||||
log.SetLevel(log.DebugLevel)
|
||||
logging.SetOutput(&buf)
|
||||
logging.SetLevel(logging.LevelDebug)
|
||||
|
||||
plan, err := planner.PlanAll()
|
||||
assert.Error(t, err, "unable to build dependency graph for no first (no-first.yml)") //nolint:testifylint // pre-existing issue from nektos/act
|
||||
@@ -149,7 +150,6 @@ func TestGraphWithSomeMissing(t *testing.T) {
|
||||
assert.Len(t, plan.Stages, 1)
|
||||
assert.Contains(t, buf.String(), "unable to build dependency graph for missing (missing.yml)")
|
||||
assert.Contains(t, buf.String(), "unable to build dependency graph for no first (no-first.yml)")
|
||||
log.SetOutput(out)
|
||||
}
|
||||
|
||||
func TestNewPlanExecutorRequiresRunsOnBeforeRunningAnyJob(t *testing.T) {
|
||||
@@ -221,7 +221,7 @@ type TestJobFileInfo struct {
|
||||
func (j *TestJobFileInfo) runTest(ctx context.Context, t *testing.T, cfg *Config) {
|
||||
fmt.Printf("::group::%s\n", j.workflowPath) //nolint:forbidigo // pre-existing issue from nektos/act
|
||||
|
||||
log.SetLevel(logLevel)
|
||||
logging.SetLevel(logLevel)
|
||||
|
||||
workdir, err := filepath.Abs(j.workdir)
|
||||
assert.NoError(t, err, workdir) //nolint:testifylint // pre-existing issue from nektos/act
|
||||
@@ -281,7 +281,7 @@ func TestRunEvent(t *testing.T) {
|
||||
requireDocker(t)
|
||||
t.Parallel()
|
||||
|
||||
ctx := context.Background()
|
||||
ctx := t.Context()
|
||||
|
||||
tables := []TestJobFileInfo{
|
||||
// Shells
|
||||
@@ -384,7 +384,7 @@ func TestRunEventHostEnvironment(t *testing.T) {
|
||||
// nix-specific cases gate on their own tool below so a missing pwsh/nix skips just those.
|
||||
requireHostTools(t, "bash", "node")
|
||||
|
||||
ctx := context.Background()
|
||||
ctx := t.Context()
|
||||
|
||||
tables := []TestJobFileInfo{}
|
||||
|
||||
@@ -482,7 +482,7 @@ func TestRunEventHostEnvironment(t *testing.T) {
|
||||
func TestDryrunEvent(t *testing.T) {
|
||||
t.Parallel()
|
||||
// Dryrun plans without containers or network (shells and local actions only).
|
||||
ctx := common.WithDryrun(context.Background(), true)
|
||||
ctx := common.WithDryrun(t.Context(), true)
|
||||
|
||||
tables := []TestJobFileInfo{
|
||||
// Shells
|
||||
@@ -513,7 +513,7 @@ func TestReusableWorkflowCaller(t *testing.T) {
|
||||
requireDocker(t)
|
||||
t.Parallel()
|
||||
table := TestJobFileInfo{workdir, "uses-workflow", "push", "", platforms, map[string]string{"secret": "keep_it_private"}}
|
||||
table.runTest(context.Background(), t, &Config{Secrets: table.secrets})
|
||||
table.runTest(t.Context(), t, &Config{Secrets: table.secrets})
|
||||
}
|
||||
|
||||
func TestRunEventSecrets(t *testing.T) {
|
||||
@@ -534,7 +534,7 @@ func TestRunEventSecrets(t *testing.T) {
|
||||
secrets, _ := godotenv.Read(filepath.Join(workdir, workflowPath, ".secrets"))
|
||||
assert.NoError(t, err, "Failed to read .secrets") //nolint:testifylint // pre-existing issue from nektos/act
|
||||
|
||||
tjfi.runTest(context.Background(), t, &Config{Secrets: secrets, Env: env})
|
||||
tjfi.runTest(t.Context(), t, &Config{Secrets: secrets, Env: env})
|
||||
}
|
||||
|
||||
func TestRunEventPullRequest(t *testing.T) {
|
||||
@@ -551,5 +551,5 @@ func TestRunEventPullRequest(t *testing.T) {
|
||||
platforms: platforms,
|
||||
}
|
||||
|
||||
tjfi.runTest(context.Background(), t, &Config{EventPath: filepath.Join(workdir, workflowPath, "event.json")})
|
||||
tjfi.runTest(t.Context(), t, &Config{EventPath: filepath.Join(workdir, workflowPath, "event.json")})
|
||||
}
|
||||
+22
-15
@@ -47,11 +47,11 @@ const maxSymlinkDepth = 10
|
||||
func (s stepStage) String() string {
|
||||
switch s {
|
||||
case stepStagePre:
|
||||
return "Pre"
|
||||
return StagePre
|
||||
case stepStageMain:
|
||||
return "Main"
|
||||
return StageMain
|
||||
case stepStagePost:
|
||||
return "Post"
|
||||
return StagePost
|
||||
}
|
||||
return "Unknown"
|
||||
}
|
||||
@@ -97,14 +97,14 @@ func runStepExecutor(step step, stage stepStage, executor common.Executor) commo
|
||||
if err != nil {
|
||||
stepResult.Conclusion = 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
|
||||
}
|
||||
|
||||
if !runStep {
|
||||
stepResult.Conclusion = 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
|
||||
}
|
||||
|
||||
@@ -112,12 +112,13 @@ func runStepExecutor(step step, stage stepStage, executor common.Executor) commo
|
||||
if strings.Contains(stepString, "::add-mask::") {
|
||||
stepString = "add-mask command"
|
||||
}
|
||||
startLogger := logger.With(StepStartedField, true)
|
||||
if stage == stepStageMain {
|
||||
// Main steps print their own raw "Run <title>" header, so this line is redundant and
|
||||
// only leaks into the "Set up job" section for the first step; keep it as a debug trace.
|
||||
logger.Debugf("Run %s %s", stage, stepString)
|
||||
startLogger.Debugf("Run %s %s", stage, stepString)
|
||||
} else {
|
||||
logger.Infof("Run %s %s", stage, stepString)
|
||||
startLogger.Infof("Run %s %s", stage, stepString)
|
||||
}
|
||||
|
||||
// Prepare and clean Runner File Commands
|
||||
@@ -186,29 +187,35 @@ func runStepExecutor(step step, stage stepStage, executor common.Executor) commo
|
||||
if err == nil {
|
||||
err = insecureErr
|
||||
}
|
||||
if fileErr := processRunnerEnvFileCommand(ctx, envFileCommand, rc, rc.setEnvFile); fileErr != nil && err == nil {
|
||||
fileCommandCtx := ctx
|
||||
if ctx.Err() != nil { // an interrupted step still records its file commands, a live deadline stays in force
|
||||
var cancelFileCommands context.CancelFunc
|
||||
fileCommandCtx, cancelFileCommands = context.WithTimeout(context.WithoutCancel(ctx), jobCleanupTimeout)
|
||||
defer cancelFileCommands()
|
||||
}
|
||||
if fileErr := processRunnerEnvFileCommand(fileCommandCtx, envFileCommand, rc, rc.setEnvFile); fileErr != nil && err == nil {
|
||||
err = fileErr
|
||||
}
|
||||
if fileErr := processRunnerEnvFileCommand(ctx, stateFileCommand, rc, rc.saveState); fileErr != nil && err == nil {
|
||||
if fileErr := processRunnerEnvFileCommand(fileCommandCtx, stateFileCommand, rc, rc.saveState); fileErr != nil && err == nil {
|
||||
err = fileErr
|
||||
}
|
||||
if fileErr := processRunnerEnvFileCommand(ctx, outputFileCommand, rc, rc.setOutput); fileErr != nil && err == nil {
|
||||
if fileErr := processRunnerEnvFileCommand(fileCommandCtx, outputFileCommand, rc, rc.setOutput); fileErr != nil && err == nil {
|
||||
err = fileErr
|
||||
}
|
||||
if fileErr := rc.UpdateExtraPath(ctx, path.Join(actPath, pathFileCommand)); fileErr != nil && err == nil {
|
||||
if fileErr := rc.UpdateExtraPath(fileCommandCtx, path.Join(actPath, pathFileCommand)); fileErr != nil && err == nil {
|
||||
err = fileErr
|
||||
}
|
||||
_ = rc.JobContainer.Copy(actPath, files...)(ctx)
|
||||
_ = rc.JobContainer.Copy(actPath, files...)(fileCommandCtx)
|
||||
|
||||
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 {
|
||||
stepResult.Outcome = model.StepStatusFailure
|
||||
|
||||
continueOnError, parseErr := isContinueOnError(ctx, stepModel.RawContinueOnError, step, stage)
|
||||
if parseErr != nil {
|
||||
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
|
||||
}
|
||||
|
||||
@@ -223,7 +230,7 @@ func runStepExecutor(step step, stage stepStage, executor common.Executor) commo
|
||||
|
||||
// Infof: Errorf entries are promoted to the user log by the reporter,
|
||||
// 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
|
||||
}
|
||||
|
||||
@@ -34,7 +34,7 @@ func (sab *stepActionBuiltin) main() common.Executor {
|
||||
}
|
||||
|
||||
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::")
|
||||
|
||||
rc := sab.RunContext
|
||||
|
||||
@@ -43,7 +43,7 @@ func (sal *stepActionLocal) main() common.Executor {
|
||||
}
|
||||
|
||||
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::")
|
||||
|
||||
actionDir := filepath.Join(sal.getRunContext().Config.Workdir, sal.Step.Uses)
|
||||
|
||||
@@ -35,7 +35,7 @@ func (salm *stepActionLocalMocks) readAction(_ context.Context, step *model.Step
|
||||
}
|
||||
|
||||
func TestStepActionLocalTest(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
ctx := t.Context()
|
||||
|
||||
cm := &containerMock{}
|
||||
salm := &stepActionLocalMocks{}
|
||||
@@ -225,7 +225,7 @@ func TestStepActionLocalPost(t *testing.T) {
|
||||
|
||||
for _, tt := range table {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
ctx := t.Context()
|
||||
|
||||
cm := &containerMock{}
|
||||
|
||||
|
||||
@@ -88,6 +88,12 @@ func (sar *stepActionRemote) prepareActionExecutor() common.Executor {
|
||||
}
|
||||
}
|
||||
|
||||
unlock, err := git.AcquireCloneLock(ctx, actionDir)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer unlock()
|
||||
|
||||
// Best effort: the download report falls back to the ref alone when the commit is unknown.
|
||||
if _, sha, err := git.FindGitRevision(ctx, actionDir); err != nil {
|
||||
common.Logger(ctx).Debugf("unable to resolve the commit of %s: %v", sar.remoteAction.Reference(), err)
|
||||
@@ -100,7 +106,6 @@ func (sar *stepActionRemote) prepareActionExecutor() common.Executor {
|
||||
return f, f, err
|
||||
}
|
||||
|
||||
defer git.AcquireCloneLock(actionDir)()
|
||||
actionModel, err := sar.readAction(ctx, sar.Step, actionDir, sar.remoteAction.Path, remoteReader, os.WriteFile)
|
||||
sar.action = actionModel
|
||||
return err
|
||||
@@ -129,7 +134,7 @@ func (sar *stepActionRemote) main() common.Executor {
|
||||
sar.prepareActionExecutor(),
|
||||
runStepExecutor(sar, stepStageMain, func(ctx context.Context) error {
|
||||
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::")
|
||||
|
||||
github := sar.getGithubContext(ctx)
|
||||
@@ -251,8 +256,8 @@ func (ra *remoteAction) CloneURL(u string) string {
|
||||
if ra.URL == "" {
|
||||
// keep an absolute local path as-is (used by tests to resolve actions from a local
|
||||
// repo); only bare host names get the https:// scheme prepended
|
||||
if !strings.HasPrefix(u, "http://") && !strings.HasPrefix(u, "https://") && !filepath.IsAbs(u) {
|
||||
u = "https://" + u
|
||||
if !filepath.IsAbs(u) {
|
||||
u = withHTTPS(u)
|
||||
}
|
||||
} else {
|
||||
u = ra.URL
|
||||
@@ -301,6 +306,9 @@ func newRemoteAction(action string, github *model.GithubContext, instanceURL str
|
||||
}
|
||||
ra.Org, ra.Repo, _ = strings.Cut(repo, "/")
|
||||
ra.URL, ra.Ref = github.ServerURL, ref
|
||||
if ra.URL == "" {
|
||||
return nil, fmt.Errorf("unable to resolve %q without a Gitea instance", action)
|
||||
}
|
||||
if ra.Org == "" || ra.Repo == "" || ra.Ref == "" {
|
||||
return nil, fmt.Errorf("unable to resolve %q without a repository and commit", action)
|
||||
}
|
||||
|
||||
@@ -175,7 +175,7 @@ func TestStepActionRemote(t *testing.T) {
|
||||
|
||||
for _, tt := range table {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
ctx := t.Context()
|
||||
|
||||
cm := &containerMock{}
|
||||
sarm := &stepActionRemoteMocks{}
|
||||
@@ -193,7 +193,7 @@ func TestStepActionRemote(t *testing.T) {
|
||||
RunContext: &RunContext{
|
||||
Config: &Config{
|
||||
GitHubInstance: "github.com",
|
||||
ActionCacheDir: "/tmp/test-cache",
|
||||
ActionCacheDir: t.TempDir(),
|
||||
},
|
||||
Run: &model.Run{
|
||||
JobID: "1",
|
||||
@@ -438,7 +438,7 @@ func TestStepActionRemotePost(t *testing.T) {
|
||||
|
||||
for _, tt := range table {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
ctx := t.Context()
|
||||
|
||||
cm := &containerMock{}
|
||||
|
||||
@@ -447,7 +447,7 @@ func TestStepActionRemotePost(t *testing.T) {
|
||||
RunContext: &RunContext{
|
||||
Config: &Config{
|
||||
GitHubInstance: "https://github.com",
|
||||
ActionCacheDir: "/tmp/test-cache",
|
||||
ActionCacheDir: t.TempDir(),
|
||||
},
|
||||
JobContainer: cm,
|
||||
Run: &model.Run{
|
||||
@@ -688,6 +688,7 @@ func Test_newRemoteActionSelfRepo(t *testing.T) {
|
||||
Ref: "v1",
|
||||
},
|
||||
},
|
||||
{name: "unknown instance", action: "$/.gitea/actions/build", github: &model.GithubContext{Repository: "owner/workflow-repo", Sha: "abc123"}},
|
||||
{name: "empty path", action: "$/", github: workflow},
|
||||
{name: "ref suffix is not allowed", action: "$/.gitea/actions/build@v1", github: workflow},
|
||||
{name: "path traversal", action: "$/../escape", github: workflow},
|
||||
@@ -771,7 +772,7 @@ func TestStepActionRemotePreResolvesDownloadedCommit(t *testing.T) {
|
||||
readAction: readActionImpl,
|
||||
}
|
||||
|
||||
require.NoError(t, sar.prepareActionExecutor()(context.Background()))
|
||||
require.NoError(t, sar.prepareActionExecutor()(t.Context()))
|
||||
|
||||
reference, sha, ok := sar.actionDownloadInfo()
|
||||
assert.True(t, ok)
|
||||
@@ -852,7 +853,7 @@ func TestStepActionRemoteCloneTokenSurvivesNilSecrets(t *testing.T) {
|
||||
|
||||
for _, tt := range table {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
ctx := t.Context()
|
||||
|
||||
var capturedToken string
|
||||
setCloneExecutor(t, func(input git.NewGitCloneExecutorInput) common.Executor {
|
||||
@@ -867,7 +868,7 @@ func TestStepActionRemoteCloneTokenSurvivesNilSecrets(t *testing.T) {
|
||||
Config: &Config{
|
||||
GitHubInstance: tt.gitHubInstance,
|
||||
DefaultActionInstance: tt.defaultActionInstance,
|
||||
ActionCacheDir: "/tmp/test-cache",
|
||||
ActionCacheDir: t.TempDir(),
|
||||
// Mirrors the state of a composite RunContext: job secrets are
|
||||
// stripped, but the job token is still reachable via Config.Token.
|
||||
Secrets: nil,
|
||||
|
||||
@@ -6,7 +6,6 @@ package runner
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"io"
|
||||
"slices"
|
||||
"strings"
|
||||
@@ -35,7 +34,7 @@ func TestStepDockerMain(t *testing.T) {
|
||||
ContainerNewContainer = origContainerNewContainer
|
||||
}()
|
||||
|
||||
ctx := context.Background()
|
||||
ctx := t.Context()
|
||||
|
||||
sd := &stepDocker{
|
||||
RunContext: &RunContext{
|
||||
@@ -124,7 +123,7 @@ func TestStepDockerNewStepContainerAllocatePTY(t *testing.T) {
|
||||
ContainerNewContainer = origContainerNewContainer
|
||||
}()
|
||||
|
||||
ctx := context.Background()
|
||||
ctx := t.Context()
|
||||
sd := &stepDocker{
|
||||
RunContext: &RunContext{
|
||||
StepResults: map[string]*model.StepResult{},
|
||||
@@ -197,7 +196,7 @@ func TestStepDockerNewStepContainerNetworkModeAndPath(t *testing.T) {
|
||||
ContainerNewContainer = origContainerNewContainer
|
||||
}()
|
||||
|
||||
ctx := context.Background()
|
||||
ctx := t.Context()
|
||||
|
||||
platform := tc.platform
|
||||
sd := &stepDocker{
|
||||
|
||||
@@ -14,12 +14,12 @@ import (
|
||||
"strings"
|
||||
|
||||
"gitea.com/gitea/runner/act/common"
|
||||
"gitea.com/gitea/runner/act/common/logging"
|
||||
"gitea.com/gitea/runner/act/container"
|
||||
"gitea.com/gitea/runner/act/lookpath"
|
||||
|
||||
"gitea.dev/actionslib/pkg/model"
|
||||
"github.com/kballard/go-shellquote"
|
||||
"github.com/sirupsen/logrus"
|
||||
yaml "go.yaml.in/yaml/v4"
|
||||
)
|
||||
|
||||
@@ -58,8 +58,8 @@ func (sr *stepRun) main() common.Executor {
|
||||
// printRunScriptActionDetails mirrors actions/runner ScriptHandler.PrintActionDetails
|
||||
// for script steps.
|
||||
func (sr *stepRun) printRunScriptActionDetails(ctx context.Context) {
|
||||
rawLogger := common.Logger(ctx).WithField(rawOutputField, true)
|
||||
scriptLineLogger := rawLogger.WithField(scriptLineCyanField, true)
|
||||
rawLogger := common.Logger(ctx).With(rawOutputField, true)
|
||||
scriptLineLogger := rawLogger.With(scriptLineCyanField, true)
|
||||
|
||||
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 {
|
||||
return
|
||||
}
|
||||
rawLogger := common.Logger(ctx).WithField(rawOutputField, true)
|
||||
rawLogger := common.Logger(ctx).With(rawOutputField, true)
|
||||
|
||||
title := step.Uses
|
||||
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)
|
||||
// shared by the run: and uses: "Run" headers.
|
||||
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()
|
||||
var visible []string
|
||||
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
|
||||
// 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")
|
||||
rawLogger.Infof(" %s: %s", key, lines[0])
|
||||
for _, line := range lines[1:] {
|
||||
|
||||
@@ -6,14 +6,14 @@ package runner
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"io"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"gitea.com/gitea/runner/act/common"
|
||||
"gitea.com/gitea/runner/act/common/logging"
|
||||
|
||||
"gitea.dev/actionslib/pkg/model"
|
||||
"github.com/sirupsen/logrus"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
yaml "go.yaml.in/yaml/v4"
|
||||
@@ -84,12 +84,9 @@ env:
|
||||
require.NoError(t, yaml.Unmarshal([]byte(raw), &step))
|
||||
|
||||
buf := &bytes.Buffer{}
|
||||
logger := logrus.New()
|
||||
logger.SetOutput(buf)
|
||||
logger.SetLevel(logrus.InfoLevel)
|
||||
logger.SetFormatter(&jobLogFormatter{color: cyan})
|
||||
entry := logger.WithFields(logrus.Fields{"job": "j1"})
|
||||
ctx := common.WithLogger(context.Background(), entry)
|
||||
logger := logging.New(logging.NewLevelHandler(newJobLogHandler(buf, cyan), logging.LevelInfo))
|
||||
entry := logger.With("job", "j1")
|
||||
ctx := common.WithLogger(t.Context(), entry)
|
||||
|
||||
sr := &stepRun{
|
||||
Step: &step,
|
||||
@@ -133,12 +130,9 @@ env:
|
||||
require.NoError(t, yaml.Unmarshal([]byte(raw), &step))
|
||||
|
||||
buf := &bytes.Buffer{}
|
||||
logger := logrus.New()
|
||||
logger.SetOutput(buf)
|
||||
logger.SetLevel(logrus.InfoLevel)
|
||||
logger.SetFormatter(&jobLogFormatter{color: cyan})
|
||||
entry := logger.WithFields(logrus.Fields{"job": "j1"})
|
||||
ctx := common.WithLogger(context.Background(), entry)
|
||||
logger := logging.New(logging.NewLevelHandler(newJobLogHandler(buf, cyan), logging.LevelInfo))
|
||||
entry := logger.With("job", "j1")
|
||||
ctx := common.WithLogger(t.Context(), entry)
|
||||
|
||||
printRunActionHeader(ctx, &step, map[string]string{"CUSTOM": "value", "GITHUB_TOKEN": "tok"}, &RunContext{})
|
||||
|
||||
@@ -166,17 +160,17 @@ func TestIsInternalEnvKey(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestPrintColoredScriptLineCyan(t *testing.T) {
|
||||
f := &jobLogFormatter{color: cyan}
|
||||
entry := &logrus.Entry{
|
||||
Level: logrus.InfoLevel,
|
||||
f := textFormatter{w: io.Discard, color: cyan}
|
||||
record := logging.Record{
|
||||
Level: logging.LevelInfo,
|
||||
Message: "echo one",
|
||||
Data: logrus.Fields{
|
||||
"job": "j1",
|
||||
rawOutputField: true,
|
||||
scriptLineCyanField: true,
|
||||
Attrs: []logging.Attr{
|
||||
{Key: "job", Value: "j1"},
|
||||
{Key: rawOutputField, Value: true},
|
||||
{Key: scriptLineCyanField, Value: true},
|
||||
},
|
||||
}
|
||||
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())
|
||||
}
|
||||
@@ -68,7 +68,7 @@ func TestStepRun(t *testing.T) {
|
||||
|
||||
cm.On("UpdateFromEnv", "/var/run/act/workflow/outputcmd.txt", mock.AnythingOfType("*map[string]string")).Return(noopExecutor)
|
||||
|
||||
ctx := context.Background()
|
||||
ctx := t.Context()
|
||||
|
||||
cm.On("GetContainerArchive", ctx, "/var/run/act/workflow/pathcmd.txt").Return(io.NopCloser(&bytes.Buffer{}), nil)
|
||||
|
||||
|
||||
+106
-39
@@ -7,18 +7,20 @@ package runner
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"io"
|
||||
"os"
|
||||
"strings"
|
||||
"testing"
|
||||
"testing/synctest"
|
||||
|
||||
"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/internal/pkg/telemetry"
|
||||
|
||||
"gitea.dev/actionslib/pkg/model"
|
||||
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/mock"
|
||||
"github.com/stretchr/testify/require"
|
||||
@@ -164,8 +166,8 @@ func TestSetupEnv(t *testing.T) {
|
||||
sm.On("getStepModel").Return(step)
|
||||
sm.On("getEnv").Return(&env)
|
||||
|
||||
require.NoError(t, setupEnv(context.Background(), sm))
|
||||
require.NoError(t, setupInputs(context.Background(), sm))
|
||||
require.NoError(t, setupEnv(t.Context(), sm))
|
||||
require.NoError(t, setupInputs(t.Context(), sm))
|
||||
|
||||
// These are commit or system specific
|
||||
delete(env, "GITHUB_REF")
|
||||
@@ -189,7 +191,6 @@ func TestSetupEnv(t *testing.T) {
|
||||
"GITHUB_ACTION_PATH": "",
|
||||
"GITHUB_ACTION_REF": "",
|
||||
"GITHUB_ACTION_REPOSITORY": "",
|
||||
"GITHUB_API_URL": "https:///api/v1", // Gitea uses api/v1 (upstream GitHub: api/v3)
|
||||
"GITHUB_BASE_REF": "",
|
||||
"GITHUB_EVENT_NAME": "",
|
||||
"GITHUB_EVENT_PATH": "/var/run/act/workflow/event.json",
|
||||
@@ -200,7 +201,6 @@ func TestSetupEnv(t *testing.T) {
|
||||
"GITHUB_RUN_ATTEMPT": "",
|
||||
"GITHUB_RUN_ID": "runId",
|
||||
"GITHUB_RUN_NUMBER": "1",
|
||||
"GITHUB_SERVER_URL": "https://",
|
||||
"GITHUB_WORKFLOW": "",
|
||||
"INPUT_ID": "1234567",
|
||||
"INPUT_STEP_WITH": "with-value",
|
||||
@@ -299,63 +299,63 @@ func TestIsStepEnabled(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
log.SetLevel(log.DebugLevel)
|
||||
logging.SetLevel(logging.LevelDebug)
|
||||
assertObject := assert.New(t)
|
||||
|
||||
// success()
|
||||
step := createTestStep(t, "if: success()")
|
||||
assertObject.True(isStepEnabled(context.Background(), step.getIfExpression(context.Background(), stepStageMain), step, stepStageMain))
|
||||
assertObject.True(isStepEnabled(t.Context(), step.getIfExpression(t.Context(), stepStageMain), step, stepStageMain))
|
||||
|
||||
step = createTestStep(t, "if: success()")
|
||||
step.getRunContext().StepResults["a"] = &model.StepResult{
|
||||
Conclusion: model.StepStatusSuccess,
|
||||
}
|
||||
assertObject.True(isStepEnabled(context.Background(), step.getStepModel().If.Value, step, stepStageMain))
|
||||
assertObject.True(isStepEnabled(t.Context(), step.getStepModel().If.Value, step, stepStageMain))
|
||||
|
||||
step = createTestStep(t, "if: success()")
|
||||
step.getRunContext().StepResults["a"] = &model.StepResult{
|
||||
Conclusion: model.StepStatusFailure,
|
||||
}
|
||||
assertObject.False(isStepEnabled(context.Background(), step.getStepModel().If.Value, step, stepStageMain))
|
||||
assertObject.False(isStepEnabled(t.Context(), step.getStepModel().If.Value, step, stepStageMain))
|
||||
|
||||
// failure()
|
||||
step = createTestStep(t, "if: failure()")
|
||||
assertObject.False(isStepEnabled(context.Background(), step.getStepModel().If.Value, step, stepStageMain))
|
||||
assertObject.False(isStepEnabled(t.Context(), step.getStepModel().If.Value, step, stepStageMain))
|
||||
|
||||
step = createTestStep(t, "if: failure()")
|
||||
step.getRunContext().StepResults["a"] = &model.StepResult{
|
||||
Conclusion: model.StepStatusSuccess,
|
||||
}
|
||||
assertObject.False(isStepEnabled(context.Background(), step.getStepModel().If.Value, step, stepStageMain))
|
||||
assertObject.False(isStepEnabled(t.Context(), step.getStepModel().If.Value, step, stepStageMain))
|
||||
|
||||
step = createTestStep(t, "if: failure()")
|
||||
step.getRunContext().StepResults["a"] = &model.StepResult{
|
||||
Conclusion: model.StepStatusFailure,
|
||||
}
|
||||
assertObject.True(isStepEnabled(context.Background(), step.getStepModel().If.Value, step, stepStageMain))
|
||||
assertObject.True(isStepEnabled(t.Context(), step.getStepModel().If.Value, step, stepStageMain))
|
||||
|
||||
// always()
|
||||
step = createTestStep(t, "if: always()")
|
||||
assertObject.True(isStepEnabled(context.Background(), step.getStepModel().If.Value, step, stepStageMain))
|
||||
assertObject.True(isStepEnabled(t.Context(), step.getStepModel().If.Value, step, stepStageMain))
|
||||
|
||||
step = createTestStep(t, "if: always()")
|
||||
step.getRunContext().StepResults["a"] = &model.StepResult{
|
||||
Conclusion: model.StepStatusSuccess,
|
||||
}
|
||||
assertObject.True(isStepEnabled(context.Background(), step.getStepModel().If.Value, step, stepStageMain))
|
||||
assertObject.True(isStepEnabled(t.Context(), step.getStepModel().If.Value, step, stepStageMain))
|
||||
|
||||
step = createTestStep(t, "if: always()")
|
||||
step.getRunContext().StepResults["a"] = &model.StepResult{
|
||||
Conclusion: model.StepStatusFailure,
|
||||
}
|
||||
assertObject.True(isStepEnabled(context.Background(), step.getStepModel().If.Value, step, stepStageMain))
|
||||
assertObject.True(isStepEnabled(t.Context(), step.getStepModel().If.Value, step, stepStageMain))
|
||||
|
||||
// neither env nor the step's own with: values are inputs, at any stage
|
||||
step = createTestStep(t, "if: inputs.forged")
|
||||
step.getRunContext().Env["INPUT_FORGED"] = "leaked"
|
||||
*step.getEnv() = map[string]string{"INPUT_FORGED": "leaked"}
|
||||
assertObject.False(isStepEnabled(context.Background(), step.getStepModel().If.Value, step, stepStageMain))
|
||||
assertObject.False(isStepEnabled(context.Background(), step.getStepModel().If.Value, step, stepStagePost))
|
||||
assertObject.False(isStepEnabled(t.Context(), step.getStepModel().If.Value, step, stepStageMain))
|
||||
assertObject.False(isStepEnabled(t.Context(), step.getStepModel().If.Value, step, stepStagePost))
|
||||
}
|
||||
|
||||
func TestIsContinueOnError(t *testing.T) {
|
||||
@@ -383,49 +383,49 @@ func TestIsContinueOnError(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
log.SetLevel(log.DebugLevel)
|
||||
logging.SetLevel(logging.LevelDebug)
|
||||
assertObject := assert.New(t)
|
||||
|
||||
// absent
|
||||
step := createTestStep(t, "name: test")
|
||||
continueOnError, err := isContinueOnError(context.Background(), step.getStepModel().RawContinueOnError, step, stepStageMain)
|
||||
continueOnError, err := isContinueOnError(t.Context(), step.getStepModel().RawContinueOnError, step, stepStageMain)
|
||||
assertObject.False(continueOnError)
|
||||
assertObject.NoError(err) //nolint:testifylint // pre-existing issue from nektos/act
|
||||
|
||||
// explcit true
|
||||
step = createTestStep(t, "continue-on-error: true")
|
||||
continueOnError, err = isContinueOnError(context.Background(), step.getStepModel().RawContinueOnError, step, stepStageMain)
|
||||
continueOnError, err = isContinueOnError(t.Context(), step.getStepModel().RawContinueOnError, step, stepStageMain)
|
||||
assertObject.True(continueOnError)
|
||||
assertObject.NoError(err) //nolint:testifylint // pre-existing issue from nektos/act
|
||||
|
||||
// explicit false
|
||||
step = createTestStep(t, "continue-on-error: false")
|
||||
continueOnError, err = isContinueOnError(context.Background(), step.getStepModel().RawContinueOnError, step, stepStageMain)
|
||||
continueOnError, err = isContinueOnError(t.Context(), step.getStepModel().RawContinueOnError, step, stepStageMain)
|
||||
assertObject.False(continueOnError)
|
||||
assertObject.NoError(err) //nolint:testifylint // pre-existing issue from nektos/act
|
||||
|
||||
// expression true
|
||||
step = createTestStep(t, "continue-on-error: ${{ 'test' == 'test' }}")
|
||||
continueOnError, err = isContinueOnError(context.Background(), step.getStepModel().RawContinueOnError, step, stepStageMain)
|
||||
continueOnError, err = isContinueOnError(t.Context(), step.getStepModel().RawContinueOnError, step, stepStageMain)
|
||||
assertObject.True(continueOnError)
|
||||
assertObject.NoError(err) //nolint:testifylint // pre-existing issue from nektos/act
|
||||
|
||||
// expression false
|
||||
step = createTestStep(t, "continue-on-error: ${{ 'test' != 'test' }}")
|
||||
continueOnError, err = isContinueOnError(context.Background(), step.getStepModel().RawContinueOnError, step, stepStageMain)
|
||||
continueOnError, err = isContinueOnError(t.Context(), step.getStepModel().RawContinueOnError, step, stepStageMain)
|
||||
assertObject.False(continueOnError)
|
||||
assertObject.NoError(err) //nolint:testifylint // pre-existing issue from nektos/act
|
||||
|
||||
// the step's own with: values are not inputs
|
||||
step = createTestStep(t, "continue-on-error: ${{ inputs.forged }}")
|
||||
*step.getEnv() = map[string]string{"INPUT_FORGED": "true"}
|
||||
continueOnError, err = isContinueOnError(context.Background(), step.getStepModel().RawContinueOnError, step, stepStageMain)
|
||||
continueOnError, err = isContinueOnError(t.Context(), step.getStepModel().RawContinueOnError, step, stepStageMain)
|
||||
assertObject.False(continueOnError)
|
||||
require.NoError(t, err)
|
||||
|
||||
// expression parse error
|
||||
step = createTestStep(t, "continue-on-error: ${{ 'test' != test }}")
|
||||
continueOnError, err = isContinueOnError(context.Background(), step.getStepModel().RawContinueOnError, step, stepStageMain)
|
||||
continueOnError, err = isContinueOnError(t.Context(), step.getStepModel().RawContinueOnError, step, stepStageMain)
|
||||
assertObject.False(continueOnError)
|
||||
assertObject.Error(err)
|
||||
}
|
||||
@@ -449,9 +449,9 @@ func TestRunStepExecutorDoesNotLeakRefusalToNextStep(t *testing.T) {
|
||||
StepResults: map[string]*model.StepResult{},
|
||||
JobContainer: cm,
|
||||
}
|
||||
rc.ExprEval = rc.NewExpressionEvaluator(context.Background())
|
||||
rc.ExprEval = rc.NewExpressionEvaluator(t.Context())
|
||||
// Dryrun skips reading the path file back from the (mocked) container.
|
||||
ctx := common.WithDryrun(context.Background(), true)
|
||||
ctx := common.WithDryrun(t.Context(), true)
|
||||
|
||||
// A refusal parsed out of the job container's own output belongs to no step, so the
|
||||
// first step must not be failed by it.
|
||||
@@ -475,11 +475,31 @@ func TestRunStepExecutorDoesNotLeakRefusalToNextStep(t *testing.T) {
|
||||
require.NoError(t, errB)
|
||||
}
|
||||
|
||||
type fileCommandTestContainer struct {
|
||||
*container.HostEnvironment
|
||||
}
|
||||
|
||||
func (c *fileCommandTestContainer) UpdateFromEnv(srcPath string, env *map[string]string) common.Executor {
|
||||
return func(ctx context.Context) error {
|
||||
if err := ctx.Err(); err != nil {
|
||||
return err
|
||||
}
|
||||
return c.HostEnvironment.UpdateFromEnv(srcPath, env)(ctx)
|
||||
}
|
||||
}
|
||||
|
||||
func (c *fileCommandTestContainer) GetContainerArchive(ctx context.Context, srcPath string) (io.ReadCloser, error) {
|
||||
if err := ctx.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return c.HostEnvironment.GetContainerArchive(ctx, srcPath)
|
||||
}
|
||||
|
||||
func TestRunStepExecutorParity(t *testing.T) {
|
||||
newStep := func(t *testing.T, stepModel *model.Step) *stepRun {
|
||||
rc := createRunContext(t)
|
||||
rc.JobContainer = &container.HostEnvironment{ActPath: t.TempDir()}
|
||||
rc.ExprEval = rc.NewExpressionEvaluator(context.Background())
|
||||
rc.ExprEval = rc.NewExpressionEvaluator(t.Context())
|
||||
return &stepRun{RunContext: rc, Step: stepModel, env: map[string]string{}}
|
||||
}
|
||||
badExpression := "${{ 'test' != test }}"
|
||||
@@ -493,30 +513,48 @@ func TestRunStepExecutorParity(t *testing.T) {
|
||||
} {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
step := newStep(t, test.step)
|
||||
logger, hook := logrustest.NewNullLogger()
|
||||
err := runStepExecutor(step, stepStageMain, test.executor)(common.WithLogger(context.Background(), logger))
|
||||
logger, hook := logtest.NewLogger()
|
||||
err := runStepExecutor(step, stepStageMain, test.executor)(common.WithLogger(t.Context(), logger))
|
||||
|
||||
require.ErrorContains(t, err, test.wantError)
|
||||
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("only a step that runs is marked started", func(t *testing.T) {
|
||||
for _, test := range []struct {
|
||||
condition string
|
||||
started int
|
||||
}{{"true", 1}, {"false", 0}} {
|
||||
logger, hook := logtest.NewLogger()
|
||||
step := newStep(t, &model.Step{ID: "pre", If: yaml.Node{Value: test.condition}})
|
||||
require.NoError(t, runStepExecutor(step, stepStagePre, noopExecutor)(common.WithLogger(context.Background(), logger)))
|
||||
started := 0
|
||||
for _, entry := range hook.AllRecords() {
|
||||
if v, _ := entry.Attr(StepStartedField); v == true {
|
||||
started++
|
||||
}
|
||||
}
|
||||
assert.Equal(t, test.started, started, test.condition)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("file command error honors continue-on-error", func(t *testing.T) {
|
||||
step := newStep(t, &model.Step{ID: "commands", RawContinueOnError: "true"})
|
||||
logger, hook := logrustest.NewNullLogger()
|
||||
logger, hook := logtest.NewLogger()
|
||||
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_OUTPUT"], []byte("kept=value\n"), 0o600))
|
||||
require.NoError(t, os.WriteFile(step.env["GITHUB_STATE"], []byte("saved=value\n"), 0o600))
|
||||
return nil
|
||||
})(common.WithLogger(context.Background(), logger))
|
||||
})(common.WithLogger(t.Context(), logger))
|
||||
|
||||
require.NoError(t, err)
|
||||
result := step.RunContext.StepResults[step.Step.ID]
|
||||
assert.Equal(t, model.StepStatusFailure, result.Outcome)
|
||||
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, "value", result.Outputs["kept"])
|
||||
assert.Equal(t, "value", step.RunContext.IntraActionState[step.Step.ID]["saved"])
|
||||
@@ -527,13 +565,37 @@ func TestRunStepExecutorParity(t *testing.T) {
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("file commands survive a job timeout", func(t *testing.T) {
|
||||
synctest.Test(t, func(t *testing.T) {
|
||||
step := newStep(t, &model.Step{ID: "interrupted"})
|
||||
ctx, cancel := applyJobTimeout(t.Context(), step.RunContext, &model.Job{TimeoutMinutes: "1"})
|
||||
defer cancel()
|
||||
step.RunContext.JobContainer = &fileCommandTestContainer{step.RunContext.JobContainer.(*container.HostEnvironment)}
|
||||
err := runStepExecutor(step, stepStageMain, func(execCtx context.Context) error {
|
||||
for name, content := range map[string]string{
|
||||
"GITHUB_ENV": "KEPT_ENV=value\n", "GITHUB_STATE": "saved=value\n",
|
||||
"GITHUB_OUTPUT": "kept=value\n", "GITHUB_PATH": "/kept/bin\n",
|
||||
} {
|
||||
require.NoError(t, os.WriteFile(step.env[name], []byte(content), 0o600))
|
||||
}
|
||||
<-execCtx.Done()
|
||||
return execCtx.Err()
|
||||
})(ctx)
|
||||
require.ErrorIs(t, err, context.DeadlineExceeded)
|
||||
assert.Equal(t, "value", step.RunContext.Env["KEPT_ENV"])
|
||||
assert.Equal(t, "value", step.RunContext.IntraActionState[step.Step.ID]["saved"])
|
||||
assert.Equal(t, "value", step.RunContext.StepResults[step.Step.ID].Outputs["kept"])
|
||||
assert.Contains(t, step.RunContext.ExtraPath, "/kept/bin")
|
||||
})
|
||||
})
|
||||
|
||||
t.Run("composite child files do not leak", func(t *testing.T) {
|
||||
outer := newStep(t, &model.Step{ID: "outer"})
|
||||
childRC := &RunContext{
|
||||
Config: outer.RunContext.Config, Run: outer.RunContext.Run, Env: map[string]string{}, StepResults: map[string]*model.StepResult{},
|
||||
JobContainer: outer.RunContext.JobContainer, Parent: outer.RunContext,
|
||||
}
|
||||
childRC.ExprEval = childRC.NewExpressionEvaluator(context.Background())
|
||||
childRC.ExprEval = childRC.NewExpressionEvaluator(t.Context())
|
||||
child := &stepRun{RunContext: childRC, Step: &model.Step{ID: "child"}, env: map[string]string{}}
|
||||
|
||||
err := runStepExecutor(outer, stepStageMain, func(ctx context.Context) error {
|
||||
@@ -544,7 +606,7 @@ func TestRunStepExecutorParity(t *testing.T) {
|
||||
})(ctx))
|
||||
outer.RunContext.setOutput(ctx, map[string]string{"name": "declared"}, "outer")
|
||||
return nil
|
||||
})(context.Background())
|
||||
})(t.Context())
|
||||
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, map[string]string{"declared": "outer"}, outer.RunContext.StepResults[outer.Step.ID].Outputs)
|
||||
@@ -577,12 +639,17 @@ func TestRunStepExecutorParity(t *testing.T) {
|
||||
})
|
||||
|
||||
t.Run("timeout must be positive", func(t *testing.T) {
|
||||
exprEval := createRunContext(t).NewExpressionEvaluator(context.Background())
|
||||
exprEval := createRunContext(t).NewExpressionEvaluator(t.Context())
|
||||
for timeout, wantDeadline := range map[string]bool{"-1": false, "0": false, "1": true} {
|
||||
ctx, cancel := evaluateStepTimeout(context.Background(), exprEval, &model.Step{TimeoutMinutes: timeout})
|
||||
ctx, cancel := evaluateStepTimeout(t.Context(), exprEval, &model.Step{TimeoutMinutes: timeout})
|
||||
_, hasDeadline := ctx.Deadline()
|
||||
cancel()
|
||||
assert.Equal(t, wantDeadline, hasDeadline, timeout)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func lastRecordAttr(h *logtest.Handler, key string) any {
|
||||
v, _ := h.LastRecord().Attr(key)
|
||||
return v
|
||||
}
|
||||
+3
-3
@@ -37,10 +37,10 @@ async function main() {
|
||||
await request('POST', `${api}/networks/${name}/connect`, {Container: job});
|
||||
fs.appendFileSync(process.env.GITHUB_STATE, 'post=true\n');
|
||||
}
|
||||
const label = process.env.PROXY_TEST_MODE === 'proxy' ? job : undefined;
|
||||
assert.equal(JSON.parse(await request('GET', `${api}/volumes/${name}`)).Labels?.['com.gitea.runner.job'], label);
|
||||
const label = process.env.PROXY_TEST_MODE === 'proxy' ? '' : undefined;
|
||||
assert.equal(JSON.parse(await request('GET', `${api}/volumes/${name}`)).Labels?.[`com.gitea.runner.job.${job}`], label);
|
||||
const network = JSON.parse(await request('GET', `${api}/networks/${name}`));
|
||||
assert.equal(network.Labels?.['com.gitea.runner.job'], label);
|
||||
assert.equal(network.Labels?.[`com.gitea.runner.job.${job}`], label);
|
||||
assert(Object.values(network.Containers).some((container) => container.Name === job));
|
||||
if (post) {
|
||||
console.log('docker proxy post verified');
|
||||
|
||||
+1
-1
@@ -52,5 +52,5 @@ jobs:
|
||||
- run: |
|
||||
docker run --rm --user 1001 --group-add 2375 -v /var/run/docker.sock:/var/run/docker.sock -v /usr/local/bin/docker:/usr/local/bin/docker:ro -v "${GITEA_DOCKER_WORKSPACE:?}/data:/data:ro" \
|
||||
"$PROXY_TEST_IMAGE" sh -c 'grep -qx from-job /data/marker && docker volume create "$0"' "$PROXY_TEST_RESOURCE-nested"
|
||||
test "$(docker volume inspect -f '{{ index .Labels "com.gitea.runner.job" }}' "$PROXY_TEST_RESOURCE-nested")" = "$JOB_CONTAINER_NAME"
|
||||
test "$(docker volume ls -q -f "label=com.gitea.runner.job.$JOB_CONTAINER_NAME" -f "name=$PROXY_TEST_RESOURCE-nested")" = "$PROXY_TEST_RESOURCE-nested"
|
||||
echo "docker binds verified"
|
||||
@@ -82,7 +82,7 @@ The script environment is **not** a full copy of the job container environment.
|
||||
|
||||
## Output and errors
|
||||
|
||||
- **Stdout/stderr** are written to the **runner process log** (logrus), prefixed with `post-task script stdout:` / `post-task script stderr:`.
|
||||
- **Stdout/stderr** are written to the **runner process log**, prefixed with `post-task script stdout:` / `post-task script stderr:`.
|
||||
- **Non-zero exit codes** are logged as warnings only. They do **not** change the job result already reported to Gitea.
|
||||
- **Timeouts and start failures** are logged as warnings; the runner still completes the task acknowledgement.
|
||||
|
||||
|
||||
@@ -2,12 +2,12 @@ module gitea.com/gitea/runner
|
||||
|
||||
go 1.27
|
||||
|
||||
toolchain go1.27.1
|
||||
toolchain go1.27.2
|
||||
|
||||
require (
|
||||
connectrpc.com/connect v1.21.0
|
||||
dario.cat/mergo v1.0.2
|
||||
gitea.dev/actionslib v1.3.0
|
||||
gitea.dev/actionslib v1.4.1
|
||||
github.com/avast/retry-go/v5 v5.0.0
|
||||
github.com/bmatcuk/doublestar/v4 v4.10.2
|
||||
github.com/coder/websocket v1.8.15
|
||||
@@ -17,8 +17,6 @@ require (
|
||||
github.com/docker/cli v29.8.2+incompatible
|
||||
github.com/docker/go-connections v0.8.1
|
||||
github.com/docker/go-units v0.5.0
|
||||
github.com/go-git/go-billy/v5 v5.9.2
|
||||
github.com/go-git/go-git/v5 v5.19.2
|
||||
github.com/google/go-cmp v0.7.0
|
||||
github.com/johannesboyne/gofakes3 v1.2.0
|
||||
github.com/joho/godotenv v1.5.1
|
||||
@@ -26,8 +24,8 @@ require (
|
||||
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51
|
||||
github.com/mattn/go-isatty v0.0.24
|
||||
github.com/moby/go-archive v0.3.3
|
||||
github.com/moby/moby/api v1.56.0
|
||||
github.com/moby/moby/client v0.6.0
|
||||
github.com/moby/moby/api v1.56.1
|
||||
github.com/moby/moby/client v0.6.1
|
||||
github.com/moby/patternmatcher v0.6.1
|
||||
github.com/opencontainers/image-spec v1.1.1
|
||||
github.com/opencontainers/selinux v1.15.1
|
||||
@@ -39,15 +37,15 @@ require (
|
||||
github.com/stretchr/testify v1.12.1
|
||||
github.com/timshannon/bolthold v0.0.0-20240314194003-30aac6950928
|
||||
go.etcd.io/bbolt v1.5.0
|
||||
go.opentelemetry.io/otel v1.46.0
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.46.0
|
||||
go.opentelemetry.io/otel/sdk v1.46.0
|
||||
go.opentelemetry.io/otel/trace v1.46.0
|
||||
go.opentelemetry.io/proto/otlp v1.11.0
|
||||
go.opentelemetry.io/otel v1.47.0
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.47.0
|
||||
go.opentelemetry.io/otel/sdk v1.47.0
|
||||
go.opentelemetry.io/otel/trace v1.47.0
|
||||
go.opentelemetry.io/proto/otlp v1.11.1
|
||||
go.yaml.in/yaml/v4 v4.0.0-rc.6
|
||||
golang.org/x/net v0.59.0
|
||||
golang.org/x/sync v0.23.0
|
||||
golang.org/x/sys v0.48.0
|
||||
golang.org/x/net v0.60.0
|
||||
golang.org/x/sync v0.24.0
|
||||
golang.org/x/sys v0.49.0
|
||||
golang.org/x/term v0.46.0
|
||||
golang.org/x/text v0.42.0
|
||||
google.golang.org/protobuf v1.36.12
|
||||
@@ -58,49 +56,36 @@ require (
|
||||
require (
|
||||
cyphar.com/go-pathrs v0.2.5 // indirect
|
||||
github.com/Microsoft/go-winio v0.6.2 // indirect
|
||||
github.com/ProtonMail/go-crypto v1.4.1 // indirect
|
||||
github.com/beorn7/perks v1.0.1 // indirect
|
||||
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
||||
github.com/cloudflare/circl v1.6.5 // indirect
|
||||
github.com/containerd/errdefs/pkg v0.3.0 // indirect
|
||||
github.com/containerd/log v0.1.0 // indirect
|
||||
github.com/cyphar/filepath-securejoin v0.7.0 // indirect
|
||||
github.com/docker/docker-credential-helpers v0.9.8 // indirect
|
||||
github.com/emirpasic/gods v1.18.1 // indirect
|
||||
github.com/felixge/httpsnoop v1.1.0 // indirect
|
||||
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect
|
||||
github.com/go-logr/logr v1.4.4 // indirect
|
||||
github.com/go-logr/stdr v1.2.2 // indirect
|
||||
github.com/go-viper/mapstructure/v2 v2.5.0 // indirect
|
||||
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 // indirect
|
||||
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect
|
||||
github.com/google/uuid v1.6.0 // indirect
|
||||
github.com/inconshreveable/mousetrap v1.1.0 // indirect
|
||||
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect
|
||||
github.com/kevinburke/ssh_config v1.6.0 // indirect
|
||||
github.com/klauspost/compress v1.19.2 // indirect
|
||||
github.com/klauspost/cpuid/v2 v2.4.0 // indirect
|
||||
github.com/moby/docker-image-spec v1.3.1 // indirect
|
||||
github.com/moby/sys/sequential v0.7.0 // indirect
|
||||
github.com/moby/sys/user v0.4.1 // indirect
|
||||
github.com/moby/sys/userns v0.2.0 // indirect
|
||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
|
||||
github.com/opencontainers/go-digest v1.0.0 // indirect
|
||||
github.com/pjbgf/sha1cd v0.6.0 // indirect
|
||||
github.com/prometheus/common v0.70.1 // indirect
|
||||
github.com/prometheus/procfs v0.21.1 // indirect
|
||||
github.com/ryszard/goskiplist v0.0.0-20150312221310-2dfbae5fcf46 // indirect
|
||||
github.com/santhosh-tekuri/jsonschema/v6 v6.0.3 // indirect
|
||||
github.com/sergi/go-diff v1.4.0 // indirect
|
||||
github.com/skeema/knownhosts v1.3.2 // indirect
|
||||
github.com/stretchr/objx v0.5.3 // indirect
|
||||
github.com/xanzy/ssh-agent v0.3.3 // indirect
|
||||
go.opentelemetry.io/auto/sdk v1.2.1 // indirect
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.70.0 // indirect
|
||||
go.opentelemetry.io/otel/metric v1.46.0 // indirect
|
||||
go.opentelemetry.io/otel/log v1.47.0 // indirect
|
||||
go.opentelemetry.io/otel/metric v1.47.0 // indirect
|
||||
go.shabbyrobe.org/gocovmerge v0.0.0-20230507111327-fa4f82cfbf4d // indirect
|
||||
go.yaml.in/yaml/v3 v3.0.5 // indirect
|
||||
golang.org/x/crypto v0.57.0 // indirect
|
||||
golang.org/x/tools v0.49.0 // indirect
|
||||
gopkg.in/warnings.v0 v0.1.2 // indirect
|
||||
)
|
||||
@@ -4,19 +4,12 @@ cyphar.com/go-pathrs v0.2.5 h1:SnX9FBvnoyn3lUs1dkMgZ52bAETpirNu3FTRh5HlRik=
|
||||
cyphar.com/go-pathrs v0.2.5/go.mod h1:y8f1EMG7r+hCuFf/rXsKqMJrJAUoADZGNh5/vZPKcGc=
|
||||
dario.cat/mergo v1.0.2 h1:85+piFYR1tMbRrLcDwR18y4UKJ3aH1Tbzi24VRW1TK8=
|
||||
dario.cat/mergo v1.0.2/go.mod h1:E/hbnu0NxMFBjpMIE34DRGLWqDy0g5FuKDhCb31ngxA=
|
||||
gitea.dev/actionslib v1.3.0 h1:xZRoTL1+sK/PaglT9uXkZJ9g0nk1WLMEH4FUXdXpKKw=
|
||||
gitea.dev/actionslib v1.3.0/go.mod h1:svCefEsavx4jmn8vJ4wTG7Q0KBcGfrFmGUjrhxuNUAQ=
|
||||
gitea.dev/actionslib v1.4.1 h1:DWlUnzDfuuMArQWS6yZ+vwJu9av7sZVMOox7li/kyu4=
|
||||
gitea.dev/actionslib v1.4.1/go.mod h1:RbXaZudZ04YndjBBxZJF9rZAqU6SKGkaSD1sob5lQbE=
|
||||
github.com/AdaLogics/go-fuzz-headers v0.0.0-20240806141605-e8a1dd7889d6 h1:He8afgbRMd7mFxO99hRNu+6tazq8nFF9lIwo9JFroBk=
|
||||
github.com/AdaLogics/go-fuzz-headers v0.0.0-20240806141605-e8a1dd7889d6/go.mod h1:8o94RPi1/7XTJvwPpRSzSUedZrtlirdB3r9Z20bi2f8=
|
||||
github.com/Microsoft/go-winio v0.5.2/go.mod h1:WpS1mjBmmwHBEWmogvA2mj8546UReBk4v8QkMxJ6pZY=
|
||||
github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY=
|
||||
github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=
|
||||
github.com/ProtonMail/go-crypto v1.4.1 h1:9RfcZHqEQUvP8RzecWEUafnZVtEvrBVL9BiF67IQOfM=
|
||||
github.com/ProtonMail/go-crypto v1.4.1/go.mod h1:e1OaTyu5SYVrO9gKOEhTc+5UcXtTUa+P3uLudwcgPqo=
|
||||
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be h1:9AeTilPcZAjCFIImctFaOjnTIavg87rW78vTPkQqLI8=
|
||||
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be/go.mod h1:ySMOLuWl6zY27l47sB3qLNK6tF2fkHG55UZxx8oIVo4=
|
||||
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio=
|
||||
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs=
|
||||
github.com/avast/retry-go/v5 v5.0.0 h1:kf1Qc2UsTZ4qq8elDymqfbISvkyMuhgRxuJqX2NHP7k=
|
||||
github.com/avast/retry-go/v5 v5.0.0/go.mod h1://d+usmKWio1agtZfS1H/ltTqwtIfBnRq9zEwjc3eH8=
|
||||
github.com/aws/aws-sdk-go-v2 v1.41.5 h1:dj5kopbwUsVUVFgO4Fi5BIT3t4WyqIDjGKCangnV/yY=
|
||||
@@ -53,8 +46,6 @@ github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UF
|
||||
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||
github.com/cevatbarisyilmaz/ara v0.0.4 h1:SGH10hXpBJhhTlObuZzTuFn1rrdmjQImITXnZVPSodc=
|
||||
github.com/cevatbarisyilmaz/ara v0.0.4/go.mod h1:BfFOxnUd6Mj6xmcvRxHN3Sr21Z1T3U2MYkYOmoQe4Ts=
|
||||
github.com/cloudflare/circl v1.6.5 h1:O64F26HEqNhznd/hrC5KZXVKYuKM2rx4deZDTc4ihQA=
|
||||
github.com/cloudflare/circl v1.6.5/go.mod h1:h5LNyxAc5nTue9DS5jT+48en2PSDYt3zdGnz5OstK6c=
|
||||
github.com/coder/websocket v1.8.15 h1:6B2JPeOGlpff2Uz6vOEH1Vzpi0iUz20A+lPVhPHtNUA=
|
||||
github.com/coder/websocket v1.8.15/go.mod h1:NX3SzP+inril6yawo5CQXx8+fk145lPDC6pumgx0mVg=
|
||||
github.com/containerd/errdefs v1.0.0 h1:tg5yIfIlQIrxYtu9ajqY42W3lpS19XqdxRQeEwYG8PI=
|
||||
@@ -82,22 +73,8 @@ github.com/docker/go-connections v0.8.1 h1:JibmG5hULs5qXSr/cp/w3Pw5fZuStt4MOHMUE
|
||||
github.com/docker/go-connections v0.8.1/go.mod h1:no1qkHdjq7kLMGUXYAduOhYPSJxxvgWBh7ogVvptn3Q=
|
||||
github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4=
|
||||
github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
|
||||
github.com/elazarl/goproxy v1.7.2 h1:Y2o6urb7Eule09PjlhQRGNsqRfPmYI3KKQLFpCAV3+o=
|
||||
github.com/elazarl/goproxy v1.7.2/go.mod h1:82vkLNir0ALaW14Rc399OTTjyNREgmdL2cVoIbS6XaE=
|
||||
github.com/emirpasic/gods v1.18.1 h1:FXtiHYKDGKCW2KzwZKx0iC0PQmdlorYgdFG9jPXJ1Bc=
|
||||
github.com/emirpasic/gods v1.18.1/go.mod h1:8tpGGwCnJ5H4r6BWwaV6OrWmMoPhUl5jm/FMNAnJvWQ=
|
||||
github.com/felixge/httpsnoop v1.1.0 h1:3YtUj32ZZkqZtt3sZZsClsymw/QDuVfpNhoA31zeORc=
|
||||
github.com/felixge/httpsnoop v1.1.0/go.mod h1:Zqxgdd+1Rkcz8euOqdr7lqgCRJztwr5hp9vDSi5UZCE=
|
||||
github.com/gliderlabs/ssh v0.3.8 h1:a4YXD1V7xMF9g5nTkdfnja3Sxy1PVDCj1Zg4Wb8vY6c=
|
||||
github.com/gliderlabs/ssh v0.3.8/go.mod h1:xYoytBv1sV0aL3CavoDuJIQNURXkkfPA/wxQ1pL1fAU=
|
||||
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 h1:+zs/tPmkDkHx3U66DAb0lQFJrpS6731Oaa12ikc+DiI=
|
||||
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376/go.mod h1:an3vInlBmSxCcxctByoQdvwPiA7DTK7jaaFDBTtu0ic=
|
||||
github.com/go-git/go-billy/v5 v5.9.2 h1:OXFSRyz4g20upsGDJgQG9Bak1l/ZEv8GHVYB52O71sE=
|
||||
github.com/go-git/go-billy/v5 v5.9.2/go.mod h1:ExsU+jcGwXTBOnyilvAnEM1wug1IxHr4yP2ZXsNRtV0=
|
||||
github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399 h1:eMje31YglSBqCdIqdhKBW8lokaMrL3uTkpGYlE2OOT4=
|
||||
github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399/go.mod h1:1OCfN199q1Jm3HZlxleg+Dw/mwps2Wbk9frAWm+4FII=
|
||||
github.com/go-git/go-git/v5 v5.19.2 h1:wkfn7vOlUBu8ivAWKBWisTiwJK4jYHzTF8Ndv1LyGqY=
|
||||
github.com/go-git/go-git/v5 v5.19.2/go.mod h1:QqCBE1EFN5ddFmrliLQ3/ntRCUjZU3EJuwuB/jWEHjk=
|
||||
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
|
||||
github.com/go-logr/logr v1.4.4 h1:tG4xh9yMsRCAiodLVTxyrkzSZ9+o0L1Kg/+cPVcbP/8=
|
||||
github.com/go-logr/logr v1.4.4/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
|
||||
@@ -105,8 +82,6 @@ github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
|
||||
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
|
||||
github.com/go-viper/mapstructure/v2 v2.5.0 h1:vM5IJoUAy3d7zRSVtIwQgBj7BiWtMPfmPEgAXnvj1Ro=
|
||||
github.com/go-viper/mapstructure/v2 v2.5.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
|
||||
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 h1:f+oWsMOmNPc8JmEHVZIycC7hBoQxHH9pNKQORJNozsQ=
|
||||
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8/go.mod h1:wcDNUvekVysuuOpQKo3191zZyTpiI6se1N1ULghS0sw=
|
||||
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
||||
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
||||
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaUGG7oYTSPP8MxqL4YI3kZKwcP4=
|
||||
@@ -115,8 +90,6 @@ github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
|
||||
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
|
||||
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A=
|
||||
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo=
|
||||
github.com/johannesboyne/gofakes3 v1.2.0 h1:I9VEzPWvvAUAGzDlhYFoZjF0AXMlkcEyZlmBwiI6Oms=
|
||||
github.com/johannesboyne/gofakes3 v1.2.0/go.mod h1:UHhRZRod9rENGFrUWTYnQHZqlNgSmjOq8DaD/ATQYRM=
|
||||
github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
|
||||
@@ -125,19 +98,8 @@ github.com/julienschmidt/httprouter v1.3.0 h1:U0609e9tgbseu3rBINet9P48AI/D3oJs4d
|
||||
github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM=
|
||||
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 h1:Z9n2FFNUXsshfwJMBgNA0RU6/i7WVaAegv3PtuIHPMs=
|
||||
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8=
|
||||
github.com/kevinburke/ssh_config v1.6.0 h1:J1FBfmuVosPHf5GRdltRLhPJtJpTlMdKTBjRgTaQBFY=
|
||||
github.com/kevinburke/ssh_config v1.6.0/go.mod h1:q2RIzfka+BXARoNexmF9gkxEX7DmvbW9P4hIVx2Kg4M=
|
||||
github.com/klauspost/compress v1.19.2 h1:hMRETovs/pu/dVWN7zIT1PGG8t509MwT6bO7XSi26R8=
|
||||
github.com/klauspost/compress v1.19.2/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ=
|
||||
github.com/klauspost/cpuid/v2 v2.4.0 h1:S6Hrbc7+ywsr0r+RLapfGBHfyefhCTwEh3A0tV913Dw=
|
||||
github.com/klauspost/cpuid/v2 v2.4.0/go.mod h1:19jmZ9mjzoF//ddRSUsv0zfBTJWh3QJh9FNxZTMrGxU=
|
||||
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
||||
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
||||
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||
github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
|
||||
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
|
||||
github.com/mattn/go-isatty v0.0.24 h1:tGZZoVgT/KiqK1c8ocVLeDS8BSWMRd47J3Lbz7vsReI=
|
||||
@@ -146,10 +108,10 @@ github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3N
|
||||
github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo=
|
||||
github.com/moby/go-archive v0.3.3 h1:OxxR9paxsluYi+zDUEXTTaIxtkK3viymW+Ka7vRhhME=
|
||||
github.com/moby/go-archive v0.3.3/go.mod h1:Npdv43fFqlhZW7Xo8fbm3ZMYFvAGNviUPqX21VERbcE=
|
||||
github.com/moby/moby/api v1.56.0 h1:GQzua3NA599ASSIICx0iFgiJeO9YkdDARvQsm23ZZuQ=
|
||||
github.com/moby/moby/api v1.56.0/go.mod h1:sZ+THbVWkjOmBPPfbnzdD/G1LuIexWhqlSHHPTDQ1Uk=
|
||||
github.com/moby/moby/client v0.6.0 h1:AJjEB21QPbXSXjDsZorFBoDZPhMrfbpaPLgSMAW9Bgs=
|
||||
github.com/moby/moby/client v0.6.0/go.mod h1:OCo00wNRyA3m4lmJ228W3JbyCN4ZNNYjpOXiJydBdcQ=
|
||||
github.com/moby/moby/api v1.56.1 h1:PpWkvVPB7Fr/No8w+TfyJ/I6rWZ2YPZhT3JorIg+06c=
|
||||
github.com/moby/moby/api v1.56.1/go.mod h1:sZ+THbVWkjOmBPPfbnzdD/G1LuIexWhqlSHHPTDQ1Uk=
|
||||
github.com/moby/moby/client v0.6.1 h1:gvfKmdcWw+9MzXYP0vAUKQtBTGu1L8475K2nsmL7O98=
|
||||
github.com/moby/moby/client v0.6.1/go.mod h1:XHgTFqz9NCgS/VuoxXMmEDVmVgXQ4vFEc15wsCIwb24=
|
||||
github.com/moby/patternmatcher v0.6.1 h1:qlhtafmr6kgMIJjKJMDmMWq7WLkKIo23hsrpR3x084U=
|
||||
github.com/moby/patternmatcher v0.6.1/go.mod h1:hDPoyOpDY7OrrMDLaYoY3hf52gNCR/YOUYxkhApJIxc=
|
||||
github.com/moby/sys/mount v0.3.5 h1:eS3fsZTjHaBihwjp4/+5Z3jxqLXYsbwxqpVSfFv3M00=
|
||||
@@ -164,18 +126,12 @@ github.com/moby/sys/userns v0.2.0 h1:nEtDtp7NCV/6dutSklNe8FrENPwFdc4mXnZqC/JWgXM
|
||||
github.com/moby/sys/userns v0.2.0/go.mod h1:IHUYgu/kao6N8YZlp9Cf444ySSvCmDlmzUcYfDHOl28=
|
||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
|
||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
|
||||
github.com/onsi/gomega v1.34.1 h1:EUMJIKUjM8sKjYbtxQI9A4z2o+rruxnzNvpknOXie6k=
|
||||
github.com/onsi/gomega v1.34.1/go.mod h1:kU1QgUvBDLXBJq618Xvm2LUX6rSAfRaFRTcdOeDLwwY=
|
||||
github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=
|
||||
github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=
|
||||
github.com/opencontainers/image-spec v1.1.1 h1:y0fUlFfIZhPF1W537XOLg0/fcx6zcHCJwooC2xJA040=
|
||||
github.com/opencontainers/image-spec v1.1.1/go.mod h1:qpqAh3Dmcf36wStyyWU+kCeDgrGnAve2nCC8+7h8Q0M=
|
||||
github.com/opencontainers/selinux v1.15.1 h1:ERxeh5caJvCzNAKdI8WQbJmB1LDTn4BuaAg8wihLBpA=
|
||||
github.com/opencontainers/selinux v1.15.1/go.mod h1:LenyElirjUHszfxrjuFqC85HIeXZKumHcKMQtnaDlQQ=
|
||||
github.com/pjbgf/sha1cd v0.6.0 h1:3WJ8Wz8gvDz29quX1OcEmkAlUg9diU4GxJHqs0/XiwU=
|
||||
github.com/pjbgf/sha1cd v0.6.0/go.mod h1:lhpGlyHLpQZoxMv8HcgXvZEhcGs0PG/vsZnEJ7H0iCM=
|
||||
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/prometheus/client_golang v1.24.1 h1:JnJkREXzWxUdCuPFpIWZiPispT9xVV59uiuyR2bPlnU=
|
||||
github.com/prometheus/client_golang v1.24.1/go.mod h1:F+oSRECHg4sse5ucfYpYDeIv/hu68Zo0uoHKetWnzcE=
|
||||
@@ -185,20 +141,13 @@ github.com/prometheus/common v0.70.1 h1:1HvjP4D5oL3t8RsPlwxA9onvvStjtIHYE5XuuwOi
|
||||
github.com/prometheus/common v0.70.1/go.mod h1:VdFUQDMZK3VLkurFUVhia6uys/0suUp86TJz5qbJRhc=
|
||||
github.com/prometheus/procfs v0.21.1 h1:GljZCt+zSTS+NZq88cyQ1LjZ+RCHp3uVuabBWA5+OJI=
|
||||
github.com/prometheus/procfs v0.21.1/go.mod h1:aB55Cww9pdSJVHk0hUf0inxWyyjPogFIjmHKYgMKmtY=
|
||||
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
|
||||
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
|
||||
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||
github.com/ryszard/goskiplist v0.0.0-20150312221310-2dfbae5fcf46 h1:GHRpF1pTW19a8tTFrMLUcfWwyC0pnifVo2ClaLq+hP8=
|
||||
github.com/ryszard/goskiplist v0.0.0-20150312221310-2dfbae5fcf46/go.mod h1:uAQ5PCi+MFsC7HjREoAz1BU+Mq60+05gifQSsHSDG/8=
|
||||
github.com/santhosh-tekuri/jsonschema/v6 v6.0.3 h1:1EYB5IzjZawrrnELUi78f9fPu57HuXjmddZPjrls/28=
|
||||
github.com/santhosh-tekuri/jsonschema/v6 v6.0.3/go.mod h1:JXeL+ps8p7/KNMjDQk3TCwPpBy0wYklyWTfbkIzdIFU=
|
||||
github.com/sergi/go-diff v1.4.0 h1:n/SP9D5ad1fORl+llWyN+D6qoUETXNZARKjyY2/KVCw=
|
||||
github.com/sergi/go-diff v1.4.0/go.mod h1:A0bzQcvG0E7Rwjx0REVgAGH58e96+X0MeOfepqsbeW4=
|
||||
github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0=
|
||||
github.com/sirupsen/logrus v1.10.2 h1:G2SED73/qrAu6YwbdxOD6peLkCBI3z7L+ykJFTXJBBo=
|
||||
github.com/sirupsen/logrus v1.10.2/go.mod h1:SLEg8TqYulVKKfIGHldVp2K2aYz2DKSVBq4g/H5bR7Q=
|
||||
github.com/skeema/knownhosts v1.3.2 h1:EDL9mgf4NzwMXCTfaxSD/o/a5fxDw/xL9nkU28JjdBg=
|
||||
github.com/skeema/knownhosts v1.3.2/go.mod h1:bEg3iQAuw+jyiw+484wwFJoKSLwcfd7fqRy+N0QTiow=
|
||||
github.com/spf13/afero v1.2.1 h1:qgMbHoJbPbw579P+1zVY+6n4nIFuIchaIjzZ/I/Yq8M=
|
||||
github.com/spf13/afero v1.2.1/go.mod h1:9ZxEEn6pIJ8Rxe320qSDBk6AsU0r9pR7Q4OcevTdifk=
|
||||
github.com/spf13/cobra v1.10.2 h1:DMTTonx5m65Ic0GOoRY2c16WCbHxOOw6xxezuLaBpcU=
|
||||
@@ -211,8 +160,6 @@ github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSS
|
||||
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
||||
github.com/stretchr/objx v0.5.3 h1:jmXUvGomnU1o3W/V5h2VEradbpJDwGrzugQQvL0POH4=
|
||||
github.com/stretchr/objx v0.5.3/go.mod h1:rDQraq+vQZU7Fde9LOZLr8Tax6zZvy4kuNKF+QYS+U0=
|
||||
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
||||
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
||||
@@ -220,8 +167,6 @@ github.com/stretchr/testify v1.12.1 h1:EuwCh5fleGS7H32xRwO3wRGT7DxrDhLAT6FF8MpWD
|
||||
github.com/stretchr/testify v1.12.1/go.mod h1:MDEgiDPPsNp5cuIrHPPCyornHKgEVbtFUmoNlxoYthg=
|
||||
github.com/timshannon/bolthold v0.0.0-20240314194003-30aac6950928 h1:zjNCuOOhh1TKRU0Ru3PPPJt80z7eReswCao91gBLk00=
|
||||
github.com/timshannon/bolthold v0.0.0-20240314194003-30aac6950928/go.mod h1:PCFYfAEfKT+Nd6zWvUpsXduMR1bXFLf0uGSlEF05MCI=
|
||||
github.com/xanzy/ssh-agent v0.3.3 h1:+/15pJfg/RsTxqYcX6fHqOXZwwMP+2VyYWJeWM2qQFM=
|
||||
github.com/xanzy/ssh-agent v0.3.3/go.mod h1:6dzNDKs0J9rVPHPhaGCukekBHKqfl+L3KghI1Bc68Uw=
|
||||
go.etcd.io/bbolt v1.3.8/go.mod h1:N9Mkw9X8x5fupy0IKsmuqVtoGDyxsaDlbk4Rd05IAQw=
|
||||
go.etcd.io/bbolt v1.5.0 h1:S7GAl7Fxv12yohbwFfIbQCGDWbQbtDGPET4P/bD4lxU=
|
||||
go.etcd.io/bbolt v1.5.0/go.mod h1:mkltfYE5aUHQxUct9N9V+Kp7aSjFqjgrhcXIS70Lrdk=
|
||||
@@ -230,20 +175,22 @@ go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ
|
||||
go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.70.0 h1:LMuyCAyfalSjDyjdC65nK6N0zoTT63+E/u95X0JovZI=
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.70.0/go.mod h1:085m8qbm4hgc8rZWGDEa4vmyyo2c3nPxUslYUKUIU04=
|
||||
go.opentelemetry.io/otel v1.46.0 h1:FHt5/CDyVxi/8IM1CH7VE/rRgq3kLHa2mSTVMO8AWyc=
|
||||
go.opentelemetry.io/otel v1.46.0/go.mod h1:Gj3SEScelsNC45tp4nSxRYlS+f5iez7W8XPMCt905kE=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.46.0 h1:OFnwLJr+pF3iHrlGSzbxyuo6/6HyBlnlN1CWEJmBVcw=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.46.0/go.mod h1:716wFneO0ov19A2beH5hjfh9AK5z/VWNAtDijp1Y0/g=
|
||||
go.opentelemetry.io/otel/metric v1.46.0 h1:yBnkXvgV7AXFILZc5K6IZe/CBFF3OS7BJ8ov6/lj0K8=
|
||||
go.opentelemetry.io/otel/metric v1.46.0/go.mod h1:iPmdWqifKUdzziPkvvzIJXITl56fQx2mGM/DHLB3/2o=
|
||||
go.opentelemetry.io/otel/sdk v1.46.0 h1:h5CNQQjEbuQXY/JfZtgt3i7HVFV3aHPO2OAwO2eTYPI=
|
||||
go.opentelemetry.io/otel/sdk v1.46.0/go.mod h1:GAERFXFt5SYCEB+YiKUbMBeza6UaDH7GmGOZEfh2gSM=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.46.0 h1:0piZ26EG4RBfebb2jhDH6ERCYHoVWduc3kLgPCwSnSE=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.46.0/go.mod h1:I1PbKrdVc8Qu8HYVDNtqVIwLwjNrhsV/uFuxfwg8mO4=
|
||||
go.opentelemetry.io/otel/trace v1.46.0 h1:OULy7ccdJnZtJ0UDYFOIGaCmiWzJ8Vi2G/Rsu60qs1c=
|
||||
go.opentelemetry.io/otel/trace v1.46.0/go.mod h1:J7GAXweO77XSFkB/rmAqk9D6ihszhFjLU+d9WuUxDLI=
|
||||
go.opentelemetry.io/proto/otlp v1.11.0 h1:5rrYs0Ykyj50sdU/JU0x8etU+LubXWb+gED6TbEdMIk=
|
||||
go.opentelemetry.io/proto/otlp v1.11.0/go.mod h1:SmVizdCOAm3XBtG1g1NnOdhW6jtddT72hLMhv8VwA8E=
|
||||
go.opentelemetry.io/otel v1.47.0 h1:j7ALJ/zgkS7Z6aeJW09p8VC9804bC+PpeTfCD4XPnOM=
|
||||
go.opentelemetry.io/otel v1.47.0/go.mod h1:8wS9O2qfXrYrzp6hIF/HOYJJf/wIhFPhR2xLuP+iXQU=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.47.0 h1:julhjPeUH/q/7hinbSdDdqt5h7Zw9YWmRlWRhI0jd54=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.47.0/go.mod h1:Ao2mz688LH/tFf0yMAenidq6k2YNSx6SIY2q6jDACck=
|
||||
go.opentelemetry.io/otel/log v1.47.0 h1:cOTS1CcLbSQeZKanGJ+0JpF/+t4PELi3O3bbl2lqCcI=
|
||||
go.opentelemetry.io/otel/log v1.47.0/go.mod h1:9byitSQ5pLC6PpqwGXjqdMKya6ZTswHRZh2vvXT33nw=
|
||||
go.opentelemetry.io/otel/metric v1.47.0 h1:4PptaldXx3Eat1XjMZ68pPJEs5wrhlemctZE9a3UdWY=
|
||||
go.opentelemetry.io/otel/metric v1.47.0/go.mod h1:ADGSXxRrXM6bjbvLo535EstVFlPpPYZm4LBKixjDHwU=
|
||||
go.opentelemetry.io/otel/sdk v1.47.0 h1:zWXEr4j2lFefG87TU6Yg8a7ngfohIKFZHKp0Hf5hC6I=
|
||||
go.opentelemetry.io/otel/sdk v1.47.0/go.mod h1:VUc24kiOeoGsxG8G9ULx3fWKvB7jMhnGE8Oi607lgR0=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.47.0 h1:lfISg2j93VT6yqdk9OfUaZmw/GfcZqCCV3jdXtsPnKw=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.47.0/go.mod h1:ypLp+mW1Nt2x+Szt3b5/i1syodyts49lMOwxpDI3VGw=
|
||||
go.opentelemetry.io/otel/trace v1.47.0 h1:JOjX/Oci8K94QHddo+bbfya/Ai/nf6/dt9ZfrFNWSrM=
|
||||
go.opentelemetry.io/otel/trace v1.47.0/go.mod h1:jNaSLa2PZEYFG6fRjJABAu+bw4FS08uDmPg28lTghu0=
|
||||
go.opentelemetry.io/proto/otlp v1.11.1 h1:SCrPqH9NE5CmBrOqpjNCiT0X5mGwMyGERXdogfR7Yjw=
|
||||
go.opentelemetry.io/proto/otlp v1.11.1/go.mod h1:wq8W4aSf6bOy8RZyESOeQFr9Pu7wydWX/TY4sgvrTPg=
|
||||
go.shabbyrobe.org/gocovmerge v0.0.0-20230507111327-fa4f82cfbf4d h1:Ns9kd1Rwzw7t0BR8XMphenji4SmIoNZPn8zhYmaVKP8=
|
||||
go.shabbyrobe.org/gocovmerge v0.0.0-20230507111327-fa4f82cfbf4d/go.mod h1:92Uoe3l++MlthCm+koNi0tcUCX3anayogF0Pa/sp24k=
|
||||
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
|
||||
@@ -255,49 +202,26 @@ go.yaml.in/yaml/v3 v3.0.5 h1:N6y/pJk8buWs9NY5ERU2HSMfm+IuD/OtfdAnq6kESPw=
|
||||
go.yaml.in/yaml/v3 v3.0.5/go.mod h1:HVTZu1O7/Vkt2N+BFy8Zza+lnLsABggaTM2ZpNIGuKg=
|
||||
go.yaml.in/yaml/v4 v4.0.0-rc.6 h1:1h7H1ohdUh93/FyE4YaDa1Zh64K6VVbjF4K6WUxMtH4=
|
||||
go.yaml.in/yaml/v4 v4.0.0-rc.6/go.mod h1:aZqd9kCMsGL7AuUv/m/PvWLdg5sjJsZ4oHDEnfPPfY0=
|
||||
golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
|
||||
golang.org/x/crypto v0.57.0 h1:3ZVCjf8Ggz7zneR/EHRVx68Ctf+2pmIMP2UFhh9cC6M=
|
||||
golang.org/x/crypto v0.57.0/go.mod h1:Fdz0i5U6CoizGwLda9DttjSk6qlZo25zYNtR+ycvuZA=
|
||||
golang.org/x/exp v0.0.0-20260410095643-746e56fc9e2f h1:W3F4c+6OLc6H2lb//N1q4WpJkhzJCK5J6kUi1NTVXfM=
|
||||
golang.org/x/exp v0.0.0-20260410095643-746e56fc9e2f/go.mod h1:J1xhfL/vlindoeF/aINzNzt2Bket5bjo9sdOYzOsU80=
|
||||
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
golang.org/x/net v0.59.0 h1:5zfYln+w5XCxwrnMMJPufRgNoXEaGxl0wo5GqPXyues=
|
||||
golang.org/x/net v0.59.0/go.mod h1:2DA/G1UfVbCpQPeWTmMPGY7Cs2PkBkwu743bVX5PIVg=
|
||||
golang.org/x/sync v0.23.0 h1:KameEIfc1IkluZyXWLn39Wd4tURc6GbCiISGiZm2bQk=
|
||||
golang.org/x/sync v0.23.0/go.mod h1:sUUOizhqBxiL6pEWpqNLUiaJn1ShEbZ6BBqskPbjZm0=
|
||||
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/net v0.60.0 h1:79p50tfZlm0J9YfoDsSi639qSXNGVwEzOPLCxM2FsYU=
|
||||
golang.org/x/net v0.60.0/go.mod h1:2DA/G1UfVbCpQPeWTmMPGY7Cs2PkBkwu743bVX5PIVg=
|
||||
golang.org/x/sync v0.24.0 h1:iqiDrGoDgyuvAKrhLQthUEzU/R378N78gXbBjxed7oA=
|
||||
golang.org/x/sync v0.24.0/go.mod h1:sUUOizhqBxiL6pEWpqNLUiaJn1ShEbZ6BBqskPbjZm0=
|
||||
golang.org/x/sys v0.4.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.48.0 h1:bbX/i/6MgT9BVLM9RT1thmxL04yeTAhbEz4SyadbXoo=
|
||||
golang.org/x/sys v0.48.0/go.mod h1:hNLxWAXmnKAxqDtdwIYC4bM9oQPEecfsnNMuSxOs3og=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/sys v0.49.0 h1:XbzkgYJHdqh/8m2Uu0W/dQv8nktxx4BFHp1M0gROTXA=
|
||||
golang.org/x/sys v0.49.0/go.mod h1:hNLxWAXmnKAxqDtdwIYC4bM9oQPEecfsnNMuSxOs3og=
|
||||
golang.org/x/term v0.46.0 h1:3+OXuTbaKDgwk8jTi3aSLHRlmWqHEUDUtxnbFigO4YE=
|
||||
golang.org/x/term v0.46.0/go.mod h1:+K02xbkittuwc0Am4abfA3Fc+XRGXkvBXNO88NCXPoc=
|
||||
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.42.0 h1:JbOZXgfeCPU9gacVtYliJqOhD+zhrEqK4LfdpmlUZqI=
|
||||
golang.org/x/text v0.42.0/go.mod h1:ojzP1Z+2QtioaF8DTtO8K5q7JWVVYwZKenzujK0Zd0E=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.49.0 h1:3NI7VXzL9+1WZD52Dx2ttoPwD5DWrFGpl9mFZDlmisI=
|
||||
golang.org/x/tools v0.49.0/go.mod h1:SJNXV9DBKT0UbdttsQjbfJlAE/q+y36++zo3uL3N0Oo=
|
||||
google.golang.org/protobuf v1.36.12 h1:pJOKDDOyeXErUroCihFAd5LQuwXBSpVnKGrj5o/fwxc=
|
||||
google.golang.org/protobuf v1.36.12/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
||||
gopkg.in/mgo.v2 v2.0.0-20180705113604-9856a29383ce h1:xcEWjVhvbDy+nHP67nPDDpbYrY+ILlfndk4bRioVHaU=
|
||||
gopkg.in/mgo.v2 v2.0.0-20180705113604-9856a29383ce/go.mod h1:yeKp02qBN3iKW1OzL3MGk2IdtZzaj7SFntXj72NppTA=
|
||||
gopkg.in/warnings.v0 v0.1.2 h1:wFXVbFY8DY5/xOe1ECiWdKCzZlxgshcYVNkBHstARME=
|
||||
gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI=
|
||||
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gotest.tools/v3 v3.5.2 h1:7koQfIKdy+I8UTetycgUqXWSDwpgv193Ka+qRsmBY8Q=
|
||||
gotest.tools/v3 v3.5.2/go.mod h1:LtdLGcnqToBH83WByAAi/wiwSFCArdFIUV/xxN4pcjA=
|
||||
|
||||
@@ -8,10 +8,10 @@ import (
|
||||
"fmt"
|
||||
|
||||
"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/pkg/config"
|
||||
|
||||
log "github.com/sirupsen/logrus"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
@@ -60,13 +60,13 @@ func runCacheServer(configFile *string, cacheArgs *cacheServerArgs) func(cmd *co
|
||||
Port: port,
|
||||
InternalSecret: secret,
|
||||
Policy: run.CachePolicy(cfg),
|
||||
Logger: log.StandardLogger().WithField("module", "cache_request"),
|
||||
Logger: logging.Default().With("module", "cache_request"),
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
log.Infof("cache server is listening on %v", cacheHandler.ExternalURL())
|
||||
logging.Infof("cache server is listening on %v", cacheHandler.ExternalURL())
|
||||
|
||||
<-cmd.Context().Done()
|
||||
|
||||
|
||||
+73
-38
@@ -5,8 +5,10 @@ package cmd
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
@@ -16,6 +18,7 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"gitea.com/gitea/runner/act/common/logging"
|
||||
"gitea.com/gitea/runner/internal/app/poll"
|
||||
"gitea.com/gitea/runner/internal/app/run"
|
||||
"gitea.com/gitea/runner/internal/pkg/client"
|
||||
@@ -29,8 +32,9 @@ import (
|
||||
"gitea.com/gitea/runner/internal/pkg/ver"
|
||||
|
||||
"connectrpc.com/connect"
|
||||
runnerv1 "gitea.dev/actionslib/runner/v1"
|
||||
"github.com/avast/retry-go/v5"
|
||||
"github.com/mattn/go-isatty"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
@@ -42,11 +46,11 @@ func runDaemon(ctx context.Context, daemArgs *daemonArgs, configFile *string) fu
|
||||
}
|
||||
|
||||
initLogging(cfg)
|
||||
log.Infoln("Starting runner daemon")
|
||||
logging.Info("Starting runner daemon")
|
||||
|
||||
reg, err := config.LoadRegistration(cfg.Runner.File)
|
||||
if os.IsNotExist(err) {
|
||||
log.Error("registration file not found, please register the runner first")
|
||||
logging.Error("registration file not found, please register the runner first")
|
||||
return err
|
||||
} else if err != nil {
|
||||
return fmt.Errorf("failed to load registration file: %w", err)
|
||||
@@ -58,13 +62,13 @@ func runDaemon(ctx context.Context, daemArgs *daemonArgs, configFile *string) fu
|
||||
releaseLock, err := lock.TryLock(cfg.Runner.File)
|
||||
switch {
|
||||
case errors.Is(err, lock.ErrLocked):
|
||||
log.Errorf("another gitea-runner process is already using %q; each runner process needs its own runner file (runner.file)", cfg.Runner.File)
|
||||
logging.Errorf("another gitea-runner process is already using %q; each runner process needs its own runner file (runner.file)", cfg.Runner.File)
|
||||
return err
|
||||
case err != nil:
|
||||
// Best-effort guard: if the lock file can't be created (e.g. a
|
||||
// read-only runner-file mount), warn and start anyway rather than
|
||||
// refusing to run.
|
||||
log.Warnf("could not lock runner file %q, continuing without the single-process guard: %v", cfg.Runner.File, err)
|
||||
logging.Warnf("could not lock runner file %q, continuing without the single-process guard: %v", cfg.Runner.File, err)
|
||||
default:
|
||||
// Held until shutdown finishes: the draining runner still owns this
|
||||
// identity on the server, so releasing early would let a restart
|
||||
@@ -74,13 +78,13 @@ func runDaemon(ctx context.Context, daemArgs *daemonArgs, configFile *string) fu
|
||||
|
||||
shutdownTelemetry, err := telemetry.Setup(ctx, reg.UUID, reg.Name, metrics.Registry)
|
||||
if err != nil {
|
||||
log.WithError(err).Warn("OpenTelemetry export failed to start")
|
||||
logging.WithError(err).Warn("OpenTelemetry export failed to start")
|
||||
}
|
||||
defer func() {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
defer cancel()
|
||||
if err := shutdownTelemetry(ctx); err != nil {
|
||||
log.WithError(err).Warn("OpenTelemetry export failed to flush")
|
||||
logging.WithError(err).Warn("OpenTelemetry export failed to flush")
|
||||
}
|
||||
}()
|
||||
|
||||
@@ -90,13 +94,13 @@ func runDaemon(ctx context.Context, daemArgs *daemonArgs, configFile *string) fu
|
||||
for _, l := range lbls {
|
||||
label, err := labels.Parse(l)
|
||||
if err != nil {
|
||||
log.WithError(err).Warnf("ignored invalid label %q", l)
|
||||
logging.WithError(err).Warnf("ignored invalid label %q", l)
|
||||
continue
|
||||
}
|
||||
ls = append(ls, label)
|
||||
}
|
||||
if len(ls) == 0 {
|
||||
log.Warn("no labels configured, runner may not be able to pick up jobs")
|
||||
logging.Warn("no labels configured, runner may not be able to pick up jobs")
|
||||
}
|
||||
|
||||
// Before the first Docker API call: the standard library resolves the proxy
|
||||
@@ -104,7 +108,7 @@ func runDaemon(ctx context.Context, daemArgs *daemonArgs, configFile *string) fu
|
||||
if dockerSocketPath, err := getDockerSocketPath(cfg.Container.DockerHost); err == nil {
|
||||
run.BypassProxyForDockerHost(dockerSocketPath)
|
||||
} else {
|
||||
log.Debugf("cannot resolve the docker socket path, so Docker API calls are not exempted from the proxy: %v", err)
|
||||
logging.Debugf("cannot resolve the docker socket path, so Docker API calls are not exempted from the proxy: %v", err)
|
||||
}
|
||||
|
||||
if ls.RequireDocker() || cfg.Container.RequireDocker {
|
||||
@@ -116,20 +120,20 @@ func runDaemon(ctx context.Context, daemArgs *daemonArgs, configFile *string) fu
|
||||
for keepRunning {
|
||||
dockerSocketPath, err := getDockerSocketPath(cfg.Container.DockerHost)
|
||||
if err != nil {
|
||||
log.Errorf("Failed to get socket path: %s", err.Error())
|
||||
logging.Errorf("Failed to get socket path: %s", err.Error())
|
||||
} else if err = envcheck.CheckIfDockerRunning(tctx, dockerSocketPath); errors.Is(err, context.Canceled) {
|
||||
log.Infof("Docker wait timeout of %s expired", timeout.String())
|
||||
logging.Infof("Docker wait timeout of %s expired", timeout.String())
|
||||
break
|
||||
} else if err != nil {
|
||||
log.Errorf("Docker connection failed: %s", err.Error())
|
||||
logging.Errorf("Docker connection failed: %s", err.Error())
|
||||
} else {
|
||||
log.Infof("Docker is ready")
|
||||
logging.Infof("Docker is ready")
|
||||
break
|
||||
}
|
||||
select {
|
||||
case <-time.After(time.Second):
|
||||
case <-tctx.Done():
|
||||
log.Infof("Docker wait timeout of %s expired", timeout.String())
|
||||
logging.Infof("Docker wait timeout of %s expired", timeout.String())
|
||||
keepRunning = false
|
||||
}
|
||||
}
|
||||
@@ -165,7 +169,7 @@ func runDaemon(ctx context.Context, daemArgs *daemonArgs, configFile *string) fu
|
||||
if err := config.SaveRegistration(cfg.Runner.File, reg); err != nil {
|
||||
return fmt.Errorf("failed to save runner config: %w", err)
|
||||
}
|
||||
log.Infof("labels updated to: %v", reg.Labels)
|
||||
logging.Infof("labels updated to: %v", reg.Labels)
|
||||
}
|
||||
|
||||
cli := client.New(
|
||||
@@ -180,24 +184,41 @@ func runDaemon(ctx context.Context, daemArgs *daemonArgs, configFile *string) fu
|
||||
runner := run.NewRunner(cfg, reg, cli)
|
||||
defer func() {
|
||||
if err := runner.Close(); err != nil {
|
||||
log.Warnf("runner %s: cache server shutdown: %v", reg.Name, err)
|
||||
logging.Warnf("runner %s: cache server shutdown: %v", reg.Name, err)
|
||||
}
|
||||
}()
|
||||
|
||||
// declare the labels of the runner before fetching tasks
|
||||
resp, err := runner.Declare(ctx, ls.Names())
|
||||
resp, err := retry.NewWithData[*connect.Response[runnerv1.DeclareResponse]](
|
||||
retry.Context(ctx),
|
||||
retry.UntilSucceeded(),
|
||||
retry.Delay(declareRetryDelay),
|
||||
retry.DelayType(retry.FixedDelay),
|
||||
retry.RetryIf(shouldRetryDeclare),
|
||||
retry.OnRetry(func(attempt uint, err error) {
|
||||
if attempt == 0 {
|
||||
logging.WithError(err).Warn("fail to invoke Declare, retrying until Gitea is reachable")
|
||||
} else {
|
||||
logging.WithError(err).Debug("fail to invoke Declare, retrying")
|
||||
}
|
||||
}),
|
||||
).Do(func() (*connect.Response[runnerv1.DeclareResponse], error) {
|
||||
return runner.Declare(ctx, ls.Names())
|
||||
})
|
||||
switch {
|
||||
case err != nil && ctx.Err() != nil && !client.IsRegistrationRejected(err):
|
||||
return nil
|
||||
case err != nil && connect.CodeOf(err) == connect.CodeUnimplemented:
|
||||
log.Errorf("Your Gitea version is too old to support runner declare, please upgrade to v1.21 or later")
|
||||
logging.Errorf("Your Gitea version is too old to support runner declare, please upgrade to v1.21 or later")
|
||||
return err
|
||||
case err != nil:
|
||||
log.WithError(err).Error("fail to invoke Declare")
|
||||
logging.WithError(err).Error("fail to invoke Declare")
|
||||
if client.IsRegistrationRejected(err) {
|
||||
removeEphemeralRegistration(reg, cfg.Runner.File)
|
||||
}
|
||||
return err
|
||||
default:
|
||||
log.Infof("runner: %s, with version: %s, with labels: %v, declare successfully",
|
||||
logging.Infof("runner: %s, with version: %s, with labels: %v, declare successfully",
|
||||
resp.Msg.Runner.Name, resp.Msg.Runner.Version, resp.Msg.Runner.Labels)
|
||||
}
|
||||
runner.SetCapabilitiesFromDeclare(resp)
|
||||
@@ -247,14 +268,14 @@ func runDaemon(ctx context.Context, daemArgs *daemonArgs, configFile *string) fu
|
||||
}
|
||||
}
|
||||
|
||||
log.Infof("runner: %s shutdown initiated, waiting %s for running jobs to complete before shutting down", resp.Msg.Runner.Name, cfg.Runner.ShutdownTimeout)
|
||||
logging.Infof("runner: %s shutdown initiated, waiting %s for running jobs to complete before shutting down", resp.Msg.Runner.Name, cfg.Runner.ShutdownTimeout)
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), cfg.Runner.ShutdownTimeout)
|
||||
defer cancel()
|
||||
|
||||
err = poller.Shutdown(ctx)
|
||||
if err != nil {
|
||||
log.Warnf("runner: %s cancelled in progress jobs during shutdown", resp.Msg.Runner.Name)
|
||||
logging.Warnf("runner: %s cancelled in progress jobs during shutdown", resp.Msg.Runner.Name)
|
||||
}
|
||||
|
||||
if poller.RegistrationRejected() {
|
||||
@@ -274,9 +295,9 @@ func removeEphemeralRegistration(reg *config.Registration, file string) {
|
||||
return
|
||||
}
|
||||
if err := os.Remove(file); err == nil {
|
||||
log.Infof("removed the spent ephemeral registration %q", file)
|
||||
logging.Infof("removed the spent ephemeral registration %q", file)
|
||||
} else if !os.IsNotExist(err) {
|
||||
log.WithError(err).Warn("failed to remove the spent ephemeral registration")
|
||||
logging.WithError(err).Warn("failed to remove the spent ephemeral registration")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -307,7 +328,7 @@ func splitLabels(s string) []string {
|
||||
return lbls
|
||||
}
|
||||
|
||||
// initLogging setup the global logrus logger.
|
||||
// initLogging sets up the process-wide logger.
|
||||
func initLogging(cfg *config.Config) {
|
||||
callPrettyfier := func(f *runtime.Frame) (string, string) {
|
||||
// get function name
|
||||
@@ -320,35 +341,49 @@ func initLogging(cfg *config.Config) {
|
||||
}
|
||||
|
||||
isTerm := isatty.IsTerminal(os.Stdout.Fd())
|
||||
format := &log.TextFormatter{
|
||||
logging.SetTextFormat(logging.TextOptions{
|
||||
DisableColors: !isTerm,
|
||||
FullTimestamp: true,
|
||||
CallerPrettyfier: callPrettyfier,
|
||||
}
|
||||
log.SetFormatter(report.MaskingFormatter(format))
|
||||
Filter: report.MaskGlobal,
|
||||
})
|
||||
|
||||
l := cfg.Log.Level
|
||||
if l == "" {
|
||||
log.Infof("Log level not set, sticking to info")
|
||||
logging.Infof("Log level not set, sticking to info")
|
||||
return
|
||||
}
|
||||
|
||||
level, err := log.ParseLevel(l)
|
||||
level, err := logging.ParseLevel(l)
|
||||
if err != nil {
|
||||
log.WithError(err).
|
||||
logging.WithError(err).
|
||||
Errorf("invalid log level: %q", l)
|
||||
}
|
||||
|
||||
// debug level
|
||||
switch level {
|
||||
case log.DebugLevel, log.TraceLevel:
|
||||
log.SetReportCaller(true) // Only in debug or trace because it takes a performance toll
|
||||
log.Infof("Log level %s requested, setting up report caller for further debugging", level)
|
||||
case logging.LevelDebug, logging.LevelTrace:
|
||||
logging.SetReportCaller(true) // Only in debug or trace because it takes a performance toll
|
||||
logging.Infof("Log level %s requested, setting up report caller for further debugging", level)
|
||||
}
|
||||
|
||||
if log.GetLevel() != level {
|
||||
log.Infof("log level set to %v", level)
|
||||
log.SetLevel(level)
|
||||
if logging.GetLevel() != level {
|
||||
logging.Infof("log level set to %v", level)
|
||||
logging.SetLevel(level)
|
||||
}
|
||||
}
|
||||
|
||||
var declareRetryDelay = 30 * time.Second
|
||||
|
||||
func shouldRetryDeclare(err error) bool {
|
||||
if errors.As(err, new(*tls.CertificateVerificationError)) || errors.As(err, new(tls.AlertError)) || errors.Is(err, http.ErrSchemeMismatch) { // misconfigurations never heal
|
||||
return false
|
||||
}
|
||||
switch connect.CodeOf(err) {
|
||||
case connect.CodeUnauthenticated, connect.CodeUnimplemented, connect.CodePermissionDenied, connect.CodeInvalidArgument:
|
||||
return false
|
||||
default:
|
||||
return !client.IsRegistrationRejected(err)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -5,21 +5,23 @@ package cmd
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"errors"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"sync/atomic"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"gitea.com/gitea/runner/act/common/logging"
|
||||
"gitea.com/gitea/runner/internal/pkg/config"
|
||||
"gitea.com/gitea/runner/internal/pkg/report"
|
||||
|
||||
"connectrpc.com/connect"
|
||||
runnerv1 "gitea.dev/actionslib/runner/v1"
|
||||
"gitea.dev/actionslib/runner/v1/runnerv1connect"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"github.com/stretchr/testify/require"
|
||||
"google.golang.org/protobuf/types/known/structpb"
|
||||
)
|
||||
@@ -62,24 +64,23 @@ func TestGetDockerSocketPathUsesConfigAndEnvironment(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestInitLoggingSetsLevelAndCaller(t *testing.T) {
|
||||
oldLevel := log.GetLevel()
|
||||
oldReportCaller := log.StandardLogger().ReportCaller
|
||||
oldLevel, oldOut := logging.GetLevel(), logging.Output()
|
||||
t.Cleanup(func() {
|
||||
log.SetLevel(oldLevel)
|
||||
log.SetReportCaller(oldReportCaller)
|
||||
logging.SetLevel(oldLevel)
|
||||
logging.SetOutput(oldOut)
|
||||
logging.SetReportCaller(false)
|
||||
logging.SetTextFormat(logging.TextOptions{})
|
||||
})
|
||||
|
||||
oldFormatter := log.StandardLogger().Formatter
|
||||
t.Cleanup(func() { log.SetFormatter(oldFormatter) })
|
||||
|
||||
cfg := &config.Config{}
|
||||
cfg.Log.Level = "debug"
|
||||
initLogging(cfg)
|
||||
|
||||
require.Equal(t, log.DebugLevel, log.GetLevel())
|
||||
require.True(t, log.StandardLogger().ReportCaller)
|
||||
// act plans a job on this logger, so a live task's secrets have to be masked out of it
|
||||
require.IsType(t, report.MaskingFormatter(nil), log.StandardLogger().Formatter)
|
||||
require.Equal(t, logging.LevelDebug, logging.GetLevel())
|
||||
var buf strings.Builder
|
||||
logging.SetOutput(&buf)
|
||||
logging.Debugf("line")
|
||||
require.Contains(t, buf.String(), "[daemon_test.go:")
|
||||
}
|
||||
|
||||
var errUnregisteredRunner = errors.New("rpc error: code = Unauthenticated desc = unregistered runner")
|
||||
@@ -87,12 +88,19 @@ var errUnregisteredRunner = errors.New("rpc error: code = Unauthenticated desc =
|
||||
type daemonTestService struct {
|
||||
runnerv1connect.UnimplementedRunnerServiceHandler
|
||||
declareErr, fetchErr error
|
||||
declareCalls atomic.Int32
|
||||
beforeTask bool
|
||||
cancel context.CancelFunc
|
||||
}
|
||||
|
||||
func (s *daemonTestService) Declare(context.Context, *connect.Request[runnerv1.DeclareRequest]) (*connect.Response[runnerv1.DeclareResponse], error) {
|
||||
if s.declareCalls.Add(1) == 1 {
|
||||
return nil, connect.NewError(connect.CodeUnavailable, errors.New("bad gateway"))
|
||||
}
|
||||
if s.declareErr != nil {
|
||||
if s.beforeTask {
|
||||
s.cancel()
|
||||
}
|
||||
return nil, s.declareErr
|
||||
}
|
||||
return connect.NewResponse(&runnerv1.DeclareResponse{Runner: &runnerv1.Runner{Name: "test"}}), nil
|
||||
@@ -117,7 +125,9 @@ func (*daemonTestService) UpdateLog(_ context.Context, req *connect.Request[runn
|
||||
return connect.NewResponse(&runnerv1.UpdateLogResponse{AckIndex: req.Msg.Index + int64(len(req.Msg.Rows))}), nil
|
||||
}
|
||||
|
||||
func TestDaemonRemovesOnlyConsumedEphemeralRegistration(t *testing.T) {
|
||||
func TestDaemonRetriesDeclareOutagesAndRemovesOnlyConsumedEphemeralRegistration(t *testing.T) {
|
||||
defer func(delay time.Duration) { declareRetryDelay = delay }(declareRetryDelay)
|
||||
declareRetryDelay = 0
|
||||
for _, tc := range []struct {
|
||||
name string
|
||||
ephemeral, beforeTask bool
|
||||
@@ -132,11 +142,13 @@ func TestDaemonRemovesOnlyConsumedEphemeralRegistration(t *testing.T) {
|
||||
{"ephemeral proxy 401 on fetch", true, false, nil, connect.NewError(connect.CodeUnauthenticated, errors.New("HTTP status 401 Unauthorized")), false},
|
||||
{"ephemeral rejected on declare", true, false, errUnregisteredRunner, nil, true},
|
||||
{"persistent rejected on declare", false, false, errUnregisteredRunner, nil, false},
|
||||
{"ephemeral stopped during declare outage", true, true, connect.NewError(connect.CodeUnavailable, errors.New("bad gateway")), nil, false},
|
||||
} {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
ctx, cancel := context.WithTimeout(t.Context(), 5*time.Second)
|
||||
defer cancel()
|
||||
_, handler := runnerv1connect.NewRunnerServiceHandler(&daemonTestService{declareErr: tc.declareErr, fetchErr: tc.fetchErr, beforeTask: tc.beforeTask, cancel: cancel})
|
||||
service := &daemonTestService{declareErr: tc.declareErr, fetchErr: tc.fetchErr, beforeTask: tc.beforeTask, cancel: cancel}
|
||||
_, handler := runnerv1connect.NewRunnerServiceHandler(service)
|
||||
server := httptest.NewServer(http.StripPrefix("/api/actions", handler))
|
||||
defer server.Close()
|
||||
dir := t.TempDir()
|
||||
@@ -145,11 +157,12 @@ func TestDaemonRemovesOnlyConsumedEphemeralRegistration(t *testing.T) {
|
||||
require.NoError(t, os.WriteFile(configFile, []byte("runner:\n file: "+regFile+"\n idle_cleanup_interval: 0s\ncache:\n enabled: false\n"), 0o600))
|
||||
require.NoError(t, config.SaveRegistration(regFile, &config.Registration{Address: server.URL, UUID: "test", Name: "test", Token: "test", Labels: []string{"host:host"}, Ephemeral: tc.ephemeral}))
|
||||
err := runDaemon(ctx, &daemonArgs{Once: true}, &configFile)(nil, nil)
|
||||
if tc.declareErr == nil && tc.fetchErr == nil {
|
||||
if tc.declareErr == nil && tc.fetchErr == nil || tc.beforeTask {
|
||||
require.NoError(t, err)
|
||||
} else {
|
||||
require.Error(t, err)
|
||||
}
|
||||
require.Equal(t, int32(2), service.declareCalls.Load())
|
||||
if tc.removed {
|
||||
require.NoFileExists(t, regFile)
|
||||
} else {
|
||||
@@ -158,3 +171,27 @@ func TestDaemonRemovesOnlyConsumedEphemeralRegistration(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestShouldRetryDeclare(t *testing.T) {
|
||||
for _, tc := range []struct {
|
||||
err error
|
||||
retry bool
|
||||
}{
|
||||
{connect.NewError(connect.CodeUnavailable, errors.New("bad gateway")), true},
|
||||
{connect.NewWireError(connect.CodeInternal, errors.New("update runner")), true},
|
||||
{connect.NewError(connect.CodeUnavailable, &tls.CertificateVerificationError{Err: errors.New("unknown authority")}), false},
|
||||
{connect.NewError(connect.CodeUnavailable, http.ErrSchemeMismatch), false},
|
||||
{connect.NewError(connect.CodeUnavailable, tls.AlertError(42)), false},
|
||||
{connect.NewError(connect.CodePermissionDenied, errors.New("forbidden")), false},
|
||||
{connect.NewWireError(connect.CodeUnknown, errors.New("unregistered runner")), false},
|
||||
} {
|
||||
require.Equal(t, tc.retry, shouldRetryDeclare(tc.err), tc.err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
// A dependency that reconfigures logrus's global logger when it is loaded makes its own log lines
|
||||
// show up twice or not at all, so this fails when upgrading one starts doing that. It does not
|
||||
// cover a dependency that does so later, while running.
|
||||
func TestLogrusBridgeSurvivesDependencies(t *testing.T) {
|
||||
require.True(t, logging.LogrusBridgeIntact())
|
||||
}
|
||||
+22
-20
@@ -23,6 +23,7 @@ import (
|
||||
"gitea.com/gitea/runner/act/artifactcache"
|
||||
"gitea.com/gitea/runner/act/artifacts"
|
||||
"gitea.com/gitea/runner/act/common"
|
||||
"gitea.com/gitea/runner/act/common/logging"
|
||||
"gitea.com/gitea/runner/act/runner"
|
||||
"gitea.com/gitea/runner/internal/app/run"
|
||||
"gitea.com/gitea/runner/internal/pkg/config"
|
||||
@@ -30,7 +31,6 @@ import (
|
||||
"gitea.dev/actionslib/pkg/model"
|
||||
"github.com/joho/godotenv"
|
||||
"github.com/moby/moby/api/types/container"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"github.com/spf13/cobra"
|
||||
"golang.org/x/term"
|
||||
)
|
||||
@@ -105,7 +105,7 @@ func (i *executeArgs) LoadSecrets() map[string]string {
|
||||
secretPairParts := strings.SplitN(secretPair, "=", 2)
|
||||
secretPairParts[0] = strings.ToUpper(secretPairParts[0])
|
||||
if strings.EqualFold(s[secretPairParts[0]], secretPairParts[0]) {
|
||||
log.Errorf("Secret %s is already defined (secrets are case insensitive)", secretPairParts[0])
|
||||
logging.Errorf("Secret %s is already defined (secrets are case insensitive)", secretPairParts[0])
|
||||
}
|
||||
if len(secretPairParts) == 2 {
|
||||
s[secretPairParts[0]] = secretPairParts[1]
|
||||
@@ -116,7 +116,7 @@ func (i *executeArgs) LoadSecrets() map[string]string {
|
||||
val, err := term.ReadPassword(int(os.Stdin.Fd()))
|
||||
fmt.Println()
|
||||
if err != nil {
|
||||
log.Errorf("failed to read input: %v", err)
|
||||
logging.Errorf("failed to read input: %v", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
s[secretPairParts[0]] = string(val)
|
||||
@@ -129,7 +129,8 @@ func readEnvs(path string, envs map[string]string) bool {
|
||||
if _, err := os.Stat(path); err == nil {
|
||||
env, err := godotenv.Read(path)
|
||||
if err != nil {
|
||||
log.Fatalf("Error loading from %s: %v", path, err)
|
||||
logging.Errorf("Error loading from %s: %v", path, err)
|
||||
os.Exit(1)
|
||||
}
|
||||
maps.Copy(envs, env)
|
||||
return true
|
||||
@@ -220,7 +221,8 @@ func (i *executeArgs) Workdir() string {
|
||||
func (i *executeArgs) resolve(path string) string {
|
||||
basedir, err := filepath.Abs(i.workdir)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
logging.Error(err)
|
||||
os.Exit(1)
|
||||
}
|
||||
if path == "" {
|
||||
return path
|
||||
@@ -336,7 +338,7 @@ func runExecList(planner model.WorkflowPlanner, execArgs *executeArgs) error {
|
||||
var filterEventName string
|
||||
|
||||
if len(execArgs.event) > 0 {
|
||||
log.Infof("Using chosed event for filtering: %s", execArgs.event)
|
||||
logging.Infof("Using chosed event for filtering: %s", execArgs.event)
|
||||
filterEventName = execArgs.event
|
||||
} else if execArgs.autodetectEvent {
|
||||
// collect all events from loaded workflows
|
||||
@@ -344,7 +346,7 @@ func runExecList(planner model.WorkflowPlanner, execArgs *executeArgs) error {
|
||||
|
||||
// set default event type to first event from many available
|
||||
// this way user dont have to specify the event.
|
||||
log.Infof("Using first detected workflow event for filtering: %s", events[0])
|
||||
logging.Infof("Using first detected workflow event for filtering: %s", events[0])
|
||||
|
||||
filterEventName = events[0]
|
||||
}
|
||||
@@ -352,19 +354,19 @@ func runExecList(planner model.WorkflowPlanner, execArgs *executeArgs) error {
|
||||
var err error
|
||||
switch {
|
||||
case execArgs.job != "":
|
||||
log.Infof("Preparing plan with a job: %s", execArgs.job)
|
||||
logging.Infof("Preparing plan with a job: %s", execArgs.job)
|
||||
filterPlan, err = planner.PlanJob(execArgs.job)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
case filterEventName != "":
|
||||
log.Infof("Preparing plan for a event: %s", filterEventName)
|
||||
logging.Infof("Preparing plan for a event: %s", filterEventName)
|
||||
filterPlan, err = planner.PlanEvent(filterEventName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
default:
|
||||
log.Infof("Preparing plan with all jobs")
|
||||
logging.Infof("Preparing plan with all jobs")
|
||||
filterPlan, err = planner.PlanAll()
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -444,30 +446,30 @@ func runExec(ctx context.Context, execArgs *executeArgs) func(cmd *cobra.Command
|
||||
|
||||
switch {
|
||||
case len(execArgs.event) > 0:
|
||||
log.Infof("Using chosed event for filtering: %s", execArgs.event)
|
||||
logging.Infof("Using chosed event for filtering: %s", execArgs.event)
|
||||
eventName = execArgs.event
|
||||
case len(events) == 1 && len(events[0]) > 0:
|
||||
log.Infof("Using the only detected workflow event: %s", events[0])
|
||||
logging.Infof("Using the only detected workflow event: %s", events[0])
|
||||
eventName = events[0]
|
||||
case execArgs.autodetectEvent && len(events) > 0 && len(events[0]) > 0:
|
||||
// set default event type to first event from many available
|
||||
// this way user dont have to specify the event.
|
||||
log.Infof("Using first detected workflow event: %s", events[0])
|
||||
logging.Infof("Using first detected workflow event: %s", events[0])
|
||||
eventName = events[0]
|
||||
default:
|
||||
log.Infof("Using default workflow event: push")
|
||||
logging.Infof("Using default workflow event: push")
|
||||
eventName = "push"
|
||||
}
|
||||
|
||||
// build the plan for this run
|
||||
if execArgs.job != "" {
|
||||
log.Infof("Planning job: %s", execArgs.job)
|
||||
logging.Infof("Planning job: %s", execArgs.job)
|
||||
plan, err = planner.PlanJob(execArgs.job)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
log.Infof("Planning jobs for event: %s", eventName)
|
||||
logging.Infof("Planning jobs for event: %s", eventName)
|
||||
plan, err = planner.PlanEvent(eventName)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -482,12 +484,12 @@ func runExec(ctx context.Context, execArgs *executeArgs) func(cmd *cobra.Command
|
||||
// init a cache server
|
||||
handler, err := artifactcache.StartHandler(artifactcache.Options{
|
||||
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 {
|
||||
return err
|
||||
}
|
||||
log.Infof("cache handler listens on: %v", handler.ExternalURL())
|
||||
logging.Infof("cache handler listens on: %v", handler.ExternalURL())
|
||||
execArgs.cacheHandler = handler
|
||||
|
||||
if len(execArgs.artifactServerAddr) == 0 {
|
||||
@@ -553,7 +555,7 @@ func runExec(ctx context.Context, execArgs *executeArgs) func(cmd *cobra.Command
|
||||
}
|
||||
|
||||
if !execArgs.debug {
|
||||
logLevel := log.InfoLevel
|
||||
logLevel := logging.LevelInfo
|
||||
config.JobLoggerLevel = &logLevel
|
||||
}
|
||||
|
||||
@@ -563,7 +565,7 @@ func runExec(ctx context.Context, execArgs *executeArgs) func(cmd *cobra.Command
|
||||
}
|
||||
|
||||
artifactCancel := artifacts.Serve(ctx, execArgs.artifactServerPath, execArgs.artifactServerAddr, execArgs.artifactServerPort)
|
||||
log.Debugf("artifacts server started at %s:%s", execArgs.artifactServerPath, execArgs.artifactServerPort)
|
||||
logging.Debugf("artifacts server started at %s:%s", execArgs.artifactServerPath, execArgs.artifactServerPort)
|
||||
|
||||
ctx = common.WithDryrun(ctx, execArgs.dryrun)
|
||||
executor := r.NewPlanExecutor(plan).Finally(func(ctx context.Context) error {
|
||||
|
||||
@@ -5,7 +5,6 @@ package cmd
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
@@ -217,7 +216,7 @@ func TestPrintListReportsDuplicateJobIDs(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestLoadExecCmdDefinesExpectedFlags(t *testing.T) {
|
||||
cmd := loadExecCmd(context.Background())
|
||||
cmd := loadExecCmd(t.Context())
|
||||
|
||||
for _, name := range []string{
|
||||
"list",
|
||||
|
||||
@@ -13,6 +13,7 @@ import (
|
||||
goruntime "runtime"
|
||||
"strings"
|
||||
|
||||
"gitea.com/gitea/runner/act/common/logging"
|
||||
"gitea.com/gitea/runner/internal/app/run"
|
||||
"gitea.com/gitea/runner/internal/pkg/client"
|
||||
"gitea.com/gitea/runner/internal/pkg/config"
|
||||
@@ -24,28 +25,27 @@ import (
|
||||
pingv1 "gitea.dev/actionslib/ping/v1"
|
||||
runnerv1 "gitea.dev/actionslib/runner/v1"
|
||||
"github.com/mattn/go-isatty"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
// runRegister registers a runner to the server
|
||||
func runRegister(ctx context.Context, regArgs *registerArgs, configFile *string) func(*cobra.Command, []string) error {
|
||||
return func(cmd *cobra.Command, args []string) error {
|
||||
log.SetReportCaller(false)
|
||||
logging.SetReportCaller(false)
|
||||
isTerm := isatty.IsTerminal(os.Stdout.Fd())
|
||||
log.SetFormatter(&log.TextFormatter{
|
||||
logging.SetTextFormat(logging.TextOptions{
|
||||
DisableColors: !isTerm,
|
||||
DisableTimestamp: true,
|
||||
})
|
||||
log.SetLevel(log.DebugLevel)
|
||||
logging.SetLevel(logging.LevelDebug)
|
||||
|
||||
log.Infof("Registering runner, arch=%s, os=%s, version=%s.",
|
||||
logging.Infof("Registering runner, arch=%s, os=%s, version=%s.",
|
||||
goruntime.GOARCH, goruntime.GOOS, ver.Version())
|
||||
|
||||
// runner always needs root permission
|
||||
if os.Getuid() != 0 {
|
||||
// TODO: use a better way to check root permission
|
||||
log.Warnf("Runner in user-mode.")
|
||||
logging.Warnf("Runner in user-mode.")
|
||||
}
|
||||
|
||||
if regArgs.NoInteractive {
|
||||
@@ -55,7 +55,8 @@ func runRegister(ctx context.Context, regArgs *registerArgs, configFile *string)
|
||||
} else {
|
||||
go func() {
|
||||
if err := registerInteractive(ctx, *configFile, regArgs); err != nil {
|
||||
log.Fatal(err)
|
||||
logging.Error(err)
|
||||
os.Exit(1)
|
||||
return
|
||||
}
|
||||
os.Exit(0)
|
||||
@@ -182,13 +183,13 @@ func (r *registerInputs) assignToNext(stage registerStage, value string, cfg *co
|
||||
for _, l := range cfg.Runner.Labels {
|
||||
_, err := labels.Parse(l)
|
||||
if err != nil {
|
||||
log.WithError(err).Warnf("ignored invalid label %q", l)
|
||||
logging.WithError(err).Warnf("ignored invalid label %q", l)
|
||||
continue
|
||||
}
|
||||
ls = append(ls, l)
|
||||
}
|
||||
if len(ls) == 0 {
|
||||
log.Warn("no valid labels configured in config file, runner may not be able to pick up jobs")
|
||||
logging.Warn("no valid labels configured in config file, runner may not be able to pick up jobs")
|
||||
}
|
||||
r.Labels = ls
|
||||
return StageWaitingForRegistration
|
||||
@@ -201,7 +202,7 @@ func (r *registerInputs) assignToNext(stage registerStage, value string, cfg *co
|
||||
}
|
||||
|
||||
if validateLabels(r.Labels) != nil {
|
||||
log.Infoln("Invalid labels, please input again, leave blank to use the default labels (for example, ubuntu-latest:docker://docker.gitea.com/runner-images:ubuntu-latest)")
|
||||
logging.Info("Invalid labels, please input again, leave blank to use the default labels (for example, ubuntu-latest:docker://docker.gitea.com/runner-images:ubuntu-latest)")
|
||||
r.Labels = nil
|
||||
return StageInputLabels
|
||||
}
|
||||
@@ -273,11 +274,11 @@ func registerInteractive(ctx context.Context, configFile string, regArgs *regist
|
||||
stage = inputs.assignToNext(stage, strings.TrimSpace(cmdString), cfg)
|
||||
|
||||
if stage == StageWaitingForRegistration {
|
||||
log.Infof("Registering runner, name=%s, instance=%s, labels=%v.", inputs.RunnerName, inputs.InstanceAddr, inputs.Labels)
|
||||
logging.Infof("Registering runner, name=%s, instance=%s, labels=%v.", inputs.RunnerName, inputs.InstanceAddr, inputs.Labels)
|
||||
if err := doRegister(ctx, cfg, inputs); err != nil {
|
||||
return fmt.Errorf("failed to register runner: %w", err)
|
||||
}
|
||||
log.Infof("Runner registered successfully.")
|
||||
logging.Infof("Runner registered successfully.")
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -286,7 +287,7 @@ func registerInteractive(ctx context.Context, configFile string, regArgs *regist
|
||||
}
|
||||
|
||||
if stage <= StageUnknown {
|
||||
log.Errorf("Invalid input, please re-run act command.")
|
||||
logging.Errorf("Invalid input, please re-run act command.")
|
||||
return nil
|
||||
}
|
||||
}
|
||||
@@ -295,18 +296,18 @@ func registerInteractive(ctx context.Context, configFile string, regArgs *regist
|
||||
func printStageHelp(stage registerStage) {
|
||||
switch stage {
|
||||
case StageOverwriteLocalConfig:
|
||||
log.Infoln("Runner is already registered, overwrite local config? [y/N]")
|
||||
logging.Info("Runner is already registered, overwrite local config? [y/N]")
|
||||
case StageInputInstance:
|
||||
log.Infoln("Enter the Gitea instance URL (for example, https://gitea.com/):")
|
||||
logging.Info("Enter the Gitea instance URL (for example, https://gitea.com/):")
|
||||
case StageInputToken:
|
||||
log.Infoln("Enter the runner token:")
|
||||
logging.Info("Enter the runner token:")
|
||||
case StageInputRunnerName:
|
||||
hostname, _ := os.Hostname()
|
||||
log.Infof("Enter the runner name (if set empty, use hostname: %s):\n", hostname)
|
||||
logging.Infof("Enter the runner name (if set empty, use hostname: %s):\n", hostname)
|
||||
case StageInputLabels:
|
||||
log.Infoln("Enter the runner labels, leave blank to use the default labels (comma-separated, for example, ubuntu-latest:docker://docker.gitea.com/runner-images:ubuntu-latest):")
|
||||
logging.Info("Enter the runner labels, leave blank to use the default labels (comma-separated, for example, ubuntu-latest:docker://docker.gitea.com/runner-images:ubuntu-latest):")
|
||||
case StageWaitingForRegistration:
|
||||
log.Infoln("Waiting for registration...")
|
||||
logging.Info("Waiting for registration...")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -322,7 +323,7 @@ func registerNoInteractive(ctx context.Context, configFile string, regArgs *regi
|
||||
// specify labels in config file.
|
||||
if len(cfg.Runner.Labels) > 0 {
|
||||
if regArgs.Labels != "" {
|
||||
log.Warn("Labels from command will be ignored, use labels defined in config file.")
|
||||
logging.Warn("Labels from command will be ignored, use labels defined in config file.")
|
||||
}
|
||||
inputs.Labels = cfg.Runner.Labels
|
||||
}
|
||||
@@ -332,16 +333,16 @@ func registerNoInteractive(ctx context.Context, configFile string, regArgs *regi
|
||||
|
||||
if inputs.RunnerName == "" {
|
||||
inputs.RunnerName, _ = os.Hostname()
|
||||
log.Infof("Runner name is empty, use hostname '%s'.", inputs.RunnerName)
|
||||
logging.Infof("Runner name is empty, use hostname '%s'.", inputs.RunnerName)
|
||||
}
|
||||
if err := inputs.validate(); err != nil {
|
||||
log.WithError(err).Errorf("Invalid input, please re-run act command.")
|
||||
logging.WithError(err).Errorf("Invalid input, please re-run act command.")
|
||||
return err
|
||||
}
|
||||
if err := doRegister(ctx, cfg, inputs); err != nil {
|
||||
return fmt.Errorf("failed to register runner: %w", err)
|
||||
}
|
||||
log.Infof("Runner registered successfully.")
|
||||
logging.Infof("Runner registered successfully.")
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -355,7 +356,7 @@ func doRegister(ctx context.Context, cfg *config.Config, inputs *registerInputs)
|
||||
// Best-effort guard: if the lock file can't be created, warn and
|
||||
// register anyway; writing the runner file will surface any real
|
||||
// permission problem with a clearer error.
|
||||
log.Warnf("could not lock runner file %q, continuing without the single-process guard: %v", cfg.Runner.File, err)
|
||||
logging.Warnf("could not lock runner file %q, continuing without the single-process guard: %v", cfg.Runner.File, err)
|
||||
default:
|
||||
defer func() { _ = releaseLock() }()
|
||||
}
|
||||
@@ -375,7 +376,7 @@ func doRegister(ctx context.Context, cfg *config.Config, inputs *registerInputs)
|
||||
})); err != nil {
|
||||
return fmt.Errorf("cannot ping the Gitea instance server: %w", err)
|
||||
}
|
||||
log.Debugln("Successfully pinged the Gitea instance server")
|
||||
logging.Debug("Successfully pinged the Gitea instance server")
|
||||
|
||||
reg := &config.Registration{
|
||||
Name: inputs.RunnerName,
|
||||
@@ -403,7 +404,7 @@ func doRegister(ctx context.Context, cfg *config.Config, inputs *registerInputs)
|
||||
Capabilities: run.RunnerCapabilities(),
|
||||
}))
|
||||
if err != nil {
|
||||
log.WithError(err).Error("poller: cannot register new runner")
|
||||
logging.WithError(err).Error("poller: cannot register new runner")
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -414,7 +415,7 @@ func doRegister(ctx context.Context, cfg *config.Config, inputs *registerInputs)
|
||||
|
||||
if inputs.Ephemeral != resp.Msg.Runner.Ephemeral {
|
||||
// TODO we cannot remove the configuration via runner api, if we return an error here we just fill the database
|
||||
log.Error("poller: cannot register new runner as ephemeral upgrade Gitea to gain security, run-once will be used automatically")
|
||||
logging.Error("poller: cannot register new runner as ephemeral upgrade Gitea to gain security, run-once will be used automatically")
|
||||
}
|
||||
|
||||
if err := config.SaveRegistration(cfg.Runner.File, reg); err != nil {
|
||||
|
||||
@@ -12,13 +12,13 @@ import (
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"gitea.com/gitea/runner/act/common/logging"
|
||||
"gitea.com/gitea/runner/internal/pkg/client"
|
||||
"gitea.com/gitea/runner/internal/pkg/config"
|
||||
"gitea.com/gitea/runner/internal/pkg/metrics"
|
||||
|
||||
"connectrpc.com/connect"
|
||||
runnerv1 "gitea.dev/actionslib/runner/v1"
|
||||
log "github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
// TaskRunner abstracts task execution so the poller can be tested
|
||||
@@ -236,9 +236,9 @@ func (p *Poller) reportAvailability(ready bool, reason string) {
|
||||
defer p.availabilityMu.Unlock()
|
||||
switch {
|
||||
case !ready && p.availabilityReady:
|
||||
log.Warnf("runner temporarily unavailable: %s", reason)
|
||||
logging.Warnf("runner temporarily unavailable: %s", reason)
|
||||
case ready && !p.availabilityReady:
|
||||
log.Info("runner local health recovered, resuming task polling")
|
||||
logging.Info("runner local health recovered, resuming task polling")
|
||||
}
|
||||
p.availabilityReady = ready
|
||||
p.availabilityReason = reason
|
||||
@@ -339,12 +339,12 @@ func (p *Poller) runTaskWithRecover(ctx context.Context, task *runnerv1.Task) {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
err := fmt.Errorf("panic: %v", r)
|
||||
log.WithError(err).Error("panic in runTaskWithRecover")
|
||||
logging.WithError(err).Error("panic in runTaskWithRecover")
|
||||
}
|
||||
}()
|
||||
|
||||
if err := p.runner.Run(ctx, task); err != nil {
|
||||
log.WithError(err).Error("failed to run task")
|
||||
logging.WithError(err).Error("failed to run task")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -366,7 +366,7 @@ func (p *Poller) fetchTask(ctx context.Context, s *workerState) (*runnerv1.Task,
|
||||
if errors.Is(err, context.DeadlineExceeded) {
|
||||
if !s.fetchTimedOut {
|
||||
s.fetchTimedOut = true
|
||||
log.Warnf("fetching a task timed out after %s, raise runner.fetch_timeout if this persists", p.cfg.Runner.FetchTimeout)
|
||||
logging.Warnf("fetching a task timed out after %s, raise runner.fetch_timeout if this persists", p.cfg.Runner.FetchTimeout)
|
||||
}
|
||||
s.consecutiveEmpty++
|
||||
metrics.PollFetchTotal.WithLabelValues(metrics.LabelResultEmpty).Inc()
|
||||
@@ -381,7 +381,7 @@ func (p *Poller) fetchTask(ctx context.Context, s *workerState) (*runnerv1.Task,
|
||||
// polling and let the daemon exit with an error instead of spinning.
|
||||
rejected := client.IsRegistrationRejected(err)
|
||||
if rejected || connect.CodeOf(err) == connect.CodeUnauthenticated {
|
||||
log.WithError(err).Error("server rejected the runner as unregistered, stopping poller")
|
||||
logging.WithError(err).Error("server rejected the runner as unregistered, stopping poller")
|
||||
p.unregistered.Store(true)
|
||||
p.registrationRejected.Store(rejected)
|
||||
p.shutdownPolling()
|
||||
@@ -390,9 +390,9 @@ func (p *Poller) fetchTask(ctx context.Context, s *workerState) (*runnerv1.Task,
|
||||
// Not a long poll, so a deadline can mean the server assigned a task
|
||||
// this runner never received.
|
||||
if errors.Is(err, context.DeadlineExceeded) {
|
||||
log.WithError(err).Errorf("fetching a task timed out after %s", p.cfg.Runner.FetchTimeout)
|
||||
logging.WithError(err).Errorf("fetching a task timed out after %s", p.cfg.Runner.FetchTimeout)
|
||||
} else {
|
||||
log.WithError(err).Error("failed to fetch task")
|
||||
logging.WithError(err).Error("failed to fetch task")
|
||||
}
|
||||
p.lastPollFailed.Store(true)
|
||||
s.consecutiveErrors++
|
||||
|
||||
@@ -11,12 +11,12 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"gitea.com/gitea/runner/act/common/logging/logtest"
|
||||
"gitea.com/gitea/runner/internal/pkg/client/mocks"
|
||||
"gitea.com/gitea/runner/internal/pkg/config"
|
||||
|
||||
connect_go "connectrpc.com/connect"
|
||||
runnerv1 "gitea.dev/actionslib/runner/v1"
|
||||
"github.com/sirupsen/logrus/hooks/test"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/mock"
|
||||
"github.com/stretchr/testify/require"
|
||||
@@ -38,7 +38,7 @@ func TestPoller_WorkerStateCounters(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
p := &Poller{client: client, cfg: cfg}
|
||||
|
||||
ctx := context.Background()
|
||||
ctx := t.Context()
|
||||
s1 := &workerState{}
|
||||
s2 := &workerState{}
|
||||
|
||||
@@ -69,7 +69,7 @@ func TestPoller_FetchErrorIncrementsErrorsOnly(t *testing.T) {
|
||||
p := &Poller{client: client, cfg: cfg}
|
||||
|
||||
s := &workerState{}
|
||||
_, ok := p.fetchTask(context.Background(), s)
|
||||
_, ok := p.fetchTask(t.Context(), s)
|
||||
require.False(t, ok)
|
||||
assert.Equal(t, int64(1), s.consecutiveErrors)
|
||||
assert.Equal(t, int64(0), s.consecutiveEmpty)
|
||||
@@ -87,19 +87,18 @@ func TestPoller_FetchTimeoutIsNoSignal(t *testing.T) {
|
||||
p := &Poller{client: client, cfg: cfg}
|
||||
p.lastPollFailed.Store(true)
|
||||
|
||||
hook := test.NewGlobal()
|
||||
defer hook.Reset()
|
||||
hook := logtest.SetDefault(t)
|
||||
|
||||
s := &workerState{consecutiveErrors: 2}
|
||||
_, ok := p.fetchTask(context.Background(), s)
|
||||
_, ok := p.fetchTask(t.Context(), s)
|
||||
require.False(t, ok)
|
||||
assert.Equal(t, int64(2), s.consecutiveErrors)
|
||||
assert.Equal(t, int64(1), s.consecutiveEmpty)
|
||||
assert.True(t, p.lastPollFailed.Load(), "a timeout must not clear a known failure")
|
||||
|
||||
// An idle runner against a server holding the request open must not flood.
|
||||
_, _ = p.fetchTask(context.Background(), s)
|
||||
assert.Len(t, hook.AllEntries(), 1)
|
||||
_, _ = p.fetchTask(t.Context(), s)
|
||||
assert.Len(t, hook.AllRecords(), 1)
|
||||
}
|
||||
|
||||
// TestPoller_FetchUnauthenticatedStopsPolling verifies that an Unauthenticated
|
||||
@@ -121,7 +120,7 @@ func TestPoller_FetchUnauthenticatedStopsPolling(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
p := New(cfg, client, nil)
|
||||
s := &workerState{}
|
||||
_, ok := p.fetchTask(context.Background(), s)
|
||||
_, ok := p.fetchTask(t.Context(), s)
|
||||
require.False(t, ok)
|
||||
|
||||
assert.True(t, p.Unregistered(), "runner should be marked unregistered")
|
||||
@@ -262,7 +261,7 @@ func TestPollerPausesAndResumesForLocalAvailability(t *testing.T) {
|
||||
return fetches.Load() > 0
|
||||
}, time.Second, 10*time.Millisecond, "polling should resume after local recovery")
|
||||
|
||||
shutdownCtx, cancel := context.WithTimeout(context.Background(), time.Second)
|
||||
shutdownCtx, cancel := context.WithTimeout(t.Context(), time.Second)
|
||||
defer cancel()
|
||||
require.NoError(t, poller.Shutdown(shutdownCtx))
|
||||
wg.Wait()
|
||||
@@ -281,7 +280,7 @@ func (m *mockRunner) Run(ctx context.Context, _ *runnerv1.Task) error {
|
||||
|
||||
func TestPollerRunIdleMaintenance(t *testing.T) {
|
||||
runner := &idleAwareRunner{}
|
||||
p := &Poller{runner: runner, jobsCtx: context.Background()}
|
||||
p := &Poller{runner: runner, jobsCtx: t.Context()}
|
||||
|
||||
p.runIdleMaintenance()
|
||||
|
||||
@@ -316,7 +315,7 @@ func TestPollerPollCallsOnIdle(t *testing.T) {
|
||||
return runner.idleCalls.Load() > 0
|
||||
}, time.Second, 10*time.Millisecond)
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
|
||||
ctx, cancel := context.WithTimeout(t.Context(), time.Second)
|
||||
defer cancel()
|
||||
require.NoError(t, poller.Shutdown(ctx))
|
||||
wg.Wait()
|
||||
@@ -345,7 +344,7 @@ func TestPollerPollOnceCallsOnIdle(t *testing.T) {
|
||||
return runner.idleCalls.Load() > 0
|
||||
}, time.Second, 10*time.Millisecond)
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
|
||||
ctx, cancel := context.WithTimeout(t.Context(), time.Second)
|
||||
defer cancel()
|
||||
require.NoError(t, poller.Shutdown(ctx))
|
||||
wg.Wait()
|
||||
@@ -412,7 +411,7 @@ func TestPoller_ConcurrencyLimitedByCapacity(t *testing.T) {
|
||||
return runner.totalCompleted.Load() >= totalTasks
|
||||
}, 2*time.Second, 10*time.Millisecond, "all tasks should complete")
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
|
||||
ctx, cancel := context.WithTimeout(t.Context(), time.Second)
|
||||
defer cancel()
|
||||
err = poller.Shutdown(ctx)
|
||||
require.NoError(t, err)
|
||||
@@ -468,7 +467,7 @@ func TestPoller_ShutdownForcesJobsOnTimeout(t *testing.T) {
|
||||
return runner.running.Load() == 1
|
||||
}, time.Second, 10*time.Millisecond, "task should start running")
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 50*time.Millisecond)
|
||||
ctx, cancel := context.WithTimeout(t.Context(), 50*time.Millisecond)
|
||||
defer cancel()
|
||||
start := time.Now()
|
||||
err = poller.Shutdown(ctx)
|
||||
|
||||
@@ -4,21 +4,15 @@
|
||||
package run
|
||||
|
||||
import (
|
||||
"io"
|
||||
|
||||
log "github.com/sirupsen/logrus"
|
||||
"gitea.com/gitea/runner/act/common/logging"
|
||||
)
|
||||
|
||||
// NullLogger is used to create a new JobLogger to discard logs. This
|
||||
// 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{}
|
||||
|
||||
// WithJobLogger creates a new logrus.Logger that will discard all logs.
|
||||
func (n NullLogger) WithJobLogger() *log.Logger {
|
||||
logger := log.New()
|
||||
logger.SetOutput(io.Discard)
|
||||
logger.SetLevel(log.TraceLevel)
|
||||
|
||||
return logger
|
||||
// WithJobLogger returns a handler that discards all logs.
|
||||
func (n NullLogger) WithJobLogger() logging.Handler {
|
||||
return logging.DiscardHandler
|
||||
}
|
||||
@@ -14,13 +14,13 @@ import (
|
||||
"time"
|
||||
|
||||
"gitea.com/gitea/runner/act/common"
|
||||
"gitea.com/gitea/runner/act/common/logging"
|
||||
"gitea.com/gitea/runner/internal/pkg/config"
|
||||
"gitea.com/gitea/runner/internal/pkg/metrics"
|
||||
"gitea.com/gitea/runner/internal/pkg/process"
|
||||
"gitea.com/gitea/runner/internal/pkg/report"
|
||||
|
||||
runnerv1 "gitea.dev/actionslib/runner/v1"
|
||||
log "github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
func (r *Runner) runPostTaskScript(ctx context.Context, reporter *report.Reporter, task *runnerv1.Task, workdir string) {
|
||||
@@ -38,7 +38,7 @@ func (r *Runner) runPostTaskScript(ctx context.Context, reporter *report.Reporte
|
||||
defer cancel()
|
||||
|
||||
env := r.postTaskScriptEnv(reporter, task, workdir)
|
||||
log.Infof("running post-task script %q for task %d", script, task.Id)
|
||||
logging.Infof("running post-task script %q for task %d", script, task.Id)
|
||||
|
||||
cmd := exec.CommandContext(scriptCtx, script)
|
||||
cmd.Env = envListFromMap(env)
|
||||
@@ -55,11 +55,11 @@ func (r *Runner) runPostTaskScript(ctx context.Context, reporter *report.Reporte
|
||||
treeKill := process.NewTreeKill(cmd)
|
||||
|
||||
if err := cmd.Start(); err != nil {
|
||||
log.Warnf("post-task script %q for task %d: %v", script, task.Id, err)
|
||||
logging.Warnf("post-task script %q for task %d: %v", script, task.Id, err)
|
||||
return
|
||||
}
|
||||
if k, kerr := treeKill.Capture(cmd.Process); kerr != nil {
|
||||
log.Warnf("post-task script %q for task %d: process tree kill setup failed, falling back to single-process kill: %v", script, task.Id, kerr)
|
||||
logging.Warnf("post-task script %q for task %d: process tree kill setup failed, falling back to single-process kill: %v", script, task.Id, kerr)
|
||||
} else {
|
||||
defer k.Close()
|
||||
}
|
||||
@@ -71,14 +71,14 @@ func (r *Runner) runPostTaskScript(ctx context.Context, reporter *report.Reporte
|
||||
common.FlushWriter(stderr)
|
||||
if err != nil {
|
||||
if errors.Is(err, context.Canceled) || errors.Is(err, context.DeadlineExceeded) {
|
||||
log.Warnf("post-task script %q for task %d: %v", script, task.Id, err)
|
||||
logging.Warnf("post-task script %q for task %d: %v", script, task.Id, err)
|
||||
return
|
||||
}
|
||||
if exitErr, ok := errors.AsType[*exec.ExitError](err); ok {
|
||||
log.Warnf("post-task script %q for task %d exited with code %d", script, task.Id, exitErr.ExitCode())
|
||||
logging.Warnf("post-task script %q for task %d exited with code %d", script, task.Id, exitErr.ExitCode())
|
||||
return
|
||||
}
|
||||
log.Warnf("post-task script %q for task %d: %v", script, task.Id, err)
|
||||
logging.Warnf("post-task script %q for task %d: %v", script, task.Id, err)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -125,7 +125,7 @@ func envListFromMap(env map[string]string) []string {
|
||||
// cmd.Wait() returns.
|
||||
func postTaskScriptLogWriter(stream string) io.Writer {
|
||||
return common.NewLineWriter(func(line string) bool {
|
||||
log.Infof("post-task script %s: %s", stream, strings.TrimRight(line, "\r\n"))
|
||||
logging.Infof("post-task script %s: %s", stream, strings.TrimRight(line, "\r\n"))
|
||||
return true
|
||||
})
|
||||
}
|
||||
@@ -10,12 +10,12 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"gitea.com/gitea/runner/act/common/logging"
|
||||
"gitea.com/gitea/runner/internal/pkg/config"
|
||||
"gitea.com/gitea/runner/internal/pkg/metrics"
|
||||
"gitea.com/gitea/runner/internal/pkg/report"
|
||||
|
||||
runnerv1 "gitea.dev/actionslib/runner/v1"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"google.golang.org/protobuf/types/known/structpb"
|
||||
@@ -25,7 +25,7 @@ func TestRunPostTaskScriptSkippedWhenEmpty(t *testing.T) {
|
||||
r := &Runner{
|
||||
cfg: &config.Config{},
|
||||
}
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
ctx, cancel := context.WithCancel(t.Context())
|
||||
defer cancel()
|
||||
|
||||
taskCtx, err := structpb.NewStruct(map[string]any{})
|
||||
@@ -48,7 +48,7 @@ func TestRunPostTaskScriptNonZeroExitDoesNotPanic(t *testing.T) {
|
||||
cfg.Runner.PostTaskScript = scriptPath
|
||||
|
||||
r := &Runner{cfg: cfg}
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
ctx, cancel := context.WithCancel(t.Context())
|
||||
defer cancel()
|
||||
|
||||
taskCtx, err := structpb.NewStruct(map[string]any{})
|
||||
@@ -66,7 +66,7 @@ func TestPostTaskScriptContextUsesFullTimeout(t *testing.T) {
|
||||
|
||||
// A task context that finished close to its own deadline must not truncate the
|
||||
// script's budget: the script should still get its full configured timeout.
|
||||
near, cancelNear := context.WithTimeout(context.Background(), time.Second)
|
||||
near, cancelNear := context.WithTimeout(t.Context(), time.Second)
|
||||
defer cancelNear()
|
||||
scriptCtx, cancel := postTaskScriptContext(near, timeout)
|
||||
defer cancel()
|
||||
@@ -75,7 +75,7 @@ func TestPostTaskScriptContextUsesFullTimeout(t *testing.T) {
|
||||
assert.Greater(t, time.Until(deadline), time.Minute, "script timeout truncated to task deadline")
|
||||
|
||||
// An already-cancelled task context must not cancel the script either.
|
||||
cancelledCtx, cancelIt := context.WithCancel(context.Background())
|
||||
cancelledCtx, cancelIt := context.WithCancel(t.Context())
|
||||
cancelIt()
|
||||
scriptCtx2, cancel2 := postTaskScriptContext(cancelledCtx, timeout)
|
||||
defer cancel2()
|
||||
@@ -91,7 +91,7 @@ func TestPostTaskScriptEnv(t *testing.T) {
|
||||
envs: map[string]string{"BASE": "1"},
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
ctx, cancel := context.WithCancel(t.Context())
|
||||
defer cancel()
|
||||
|
||||
taskCtx, err := structpb.NewStruct(map[string]any{
|
||||
@@ -128,7 +128,7 @@ func TestRunPostTaskScriptIntegration(t *testing.T) {
|
||||
envs: map[string]string{"CUSTOM": "runner-env"},
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
ctx, cancel := context.WithCancel(t.Context())
|
||||
defer cancel()
|
||||
|
||||
taskCtx, err := structpb.NewStruct(map[string]any{})
|
||||
@@ -146,12 +146,12 @@ func TestRunPostTaskScriptIntegration(t *testing.T) {
|
||||
|
||||
func setReporterJobResult(t *testing.T, reporter *report.Reporter, result runnerv1.Result) {
|
||||
t.Helper()
|
||||
require.NoError(t, reporter.Fire(&log.Entry{
|
||||
require.NoError(t, reporter.Handle(t.Context(), logging.Record{
|
||||
Time: time.Now(),
|
||||
Message: "job finished",
|
||||
Data: log.Fields{
|
||||
"stage": "Post",
|
||||
"jobResult": metrics.ResultToStatusLabel(result),
|
||||
Attrs: []logging.Attr{
|
||||
{Key: "stage", Value: "Post"},
|
||||
{Key: "jobResult", Value: metrics.ResultToStatusLabel(result)},
|
||||
},
|
||||
}))
|
||||
}
|
||||
@@ -10,9 +10,9 @@ import (
|
||||
"slices"
|
||||
"strings"
|
||||
|
||||
"gitea.com/gitea/runner/act/common/logging"
|
||||
"gitea.com/gitea/runner/act/container"
|
||||
|
||||
log "github.com/sirupsen/logrus"
|
||||
"golang.org/x/net/http/httpproxy"
|
||||
)
|
||||
|
||||
@@ -87,10 +87,10 @@ func BypassProxyForDockerHost(dockerHost string) {
|
||||
noProxy := appendNoProxy(cfg.NoProxy, host)
|
||||
for _, name := range []string{"no_proxy", "NO_PROXY"} {
|
||||
if err := os.Setenv(name, noProxy); err != nil {
|
||||
log.Warnf("cannot set %s for the runner process: %v", name, err)
|
||||
logging.Warnf("cannot set %s for the runner process: %v", name, err)
|
||||
}
|
||||
}
|
||||
log.Debugf("docker host %s is reached directly, no_proxy is now %q", host, noProxy)
|
||||
logging.Debugf("docker host %s is reached directly, no_proxy is now %q", host, noProxy)
|
||||
}
|
||||
|
||||
// WarnIfDaemonHasNoProxy points at the one part the runner cannot set: the docker daemon
|
||||
@@ -101,11 +101,11 @@ func WarnIfDaemonHasNoProxy(ctx context.Context) {
|
||||
}
|
||||
info, err := container.GetHostInfo(ctx)
|
||||
if err != nil {
|
||||
log.Debugf("cannot read the docker daemon's proxy configuration: %v", err)
|
||||
logging.Debugf("cannot read the docker daemon's proxy configuration: %v", err)
|
||||
return
|
||||
}
|
||||
if info.HTTPProxy == "" && info.HTTPSProxy == "" {
|
||||
log.Warn("the runner has a proxy but the docker daemon reports none, so image pulls will not use it: https://docs.docker.com/engine/daemon/proxy/")
|
||||
logging.Warn("the runner has a proxy but the docker daemon reports none, so image pulls will not use it: https://docs.docker.com/engine/daemon/proxy/")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+32
-32
@@ -28,6 +28,7 @@ import (
|
||||
|
||||
"gitea.com/gitea/runner/act/artifactcache"
|
||||
"gitea.com/gitea/runner/act/common"
|
||||
"gitea.com/gitea/runner/act/common/logging"
|
||||
"gitea.com/gitea/runner/act/container"
|
||||
"gitea.com/gitea/runner/act/runner"
|
||||
"gitea.com/gitea/runner/internal/pkg/client"
|
||||
@@ -44,7 +45,6 @@ import (
|
||||
"gitea.dev/actionslib/pkg/model"
|
||||
runnerv1 "gitea.dev/actionslib/runner/v1"
|
||||
docker_container "github.com/moby/moby/api/types/container"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"go.yaml.in/yaml/v4"
|
||||
)
|
||||
|
||||
@@ -109,10 +109,10 @@ func NewRunner(cfg *config.Config, reg *config.Registration, cli client.Client)
|
||||
Upstream: cfg.Cache.ExternalServer,
|
||||
S3: cacheS3(cfg),
|
||||
Policy: CachePolicy(cfg),
|
||||
Logger: log.StandardLogger().WithField("module", "cache_request"),
|
||||
Logger: logging.Default().With("module", "cache_request"),
|
||||
})
|
||||
if err != nil {
|
||||
log.Errorf("cannot init cache server, it will be disabled: %v", err)
|
||||
logging.Errorf("cannot init cache server, it will be disabled: %v", err)
|
||||
// go on
|
||||
} else {
|
||||
cacheHandler = handler
|
||||
@@ -183,7 +183,7 @@ func (r *Runner) OnIdle(ctx context.Context) {
|
||||
ctx, cancel := context.WithTimeout(ctx, time.Minute)
|
||||
defer cancel()
|
||||
if err := removeOrphanKubernetesResources(ctx, r.kubernetesOptions(nil), r.now().Add(-r.cfg.Runner.WorkdirCleanupAge)); err != nil {
|
||||
log.Warnf("failed to clean up job pods left behind by earlier jobs: %v", err)
|
||||
logging.Warnf("failed to clean up job pods left behind by earlier jobs: %v", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -194,10 +194,10 @@ func (r *Runner) cleanupOrphanDockerResources(ctx context.Context) {
|
||||
}
|
||||
cutoff := r.now().Add(-r.cfg.Runner.WorkdirCleanupAge)
|
||||
if err := removeOrphanNetworks(ctx, r.uuid, r.isolatedCacheContainer(), cutoff); err != nil {
|
||||
log.Warnf("failed to clean up networks left behind by earlier jobs: %v", err)
|
||||
logging.Warnf("failed to clean up networks left behind by earlier jobs: %v", err)
|
||||
}
|
||||
if err := removeOrphanJobVolumes(ctx, r.uuid, cutoff); err != nil {
|
||||
log.Warnf("failed to clean up volumes left behind by earlier jobs: %v", err)
|
||||
logging.Warnf("failed to clean up volumes left behind by earlier jobs: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -254,7 +254,7 @@ func (r *Runner) cleanupStaleDirs(ctx context.Context, root string, match func(n
|
||||
if errors.Is(err, os.ErrNotExist) {
|
||||
return
|
||||
}
|
||||
log.Warnf("failed to list directory %s for stale cleanup: %v", root, err)
|
||||
logging.Warnf("failed to list directory %s for stale cleanup: %v", root, err)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -274,7 +274,7 @@ func (r *Runner) cleanupStaleDirs(ctx context.Context, root string, match func(n
|
||||
}
|
||||
info, err := entry.Info()
|
||||
if err != nil {
|
||||
log.Warnf("failed to stat %s: %v", filepath.Join(root, entry.Name()), err)
|
||||
logging.Warnf("failed to stat %s: %v", filepath.Join(root, entry.Name()), err)
|
||||
continue
|
||||
}
|
||||
if info.ModTime().After(cutoff) {
|
||||
@@ -282,10 +282,10 @@ func (r *Runner) cleanupStaleDirs(ctx context.Context, root string, match func(n
|
||||
}
|
||||
dir := filepath.Join(root, entry.Name())
|
||||
if err := os.RemoveAll(dir); err != nil {
|
||||
log.Warnf("failed to clean stale directory %s: %v", dir, err)
|
||||
logging.Warnf("failed to clean stale directory %s: %v", dir, err)
|
||||
continue
|
||||
}
|
||||
log.Infof("cleaned stale directory %s", dir)
|
||||
logging.Infof("cleaned stale directory %s", dir)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -330,7 +330,7 @@ func (r *Runner) Run(ctx context.Context, task *runnerv1.Task) error {
|
||||
}
|
||||
_ = reporter.Close(lastWords)
|
||||
if err := cleanupJobVolumes(ctx, volumeCleanup); err != nil {
|
||||
log.Warnf("task %d volume cleanup after reporting: %v", task.Id, err)
|
||||
logging.Warnf("task %d volume cleanup after reporting: %v", task.Id, err)
|
||||
}
|
||||
|
||||
metrics.JobDuration.Observe(time.Since(start).Seconds())
|
||||
@@ -462,9 +462,8 @@ func (r *Runner) run(ctx context.Context, task *runnerv1.Task, reporter *report.
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
log.WithFields(taskLogFields(task, jobID)).
|
||||
WithField("actions_url", r.getDefaultActionsURL(task)).
|
||||
WithField("address", r.client.Address()).
|
||||
logging.With(taskLogFields(task, jobID)...).
|
||||
With("actions_url", r.getDefaultActionsURL(task), "address", r.client.Address()).
|
||||
Info("task started")
|
||||
|
||||
plan, err := model.CombineWorkflowPlanner(workflow).PlanJob(jobID)
|
||||
@@ -472,7 +471,7 @@ func (r *Runner) run(ctx context.Context, task *runnerv1.Task, reporter *report.
|
||||
return err
|
||||
}
|
||||
job := workflow.GetJob(jobID)
|
||||
reporter.ResetSteps(len(job.Steps))
|
||||
reporter.ResetSteps(job.Steps)
|
||||
telemetry.SetJobName(ctx, cmp.Or(job.Name, jobID))
|
||||
|
||||
taskContext := task.Context.Fields
|
||||
@@ -645,9 +644,9 @@ func (r *Runner) run(ctx context.Context, task *runnerv1.Task, reporter *report.
|
||||
reporter.Logf("workflow prepared")
|
||||
|
||||
// add logger recorders
|
||||
ctx = common.WithLoggerHook(ctx, reporter)
|
||||
ctx = common.WithLoggerHandler(ctx, reporter)
|
||||
|
||||
if !log.IsLevelEnabled(log.DebugLevel) {
|
||||
if !logging.Enabled(logging.LevelDebug) {
|
||||
ctx = runner.WithJobLoggerFactory(ctx, NullLogger{})
|
||||
}
|
||||
|
||||
@@ -658,7 +657,7 @@ func (r *Runner) run(ctx context.Context, task *runnerv1.Task, reporter *report.
|
||||
// Remove the entire task-specific directory (e.g. /workspace/<task_id>).
|
||||
taskDir := filepath.FromSlash("/" + workdirParent)
|
||||
if err := os.RemoveAll(taskDir); err != nil {
|
||||
log.Warnf("failed to clean up workspace %s: %v", taskDir, err)
|
||||
logging.Warnf("failed to clean up workspace %s: %v", taskDir, err)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -740,7 +739,7 @@ func (r *Runner) registerExternalCacheJob(token, repo, publicURL string, reporte
|
||||
if err := postInternalCache(base+"/_internal/register", r.cfg.Cache.ExternalSecret, map[string]any{
|
||||
"token": token, "repo": repo, "public_url": publicURL,
|
||||
}); err != nil {
|
||||
log.Warnf("cache external_server register failed (%s): %v", base, err)
|
||||
logging.Warnf("cache external_server register failed (%s): %v", base, err)
|
||||
if reporter != nil {
|
||||
reporter.Logf("::warning::%s", runner.EscapeCommandData(fmt.Sprintf(
|
||||
"cache external_server register failed (%s): %v — cache requests from this job will be unauthenticated and likely return 401", base, err)))
|
||||
@@ -749,7 +748,7 @@ func (r *Runner) registerExternalCacheJob(token, repo, publicURL string, reporte
|
||||
return func() {
|
||||
if err := postInternalCache(base+"/_internal/revoke", r.cfg.Cache.ExternalSecret,
|
||||
map[string]any{"token": token}); err != nil {
|
||||
log.Warnf("cache external_server revoke failed (%s): %v", base, err)
|
||||
logging.Warnf("cache external_server revoke failed (%s): %v", base, err)
|
||||
if reporter != nil {
|
||||
reporter.Logf("::warning::%s", runner.EscapeCommandData(fmt.Sprintf(
|
||||
"cache external_server revoke failed (%s): %v", base, err)))
|
||||
@@ -892,9 +891,9 @@ func warnIgnoredCachePolicy(cfg *config.Config) {
|
||||
cfg.Cache.SizeLimit != defaults.SizeLimit
|
||||
switch {
|
||||
case cfg.Cache.ExternalServer != "" && (limits || cfg.Cache.SweepInterval != defaults.SweepInterval):
|
||||
log.Warn("cache eviction settings are ignored when cache.external_server is set; configure them on that server instead")
|
||||
logging.Warn("cache eviction settings are ignored when cache.external_server is set; configure them on that server instead")
|
||||
case cfg.Cache.S3 != nil && limits:
|
||||
log.Warn("cache eviction settings are ignored when cache.s3 is set; use lifecycle rules on the bucket instead")
|
||||
logging.Warn("cache eviction settings are ignored when cache.s3 is set; use lifecycle rules on the bucket instead")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -912,7 +911,7 @@ func warnIgnoredCacheSecret(cfg *config.Config) {
|
||||
if cfg.Cache.ExternalSecretFile != "" {
|
||||
key = "cache.external_secret_file"
|
||||
}
|
||||
log.Warnf("%s is set but cache.external_server is not; the built-in cache server does not use a shared secret, so the value is ignored", key)
|
||||
logging.Warnf("%s is set but cache.external_server is not; the built-in cache server does not use a shared secret, so the value is ignored", key)
|
||||
}
|
||||
|
||||
// cacheForJob returns the cache address for the job, and the container to attach to its network to reach it there.
|
||||
@@ -942,25 +941,26 @@ func (r *Runner) detectIsolatedCacheContainer() string {
|
||||
defer cancel()
|
||||
cacheContainer, err := container.IsolatedCacheContainer(ctx, addr, r.cfg.Container.Network)
|
||||
if err != nil {
|
||||
log.Warnf("cannot check whether jobs reach the cache server: %v", err)
|
||||
logging.Warnf("cannot check whether jobs reach the cache server: %v", err)
|
||||
}
|
||||
return cacheContainer
|
||||
}
|
||||
|
||||
func taskLogFields(task *runnerv1.Task, job string) log.Fields {
|
||||
// taskLogFields returns key/value pairs for logging.With.
|
||||
func taskLogFields(task *runnerv1.Task, job string) []any {
|
||||
taskContext := task.GetContext().GetFields()
|
||||
fields := log.Fields{
|
||||
"task_id": task.Id,
|
||||
"job": job,
|
||||
"repository": taskContext["repository"].GetStringValue(),
|
||||
"run_id": taskContext["run_id"].GetStringValue(),
|
||||
"run_number": taskContext["run_number"].GetStringValue(),
|
||||
fields := []any{
|
||||
"task_id", task.Id,
|
||||
"job", job,
|
||||
"repository", taskContext["repository"].GetStringValue(),
|
||||
"run_id", taskContext["run_id"].GetStringValue(),
|
||||
"run_number", taskContext["run_number"].GetStringValue(),
|
||||
}
|
||||
if parts := strings.Split(taskContext["gitea_runtime_token"].GetStringValue(), "."); len(parts) == 3 {
|
||||
var claims struct{ JobID int64 } // Gitea's actionsClaims fields are untagged, so the claim key is "JobID"
|
||||
payload, err := base64.RawURLEncoding.DecodeString(parts[1])
|
||||
if err == nil && json.Unmarshal(payload, &claims) == nil && claims.JobID > 0 {
|
||||
fields["job_id"] = claims.JobID
|
||||
fields = append(fields, "job_id", claims.JobID)
|
||||
}
|
||||
}
|
||||
return fields
|
||||
|
||||
@@ -46,7 +46,7 @@ func TestRunnerCleanupStaleTaskDirs(t *testing.T) {
|
||||
now: func() time.Time { return now },
|
||||
}
|
||||
|
||||
r.cleanupStaleDirs(context.Background(), workdirRoot, isTaskIDDir)
|
||||
r.cleanupStaleDirs(t.Context(), workdirRoot, isTaskIDDir)
|
||||
|
||||
assert.NoDirExists(t, oldTask)
|
||||
assert.DirExists(t, freshTask)
|
||||
@@ -85,7 +85,7 @@ func TestRunnerOnIdleCleansStaleHostScratchDirs(t *testing.T) {
|
||||
now: func() time.Time { return now },
|
||||
}
|
||||
|
||||
r.OnIdle(context.Background())
|
||||
r.OnIdle(t.Context())
|
||||
|
||||
assert.NoDirExists(t, staleScratch) // stale scratch reclaimed
|
||||
assert.DirExists(t, freshScratch) // within cleanup age, kept
|
||||
@@ -113,7 +113,7 @@ func TestRunnerCleanupStaleTaskDirsMissingRoot(t *testing.T) {
|
||||
|
||||
// Must be a silent no-op rather than a warning or panic when the root
|
||||
// has not yet been created (e.g. the runner has never executed a task).
|
||||
r.cleanupStaleDirs(context.Background(), filepath.Join(t.TempDir(), "missing"), isTaskIDDir)
|
||||
r.cleanupStaleDirs(t.Context(), filepath.Join(t.TempDir(), "missing"), isTaskIDDir)
|
||||
}
|
||||
|
||||
func TestRunnerCleanupStaleTaskDirsHonorsContext(t *testing.T) {
|
||||
@@ -127,7 +127,7 @@ func TestRunnerCleanupStaleTaskDirsHonorsContext(t *testing.T) {
|
||||
require.NoError(t, os.Chtimes(dir, now.Add(-3*time.Hour), now.Add(-3*time.Hour)))
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
ctx, cancel := context.WithCancel(t.Context())
|
||||
cancel()
|
||||
|
||||
r := &Runner{
|
||||
@@ -265,7 +265,7 @@ func TestRunnerOnIdleIntegratesCleanup(t *testing.T) {
|
||||
now: func() time.Time { return now },
|
||||
}
|
||||
|
||||
r.OnIdle(context.Background())
|
||||
r.OnIdle(t.Context())
|
||||
|
||||
assert.NoDirExists(t, stale)
|
||||
}
|
||||
@@ -293,7 +293,7 @@ func TestRunnerOnIdleSkipsWhenAlreadyCancelled(t *testing.T) {
|
||||
now: func() time.Time { return now },
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
ctx, cancel := context.WithCancel(t.Context())
|
||||
cancel()
|
||||
r.OnIdle(ctx)
|
||||
|
||||
@@ -346,7 +346,7 @@ func TestRunnerOnIdleRemovesOrphanNetworks(t *testing.T) {
|
||||
|
||||
r := &Runner{uuid: "runner-1", cfg: cfg, labels: labels.Labels{kubernetesLabel}, now: func() time.Time { return now }}
|
||||
r.isolatedCacheContainer = func() string { return "a1b2c3d4e5f6" }
|
||||
r.OnIdle(context.Background())
|
||||
r.OnIdle(t.Context())
|
||||
assert.Equal(t, []string{"runner-1"}, swept)
|
||||
assert.Equal(t, swept, sweptVolumes)
|
||||
// a network of a job starting during the pass is younger than this and so out of scope
|
||||
@@ -358,13 +358,13 @@ func TestRunnerOnIdleRemovesOrphanNetworks(t *testing.T) {
|
||||
t.Cleanup(func() { dockerReachable = origDockerReachable })
|
||||
hostOnly := &Runner{uuid: "runner-2", cfg: &config.Config{Runner: cfg.Runner}, now: func() time.Time { return now }}
|
||||
hostOnly.isolatedCacheContainer = func() string { return "" }
|
||||
hostOnly.OnIdle(context.Background())
|
||||
hostOnly.OnIdle(t.Context())
|
||||
assert.Equal(t, []string{"runner-1"}, swept)
|
||||
assert.Equal(t, swept, sweptVolumes)
|
||||
|
||||
dockerReachable = func(context.Context) bool { return true }
|
||||
now = now.Add(time.Minute)
|
||||
hostOnly.OnIdle(context.Background())
|
||||
hostOnly.OnIdle(t.Context())
|
||||
assert.Equal(t, []string{"runner-1", "runner-2"}, sweptVolumes)
|
||||
assert.Equal(t, []string{"a1b2c3d4e5f6", ""}, detached)
|
||||
assert.Equal(t, []string{"runner-1"}, sweptPods)
|
||||
|
||||
@@ -18,6 +18,7 @@ import (
|
||||
"time"
|
||||
|
||||
"gitea.com/gitea/runner/act/common"
|
||||
"gitea.com/gitea/runner/act/common/logging"
|
||||
"gitea.com/gitea/runner/act/runner"
|
||||
clientmocks "gitea.com/gitea/runner/internal/pkg/client/mocks"
|
||||
"gitea.com/gitea/runner/internal/pkg/config"
|
||||
@@ -27,7 +28,6 @@ import (
|
||||
"connectrpc.com/connect"
|
||||
"gitea.dev/actionslib/pkg/model"
|
||||
runnerv1 "gitea.dev/actionslib/runner/v1"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/mock"
|
||||
"github.com/stretchr/testify/require"
|
||||
@@ -47,7 +47,7 @@ func TestRunnerCapabilitiesAndDeclare(t *testing.T) {
|
||||
})).Return(connect.NewResponse(&runnerv1.DeclareResponse{}), nil)
|
||||
|
||||
r := &Runner{client: cli}
|
||||
_, err := r.Declare(context.Background(), []string{"ubuntu"})
|
||||
_, err := r.Declare(t.Context(), []string{"ubuntu"})
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
@@ -79,9 +79,9 @@ func TestRunnerRunningCountAndNullLogger(t *testing.T) {
|
||||
r.runningCount.Add(2)
|
||||
require.Equal(t, int64(2), r.RunningCount())
|
||||
|
||||
logger := NullLogger{}.WithJobLogger()
|
||||
require.NotNil(t, logger)
|
||||
require.NotNil(t, logger.Out)
|
||||
handler := NullLogger{}.WithJobLogger()
|
||||
require.NotNil(t, handler)
|
||||
require.False(t, handler.Enabled(t.Context(), logging.LevelError))
|
||||
}
|
||||
|
||||
func TestRunnerReclaimsVolumesAfterReporting(t *testing.T) {
|
||||
@@ -417,13 +417,12 @@ func TestRestoreCalledWorkflowContext(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestTaskLogFields(t *testing.T) {
|
||||
fields := func(token string) log.Fields {
|
||||
fields := func(token string) []any {
|
||||
taskContext, err := structpb.NewStruct(map[string]any{"repository": "owner/repo", "run_id": "159", "run_number": "14", "gitea_runtime_token": token})
|
||||
require.NoError(t, err)
|
||||
return taskLogFields(&runnerv1.Task{Id: 72, Context: taskContext}, "build")
|
||||
}
|
||||
want := log.Fields{"task_id": int64(72), "job": "build", "repository": "owner/repo", "run_id": "159", "run_number": "14"}
|
||||
want := []any{"task_id", int64(72), "job", "build", "repository", "owner/repo", "run_id", "159", "run_number", "14"}
|
||||
assert.Equal(t, want, fields("malformed"))
|
||||
want["job_id"] = int64(221)
|
||||
assert.Equal(t, want, fields("header."+base64.RawURLEncoding.EncodeToString([]byte(`{"TaskID":72,"JobID":221}`))+".signature"))
|
||||
assert.Equal(t, append(want, "job_id", int64(221)), fields("header."+base64.RawURLEncoding.EncodeToString([]byte(`{"TaskID":72,"JobID":221}`))+".signature"))
|
||||
}
|
||||
@@ -6,7 +6,7 @@
|
||||
# each task's log kept under log.job. Neither controls how workflow step output is streamed
|
||||
# to the Gitea UI; tune that with runner.log_report_* below.
|
||||
log:
|
||||
# logrus severity: trace, debug, info, warn, error, fatal, panic.
|
||||
# Severity: trace, debug, info, warn, error. fatal and panic are accepted and mean error.
|
||||
# trace and debug turn on caller/file:line in log lines. Default if omitted: info.
|
||||
#level: info
|
||||
# Write a copy of each task's log to dir as <start time>-task-<id>.log, so a job's output
|
||||
|
||||
@@ -15,9 +15,10 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"gitea.com/gitea/runner/act/common/logging"
|
||||
|
||||
"github.com/docker/go-units"
|
||||
"github.com/joho/godotenv"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"go.yaml.in/yaml/v4"
|
||||
"golang.org/x/net/http/httpguts"
|
||||
)
|
||||
@@ -408,26 +409,26 @@ func LoadDefault(file string) (*Config, error) {
|
||||
|
||||
// Validate and fix invalid config combinations to prevent confusing behavior.
|
||||
if cfg.Runner.ToolCacheMode == ToolCacheModeShared && cfg.Runner.Capacity > 1 {
|
||||
log.Warnf("runner.tool_cache_mode %q with capacity %d: two jobs writing the same tool version at once corrupt it",
|
||||
logging.Warnf("runner.tool_cache_mode %q with capacity %d: two jobs writing the same tool version at once corrupt it",
|
||||
ToolCacheModeShared, cfg.Runner.Capacity)
|
||||
}
|
||||
if cfg.Runner.FetchTimeout > RequestTimeout {
|
||||
log.Warnf("fetch_timeout (%v) exceeds the RPC timeout (%v), capping it", cfg.Runner.FetchTimeout, RequestTimeout)
|
||||
logging.Warnf("fetch_timeout (%v) exceeds the RPC timeout (%v), capping it", cfg.Runner.FetchTimeout, RequestTimeout)
|
||||
cfg.Runner.FetchTimeout = RequestTimeout
|
||||
}
|
||||
if cfg.Runner.FetchIntervalMax < cfg.Runner.FetchInterval {
|
||||
log.Warnf("fetch_interval_max (%v) is less than fetch_interval (%v), setting fetch_interval_max to fetch_interval",
|
||||
logging.Warnf("fetch_interval_max (%v) is less than fetch_interval (%v), setting fetch_interval_max to fetch_interval",
|
||||
cfg.Runner.FetchIntervalMax, cfg.Runner.FetchInterval)
|
||||
cfg.Runner.FetchIntervalMax = cfg.Runner.FetchInterval
|
||||
}
|
||||
if cfg.Runner.LogReportMaxLatency >= cfg.Runner.LogReportInterval {
|
||||
log.Warnf("log_report_max_latency (%v) >= log_report_interval (%v), the max-latency timer will never fire before the periodic ticker; consider lowering log_report_max_latency",
|
||||
logging.Warnf("log_report_max_latency (%v) >= log_report_interval (%v), the max-latency timer will never fire before the periodic ticker; consider lowering log_report_max_latency",
|
||||
cfg.Runner.LogReportMaxLatency, cfg.Runner.LogReportInterval)
|
||||
}
|
||||
|
||||
// although `container.network_mode` will be deprecated, but we have to be compatible with it for now.
|
||||
if cfg.Container.NetworkMode != "" && cfg.Container.Network == "" {
|
||||
log.Warn("You are trying to use deprecated configuration item of `container.network_mode`, please use `container.network` instead.")
|
||||
logging.Warn("You are trying to use deprecated configuration item of `container.network_mode`, please use `container.network` instead.")
|
||||
if cfg.Container.NetworkMode == "bridge" {
|
||||
// Previously, if the value of `container.network_mode` is `bridge`, we will create a new network for job.
|
||||
// But “bridge” is easily confused with the bridge network created by Docker by default.
|
||||
@@ -493,7 +494,7 @@ func warnUnknownKeys(file string, document *yaml.Node) {
|
||||
var loadErrs *yaml.LoadErrors
|
||||
if err := document.Load(&Config{}, yaml.WithV3Defaults(), yaml.WithKnownFields()); errors.As(err, &loadErrs) {
|
||||
for _, loadErr := range loadErrs.Errors {
|
||||
log.Warnf("config file %q: %s, it will be ignored", file, loadErr)
|
||||
logging.Warnf("config file %q: %s, it will be ignored", file, loadErr)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Loaded 100 of 115 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user