From 5873b8b0549065fbec29333f2929d45dfc781669 Mon Sep 17 00:00:00 2001 From: silverwind Date: Sun, 17 May 2026 03:53:28 +0000 Subject: [PATCH 01/18] Remove dead code from `act/` (#971) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Removes code that whole-program reachability analysis (`deadcode` from `golang.org/x/tools`) confirmed unreachable, plus the `act/workflowpattern` package which no file outside its own directory imports. - `act/common/draw.go` — CLI box-drawing helpers left over from nektos/act's dropped CLI - `act/common/file.go` — `CopyFile`/`CopyDir` package-level helpers (container types have their own `CopyDir` methods, kept) - `act/common/executor.go` — `Warning` type and `Warningf`. The `case Warning:` arm in `(Executor).Then`'s type switch was dead too (no code ever constructed a `Warning`); the switch is replaced with `if err != nil { return err }` - `act/lookpath/env.go` — `LookPath` no-arg wrapper and `defaultEnv` struct. Only `LookPath2(file, env)` was used externally; the `Env` interface is kept - `act/runner/action_cache_offline_mode.go` — `GoGitActionCacheOfflineMode` wrapper, never instantiated - `act/workflowpattern/` — entire package, never imported Net `-943` lines. --- This PR was written with the help of Claude Opus 4.7 --------- Co-authored-by: Nicolas Reviewed-on: https://gitea.com/gitea/runner/pulls/971 Reviewed-by: Nicolas Co-authored-by: silverwind Co-committed-by: silverwind --- act/common/draw.go | 146 ------- act/common/executor.go | 28 +- act/common/file.go | 77 ---- act/lookpath/env.go | 12 - act/runner/action_cache_offline_mode.go | 45 -- act/workflowpattern/trace_writer.go | 22 - act/workflowpattern/workflow_pattern.go | 199 --------- act/workflowpattern/workflow_pattern_test.go | 418 ------------------- 8 files changed, 2 insertions(+), 945 deletions(-) delete mode 100644 act/common/draw.go delete mode 100644 act/common/file.go delete mode 100644 act/runner/action_cache_offline_mode.go delete mode 100644 act/workflowpattern/trace_writer.go delete mode 100644 act/workflowpattern/workflow_pattern.go delete mode 100644 act/workflowpattern/workflow_pattern_test.go diff --git a/act/common/draw.go b/act/common/draw.go deleted file mode 100644 index 2705c3a5..00000000 --- a/act/common/draw.go +++ /dev/null @@ -1,146 +0,0 @@ -// Copyright 2026 The Gitea Authors. All rights reserved. -// Copyright 2020 The nektos/act Authors. All rights reserved. -// SPDX-License-Identifier: MIT - -package common - -import ( - "fmt" - "io" - "os" - "strings" -) - -// Style is a specific style -type Style int - -// Styles -const ( - StyleDoubleLine = iota - StyleSingleLine - StyleDashedLine - StyleNoLine -) - -// NewPen creates a new pen -func NewPen(style Style, color int) *Pen { - bgcolor := 49 - if os.Getenv("CLICOLOR") == "0" { - color = 0 - bgcolor = 0 - } - return &Pen{ - style: style, - color: color, - bgcolor: bgcolor, - } -} - -type styleDef struct { - cornerTL string - cornerTR string - cornerBL string - cornerBR string - lineH string - lineV string -} - -var styleDefs = []styleDef{ - {"\u2554", "\u2557", "\u255a", "\u255d", "\u2550", "\u2551"}, - {"\u256d", "\u256e", "\u2570", "\u256f", "\u2500", "\u2502"}, - {"\u250c", "\u2510", "\u2514", "\u2518", "\u254c", "\u254e"}, - {" ", " ", " ", " ", " ", " "}, -} - -// Pen struct -type Pen struct { - style Style - color int - bgcolor int -} - -// Drawing struct -type Drawing struct { - buf *strings.Builder - width int -} - -func (p *Pen) drawTopBars(buf io.Writer, labels ...string) { - style := styleDefs[p.style] - for _, label := range labels { - bar := strings.Repeat(style.lineH, len(label)+2) - fmt.Fprintf(buf, " ") - fmt.Fprintf(buf, "\x1b[%d;%dm", p.color, p.bgcolor) - fmt.Fprintf(buf, "%s%s%s", style.cornerTL, bar, style.cornerTR) - fmt.Fprintf(buf, "\x1b[%dm", 0) - } - fmt.Fprintf(buf, "\n") -} - -func (p *Pen) drawBottomBars(buf io.Writer, labels ...string) { - style := styleDefs[p.style] - for _, label := range labels { - bar := strings.Repeat(style.lineH, len(label)+2) - fmt.Fprintf(buf, " ") - fmt.Fprintf(buf, "\x1b[%d;%dm", p.color, p.bgcolor) - fmt.Fprintf(buf, "%s%s%s", style.cornerBL, bar, style.cornerBR) - fmt.Fprintf(buf, "\x1b[%dm", 0) - } - fmt.Fprintf(buf, "\n") -} - -func (p *Pen) drawLabels(buf io.Writer, labels ...string) { - style := styleDefs[p.style] - for _, label := range labels { - fmt.Fprintf(buf, " ") - fmt.Fprintf(buf, "\x1b[%d;%dm", p.color, p.bgcolor) - fmt.Fprintf(buf, "%s %s %s", style.lineV, label, style.lineV) - fmt.Fprintf(buf, "\x1b[%dm", 0) - } - fmt.Fprintf(buf, "\n") -} - -// DrawArrow between boxes -func (p *Pen) DrawArrow() *Drawing { - drawing := &Drawing{ - buf: new(strings.Builder), - width: 1, - } - fmt.Fprintf(drawing.buf, "\x1b[%dm", p.color) - fmt.Fprintf(drawing.buf, "\u2b07") - fmt.Fprintf(drawing.buf, "\x1b[%dm", 0) - return drawing -} - -// DrawBoxes to draw boxes -func (p *Pen) DrawBoxes(labels ...string) *Drawing { - width := 0 - for _, l := range labels { - width += len(l) + 2 + 2 + 1 - } - drawing := &Drawing{ - buf: new(strings.Builder), - width: width, - } - p.drawTopBars(drawing.buf, labels...) - p.drawLabels(drawing.buf, labels...) - p.drawBottomBars(drawing.buf, labels...) - - return drawing -} - -// Draw to writer -func (d *Drawing) Draw(writer io.Writer, centerOnWidth int) { - padSize := max((centerOnWidth-d.GetWidth())/2, 0) - for l := range strings.SplitSeq(d.buf.String(), "\n") { - if len(l) > 0 { - padding := strings.Repeat(" ", padSize) - fmt.Fprintf(writer, "%s%s\n", padding, l) - } - } -} - -// GetWidth of drawing -func (d *Drawing) GetWidth() int { - return d.width -} diff --git a/act/common/executor.go b/act/common/executor.go index 90fed4d1..cc44cde7 100644 --- a/act/common/executor.go +++ b/act/common/executor.go @@ -12,24 +12,6 @@ import ( log "github.com/sirupsen/logrus" ) -// Warning that implements `error` but safe to ignore -type Warning struct { - Message string -} - -// Error the contract for error -func (w Warning) Error() string { - return w.Message -} - -// Warningf create a warning -func Warningf(format string, args ...any) Warning { - w := Warning{ - Message: fmt.Sprintf(format, args...), - } - return w -} - // Executor define contract for the steps of a workflow type Executor func(ctx context.Context) error @@ -162,14 +144,8 @@ func NewParallelExecutor(parallel int, executors ...Executor) Executor { // Then runs another executor if this executor succeeds func (e Executor) Then(then Executor) Executor { return func(ctx context.Context) error { - err := e(ctx) - if err != nil { - switch err.(type) { - case Warning: - Logger(ctx).Warning(err.Error()) - default: - return err - } + if err := e(ctx); err != nil { + return err } if ctx.Err() != nil { return ctx.Err() diff --git a/act/common/file.go b/act/common/file.go deleted file mode 100644 index d3a492d0..00000000 --- a/act/common/file.go +++ /dev/null @@ -1,77 +0,0 @@ -// Copyright 2022 The Gitea Authors. All rights reserved. -// Copyright 2020 The nektos/act Authors. All rights reserved. -// SPDX-License-Identifier: MIT - -package common - -import ( - "fmt" - "io" - "os" -) - -// CopyFile copy file -func CopyFile(source, dest string) (err error) { - sourcefile, err := os.Open(source) - if err != nil { - return err - } - - defer sourcefile.Close() - - destfile, err := os.Create(dest) - if err != nil { - return err - } - - defer destfile.Close() - - _, err = io.Copy(destfile, sourcefile) - if err == nil { - sourceinfo, err := os.Stat(source) - if err != nil { - _ = os.Chmod(dest, sourceinfo.Mode()) - } - } - - return err -} - -// CopyDir recursive copy of directory -func CopyDir(source, dest string) (err error) { - // get properties of source dir - sourceinfo, err := os.Stat(source) - if err != nil { - return err - } - - // create dest dir - - err = os.MkdirAll(dest, sourceinfo.Mode()) - if err != nil { - return err - } - - objects, err := os.ReadDir(source) - - for _, obj := range objects { - sourcefilepointer := source + "/" + obj.Name() - - destinationfilepointer := dest + "/" + obj.Name() - - if obj.IsDir() { - // create sub-directories - recursively - err = CopyDir(sourcefilepointer, destinationfilepointer) - if err != nil { - fmt.Println(err) //nolint:forbidigo // pre-existing issue from nektos/act - } - } else { - // perform copy - err = CopyFile(sourcefilepointer, destinationfilepointer) - if err != nil { - fmt.Println(err) //nolint:forbidigo // pre-existing issue from nektos/act - } - } - } - return err -} diff --git a/act/lookpath/env.go b/act/lookpath/env.go index a78ae056..b492d997 100644 --- a/act/lookpath/env.go +++ b/act/lookpath/env.go @@ -4,18 +4,6 @@ package lookpath -import "os" - type Env interface { Getenv(name string) string } - -type defaultEnv struct{} - -func (*defaultEnv) Getenv(name string) string { - return os.Getenv(name) -} - -func LookPath(file string) (string, error) { - return LookPath2(file, &defaultEnv{}) -} diff --git a/act/runner/action_cache_offline_mode.go b/act/runner/action_cache_offline_mode.go deleted file mode 100644 index 710bd0d5..00000000 --- a/act/runner/action_cache_offline_mode.go +++ /dev/null @@ -1,45 +0,0 @@ -// Copyright 2024 The Gitea Authors. All rights reserved. -// Copyright 2024 The nektos/act Authors. All rights reserved. -// SPDX-License-Identifier: MIT - -package runner - -import ( - "context" - "io" - "path" - - git "github.com/go-git/go-git/v5" - "github.com/go-git/go-git/v5/plumbing" -) - -type GoGitActionCacheOfflineMode struct { - Parent GoGitActionCache -} - -func (c GoGitActionCacheOfflineMode) Fetch(ctx context.Context, cacheDir, url, ref, token string) (string, error) { - sha, fetchErr := c.Parent.Fetch(ctx, cacheDir, url, ref, token) - gitPath := path.Join(c.Parent.Path, safeFilename(cacheDir)+".git") - gogitrepo, err := git.PlainOpen(gitPath) - if err != nil { - return "", fetchErr - } - refName := plumbing.ReferenceName("refs/action-cache-offline/" + ref) - r, err := gogitrepo.Reference(refName, true) - if fetchErr == nil { - if err != nil || sha != r.Hash().String() { - if err == nil { - refName = r.Name() - } - ref := plumbing.NewHashReference(refName, plumbing.NewHash(sha)) - _ = gogitrepo.Storer.SetReference(ref) - } - } else if err == nil { - return r.Hash().String(), nil - } - return sha, fetchErr -} - -func (c GoGitActionCacheOfflineMode) GetTarArchive(ctx context.Context, cacheDir, sha, includePrefix string) (io.ReadCloser, error) { - return c.Parent.GetTarArchive(ctx, cacheDir, sha, includePrefix) -} diff --git a/act/workflowpattern/trace_writer.go b/act/workflowpattern/trace_writer.go deleted file mode 100644 index 37a4c850..00000000 --- a/act/workflowpattern/trace_writer.go +++ /dev/null @@ -1,22 +0,0 @@ -// Copyright 2023 The Gitea Authors. All rights reserved. -// Copyright 2023 The nektos/act Authors. All rights reserved. -// SPDX-License-Identifier: MIT - -package workflowpattern - -import "fmt" - -type TraceWriter interface { - Info(string, ...any) -} - -type EmptyTraceWriter struct{} - -func (*EmptyTraceWriter) Info(string, ...any) { -} - -type StdOutTraceWriter struct{} - -func (*StdOutTraceWriter) Info(format string, args ...any) { - fmt.Printf(format+"\n", args...) //nolint:forbidigo // pre-existing issue from nektos/act -} diff --git a/act/workflowpattern/workflow_pattern.go b/act/workflowpattern/workflow_pattern.go deleted file mode 100644 index 9f598618..00000000 --- a/act/workflowpattern/workflow_pattern.go +++ /dev/null @@ -1,199 +0,0 @@ -// Copyright 2023 The Gitea Authors. All rights reserved. -// Copyright 2023 The nektos/act Authors. All rights reserved. -// SPDX-License-Identifier: MIT - -package workflowpattern - -import ( - "fmt" - "regexp" - "strings" -) - -type WorkflowPattern struct { - Pattern string - Negative bool - Regex *regexp.Regexp -} - -func CompilePattern(rawpattern string) (*WorkflowPattern, error) { - negative := false - pattern := rawpattern - if strings.HasPrefix(rawpattern, "!") { - negative = true - pattern = rawpattern[1:] - } - rpattern, err := PatternToRegex(pattern) - if err != nil { - return nil, err - } - regex, err := regexp.Compile(rpattern) - if err != nil { - return nil, err - } - return &WorkflowPattern{ - Pattern: pattern, - Negative: negative, - Regex: regex, - }, nil -} - -func PatternToRegex(pattern string) (string, error) { - var rpattern strings.Builder - rpattern.WriteString("^") - pos := 0 - errors := map[int]string{} - for pos < len(pattern) { - switch pattern[pos] { - case '*': - if pos+1 < len(pattern) && pattern[pos+1] == '*' { - if pos+2 < len(pattern) && pattern[pos+2] == '/' { - rpattern.WriteString("(.+/)?") - pos += 3 - } else { - rpattern.WriteString(".*") - pos += 2 - } - } else { - rpattern.WriteString("[^/]*") - pos++ - } - case '+', '?': - if pos > 0 { - rpattern.WriteByte(pattern[pos]) - } else { - rpattern.WriteString(regexp.QuoteMeta(string([]byte{pattern[pos]}))) - } - pos++ - case '[': - rpattern.WriteByte(pattern[pos]) - pos++ - if pos < len(pattern) && pattern[pos] == ']' { - errors[pos] = "Unexpected empty brackets '[]'" - pos++ - break - } - validChar := func(a, b, test byte) bool { - return test >= a && test <= b - } - startPos := pos - for pos < len(pattern) && pattern[pos] != ']' { - switch pattern[pos] { - case '-': - if pos <= startPos || pos+1 >= len(pattern) { - errors[pos] = "Invalid range" - pos++ - break - } - validRange := func(a, b byte) bool { - return validChar(a, b, pattern[pos-1]) && validChar(a, b, pattern[pos+1]) && pattern[pos-1] <= pattern[pos+1] - } - if !validRange('A', 'z') && !validRange('0', '9') { - errors[pos] = "Ranges can only include a-z, A-Z, A-z, and 0-9" - pos++ - break - } - rpattern.WriteString(pattern[pos : pos+2]) - pos += 2 - default: - if !validChar('A', 'z', pattern[pos]) && !validChar('0', '9', pattern[pos]) { - errors[pos] = "Ranges can only include a-z, A-Z and 0-9" - pos++ - break - } - rpattern.WriteString(regexp.QuoteMeta(string([]byte{pattern[pos]}))) - pos++ - } - } - if pos >= len(pattern) || pattern[pos] != ']' { - errors[pos] = "Missing closing bracket ']' after '['" - pos++ - } - rpattern.WriteString("]") - pos++ - case '\\': - if pos+1 >= len(pattern) { - errors[pos] = "Missing symbol after \\" - pos++ - break - } - rpattern.WriteString(regexp.QuoteMeta(string([]byte{pattern[pos+1]}))) - pos += 2 - default: - rpattern.WriteString(regexp.QuoteMeta(string([]byte{pattern[pos]}))) - pos++ - } - } - if len(errors) > 0 { - var errorMessage strings.Builder - for position, err := range errors { - if errorMessage.Len() > 0 { - errorMessage.WriteString(", ") - } - fmt.Fprintf(&errorMessage, "Position: %d Error: %s", position, err) - } - return "", fmt.Errorf("invalid Pattern '%s': %s", pattern, errorMessage.String()) - } - rpattern.WriteString("$") - return rpattern.String(), nil -} - -func CompilePatterns(patterns ...string) ([]*WorkflowPattern, error) { - ret := []*WorkflowPattern{} - for _, pattern := range patterns { - cp, err := CompilePattern(pattern) - if err != nil { - return nil, err - } - ret = append(ret, cp) - } - return ret, nil -} - -// returns true if the workflow should be skipped paths/branches -func Skip(sequence []*WorkflowPattern, input []string, traceWriter TraceWriter) bool { - if len(sequence) == 0 { - return false - } - for _, file := range input { - matched := false - for _, item := range sequence { - if item.Regex.MatchString(file) { - pattern := item.Pattern - if item.Negative { - matched = false - traceWriter.Info("%s excluded by pattern %s", file, pattern) - } else { - matched = true - traceWriter.Info("%s included by pattern %s", file, pattern) - } - } - } - if matched { - return false - } - } - return true -} - -// returns true if the workflow should be skipped paths-ignore/branches-ignore -func Filter(sequence []*WorkflowPattern, input []string, traceWriter TraceWriter) bool { - if len(sequence) == 0 { - return false - } - for _, file := range input { - matched := false - for _, item := range sequence { - if item.Regex.MatchString(file) == !item.Negative { - pattern := item.Pattern - traceWriter.Info("%s ignored by pattern %s", file, pattern) - matched = true - break - } - } - if !matched { - return false - } - } - return true -} diff --git a/act/workflowpattern/workflow_pattern_test.go b/act/workflowpattern/workflow_pattern_test.go deleted file mode 100644 index 980b0838..00000000 --- a/act/workflowpattern/workflow_pattern_test.go +++ /dev/null @@ -1,418 +0,0 @@ -// Copyright 2023 The Gitea Authors. All rights reserved. -// Copyright 2023 The nektos/act Authors. All rights reserved. -// SPDX-License-Identifier: MIT - -package workflowpattern - -import ( - "strings" - "testing" - - "github.com/stretchr/testify/assert" -) - -func TestMatchPattern(t *testing.T) { - kases := []struct { - inputs []string - patterns []string - skipResult bool - filterResult bool - }{ - { - patterns: []string{"*"}, - inputs: []string{"path/with/slash"}, - skipResult: true, - filterResult: false, - }, - { - patterns: []string{"path/a", "path/b", "path/c"}, - inputs: []string{"meta", "path/b", "otherfile"}, - skipResult: false, - filterResult: false, - }, - { - patterns: []string{"path/a", "path/b", "path/c"}, - inputs: []string{"path/b"}, - skipResult: false, - filterResult: true, - }, - { - patterns: []string{"path/a", "path/b", "path/c"}, - inputs: []string{"path/c", "path/b"}, - skipResult: false, - filterResult: true, - }, - { - patterns: []string{"path/a", "path/b", "path/c"}, - inputs: []string{"path/c", "path/b", "path/a"}, - skipResult: false, - filterResult: true, - }, - { - patterns: []string{"path/a", "path/b", "path/c"}, - inputs: []string{"path/c", "path/b", "path/d", "path/a"}, - skipResult: false, - filterResult: false, - }, - { - patterns: []string{}, - inputs: []string{}, - skipResult: false, - filterResult: false, - }, - { - patterns: []string{"\\!file"}, - inputs: []string{"!file"}, - skipResult: false, - filterResult: true, - }, - { - patterns: []string{"escape\\\\backslash"}, - inputs: []string{"escape\\backslash"}, - skipResult: false, - filterResult: true, - }, - { - patterns: []string{".yml"}, - inputs: []string{"fyml"}, - skipResult: true, - filterResult: false, - }, - // https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#patterns-to-match-branches-and-tags - { - patterns: []string{"feature/*"}, - inputs: []string{"feature/my-branch"}, - skipResult: false, - filterResult: true, - }, - { - patterns: []string{"feature/*"}, - inputs: []string{"feature/your-branch"}, - skipResult: false, - filterResult: true, - }, - { - patterns: []string{"feature/**"}, - inputs: []string{"feature/beta-a/my-branch"}, - skipResult: false, - filterResult: true, - }, - { - patterns: []string{"feature/**"}, - inputs: []string{"feature/beta-a/my-branch"}, - skipResult: false, - filterResult: true, - }, - { - patterns: []string{"feature/**"}, - inputs: []string{"feature/mona/the/octocat"}, - skipResult: false, - filterResult: true, - }, - { - patterns: []string{"main", "releases/mona-the-octocat"}, - inputs: []string{"main"}, - skipResult: false, - filterResult: true, - }, - { - patterns: []string{"main", "releases/mona-the-octocat"}, - inputs: []string{"releases/mona-the-octocat"}, - skipResult: false, - filterResult: true, - }, - { - patterns: []string{"*"}, - inputs: []string{"main"}, - skipResult: false, - filterResult: true, - }, - { - patterns: []string{"*"}, - inputs: []string{"releases"}, - skipResult: false, - filterResult: true, - }, - { - patterns: []string{"**"}, - inputs: []string{"all/the/branches"}, - skipResult: false, - filterResult: true, - }, - { - patterns: []string{"**"}, - inputs: []string{"every/tag"}, - skipResult: false, - filterResult: true, - }, - { - patterns: []string{"*feature"}, - inputs: []string{"mona-feature"}, - skipResult: false, - filterResult: true, - }, - { - patterns: []string{"*feature"}, - inputs: []string{"feature"}, - skipResult: false, - filterResult: true, - }, - { - patterns: []string{"*feature"}, - inputs: []string{"ver-10-feature"}, - skipResult: false, - filterResult: true, - }, - { - patterns: []string{"v2*"}, - inputs: []string{"v2"}, - skipResult: false, - filterResult: true, - }, - { - patterns: []string{"v2*"}, - inputs: []string{"v2.0"}, - skipResult: false, - filterResult: true, - }, - { - patterns: []string{"v2*"}, - inputs: []string{"v2.9"}, - skipResult: false, - filterResult: true, - }, - { - patterns: []string{"v[12].[0-9]+.[0-9]+"}, - inputs: []string{"v1.10.1"}, - skipResult: false, - filterResult: true, - }, - { - patterns: []string{"v[12].[0-9]+.[0-9]+"}, - inputs: []string{"v2.0.0"}, - skipResult: false, - filterResult: true, - }, - // https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#patterns-to-match-file-paths - { - patterns: []string{"*"}, - inputs: []string{"README.md"}, - skipResult: false, - filterResult: true, - }, - { - patterns: []string{"*"}, - inputs: []string{"server.rb"}, - skipResult: false, - filterResult: true, - }, - { - patterns: []string{"*.jsx?"}, - inputs: []string{"page.js"}, - skipResult: false, - filterResult: true, - }, - { - patterns: []string{"*.jsx?"}, - inputs: []string{"page.jsx"}, - skipResult: false, - filterResult: true, - }, - { - patterns: []string{"**"}, - inputs: []string{"all/the/files.md"}, - skipResult: false, - filterResult: true, - }, - { - patterns: []string{"*.js"}, - inputs: []string{"app.js"}, - skipResult: false, - filterResult: true, - }, - { - patterns: []string{"*.js"}, - inputs: []string{"index.js"}, - skipResult: false, - filterResult: true, - }, - { - patterns: []string{"**.js"}, - inputs: []string{"index.js"}, - skipResult: false, - filterResult: true, - }, - { - patterns: []string{"**.js"}, - inputs: []string{"js/index.js"}, - skipResult: false, - filterResult: true, - }, - { - patterns: []string{"**.js"}, - inputs: []string{"src/js/app.js"}, - skipResult: false, - filterResult: true, - }, - { - patterns: []string{"docs/*"}, - inputs: []string{"docs/README.md"}, - skipResult: false, - filterResult: true, - }, - { - patterns: []string{"docs/*"}, - inputs: []string{"docs/file.txt"}, - skipResult: false, - filterResult: true, - }, - { - patterns: []string{"docs/**"}, - inputs: []string{"docs/README.md"}, - skipResult: false, - filterResult: true, - }, - { - patterns: []string{"docs/**"}, - inputs: []string{"docs/mona/octocat.txt"}, - skipResult: false, - filterResult: true, - }, - { - patterns: []string{"docs/**/*.md"}, - inputs: []string{"docs/README.md"}, - skipResult: false, - filterResult: true, - }, - { - patterns: []string{"docs/**/*.md"}, - inputs: []string{"docs/mona/hello-world.md"}, - skipResult: false, - filterResult: true, - }, - { - patterns: []string{"docs/**/*.md"}, - inputs: []string{"docs/a/markdown/file.md"}, - skipResult: false, - filterResult: true, - }, - { - patterns: []string{"**/docs/**"}, - inputs: []string{"docs/hello.md"}, - skipResult: false, - filterResult: true, - }, - { - patterns: []string{"**/docs/**"}, - inputs: []string{"dir/docs/my-file.txt"}, - skipResult: false, - filterResult: true, - }, - { - patterns: []string{"**/docs/**"}, - inputs: []string{"space/docs/plan/space.doc"}, - skipResult: false, - filterResult: true, - }, - { - patterns: []string{"**/README.md"}, - inputs: []string{"README.md"}, - skipResult: false, - filterResult: true, - }, - { - patterns: []string{"**/README.md"}, - inputs: []string{"js/README.md"}, - skipResult: false, - filterResult: true, - }, - { - patterns: []string{"**/*src/**"}, - inputs: []string{"a/src/app.js"}, - skipResult: false, - filterResult: true, - }, - { - patterns: []string{"**/*src/**"}, - inputs: []string{"my-src/code/js/app.js"}, - skipResult: false, - filterResult: true, - }, - { - patterns: []string{"**/*-post.md"}, - inputs: []string{"my-post.md"}, - skipResult: false, - filterResult: true, - }, - { - patterns: []string{"**/*-post.md"}, - inputs: []string{"path/their-post.md"}, - skipResult: false, - filterResult: true, - }, - { - patterns: []string{"**/migrate-*.sql"}, - inputs: []string{"migrate-10909.sql"}, - skipResult: false, - filterResult: true, - }, - { - patterns: []string{"**/migrate-*.sql"}, - inputs: []string{"db/migrate-v1.0.sql"}, - skipResult: false, - filterResult: true, - }, - { - patterns: []string{"**/migrate-*.sql"}, - inputs: []string{"db/sept/migrate-v1.sql"}, - skipResult: false, - filterResult: true, - }, - { - patterns: []string{"*.md", "!README.md"}, - inputs: []string{"hello.md"}, - skipResult: false, - filterResult: true, - }, - { - patterns: []string{"*.md", "!README.md"}, - inputs: []string{"README.md"}, - skipResult: true, - filterResult: true, - }, - { - patterns: []string{"*.md", "!README.md"}, - inputs: []string{"docs/hello.md"}, - skipResult: true, - filterResult: true, - }, - { - patterns: []string{"*.md", "!README.md", "README*"}, - inputs: []string{"hello.md"}, - skipResult: false, - filterResult: true, - }, - { - patterns: []string{"*.md", "!README.md", "README*"}, - inputs: []string{"README.md"}, - skipResult: false, - filterResult: true, - }, - { - patterns: []string{"*.md", "!README.md", "README*"}, - inputs: []string{"README.doc"}, - skipResult: false, - filterResult: true, - }, - } - - for _, kase := range kases { - t.Run(strings.Join(kase.patterns, ","), func(t *testing.T) { - patterns, err := CompilePatterns(kase.patterns...) - assert.NoError(t, err) //nolint:testifylint // pre-existing issue from nektos/act - - assert.EqualValues(t, kase.skipResult, Skip(patterns, kase.inputs, &StdOutTraceWriter{}), "skipResult") //nolint:testifylint // pre-existing issue from nektos/act - assert.EqualValues(t, kase.filterResult, Filter(patterns, kase.inputs, &StdOutTraceWriter{}), "filterResult") //nolint:testifylint // pre-existing issue from nektos/act - }) - } -} From 8a99506fed7a18b788e2f3c70d1ff3e1a1a8d7c5 Mon Sep 17 00:00:00 2001 From: Nicolas Date: Sun, 17 May 2026 12:53:04 +0000 Subject: [PATCH 02/18] Fix host cleanup, volume allowlist, cache upload, and action host edge cases (#970) ## Summary - prevent host-mode execution from deleting caller-owned workdirs - harden `valid_volumes` checks against `..` and symlink escapes - return immediately after artifact cache upload write failures - default implicit remote action clone hosts to `GitHubInstance`/`github.com` Authored with assistance from OpenAI Codex GPT-5. --------- Co-authored-by: silverwind Reviewed-on: https://gitea.com/gitea/runner/pulls/970 Reviewed-by: silverwind <2021+silverwind@noreply.gitea.com> --- act/artifactcache/handler.go | 1 + act/artifactcache/handler_test.go | 49 ++++++++ act/container/docker_run.go | 148 ++++++++++++++++++++++--- act/container/docker_run_test.go | 39 +++++++ act/container/host_environment.go | 16 +-- act/container/host_environment_test.go | 19 ++-- act/runner/run_context.go | 12 +- act/runner/runner.go | 12 ++ act/runner/step_action_remote.go | 7 +- act/runner/step_action_remote_test.go | 52 +++++++++ internal/app/run/runner.go | 1 + 11 files changed, 311 insertions(+), 45 deletions(-) diff --git a/act/artifactcache/handler.go b/act/artifactcache/handler.go index c5bb0376..6efff636 100644 --- a/act/artifactcache/handler.go +++ b/act/artifactcache/handler.go @@ -431,6 +431,7 @@ func (h *Handler) upload(w http.ResponseWriter, r *http.Request, params httprout } if err := h.storage.Write(cache.ID, start, r.Body); err != nil { h.responseJSON(w, r, 500, err) + return } h.useCache(id) h.responseJSON(w, r, 200) diff --git a/act/artifactcache/handler_test.go b/act/artifactcache/handler_test.go index 7cca691c..71e84e64 100644 --- a/act/artifactcache/handler_test.go +++ b/act/artifactcache/handler_test.go @@ -11,6 +11,7 @@ import ( "fmt" "io" "net/http" + "os" "path/filepath" "strings" "testing" @@ -338,6 +339,54 @@ func TestHandler(t *testing.T) { } }) + t.Run("upload write failure returns only error", func(t *testing.T) { + key := strings.ToLower(t.Name()) + version := "c19da02a2bd7e77277f1ac29ab45c09b7d46a4ee758284e26bb3045ad11d9d20" + var id uint64 + { + body, err := json.Marshal(&Request{ + Key: key, + Version: version, + Size: 100, + }) + require.NoError(t, err) + resp, err := testClient.Post(base+"/caches", "application/json", bytes.NewReader(body)) + require.NoError(t, err) + defer resp.Body.Close() + require.Equal(t, 200, resp.StatusCode) + + got := struct { + CacheID uint64 `json:"cacheId"` + }{} + require.NoError(t, json.NewDecoder(resp.Body).Decode(&got)) + id = got.CacheID + } + + storageFile := filepath.Join(dir, "not-a-directory") + require.NoError(t, os.WriteFile(storageFile, []byte("blocked"), 0o600)) + originalStorage := handler.storage + handler.storage = &Storage{rootDir: storageFile} + defer func() { + handler.storage = originalStorage + }() + + req, err := http.NewRequest(http.MethodPatch, + fmt.Sprintf("%s/caches/%d", base, id), bytes.NewReader(make([]byte, 100))) + require.NoError(t, err) + req.Header.Set("Content-Type", "application/octet-stream") + req.Header.Set("Content-Range", "bytes 0-99/*") + resp, err := testClient.Do(req) + require.NoError(t, err) + defer resp.Body.Close() + require.Equal(t, 500, resp.StatusCode) + + body, err := io.ReadAll(resp.Body) + require.NoError(t, err) + var got map[string]string + require.NoError(t, json.Unmarshal(body, &got)) + assert.NotEmpty(t, got["error"]) + }) + t.Run("commit early", func(t *testing.T) { key := strings.ToLower(t.Name()) version := "c19da02a2bd7e77277f1ac29ab45c09b7d46a4ee758284e26bb3045ad11d9d20" diff --git a/act/container/docker_run.go b/act/container/docker_run.go index 15251721..b141bd62 100644 --- a/act/container/docker_run.go +++ b/act/container/docker_run.go @@ -17,6 +17,7 @@ import ( "path/filepath" "regexp" "runtime" + "slices" "strconv" "strings" @@ -968,22 +969,7 @@ func (cr *containerReference) sanitizeConfig(ctx context.Context, config *contai logger := common.Logger(ctx) if len(cr.input.ValidVolumes) > 0 { - globs := make([]glob.Glob, 0, len(cr.input.ValidVolumes)) - for _, v := range cr.input.ValidVolumes { - if g, err := glob.Compile(v); err != nil { - logger.Errorf("create glob from %s error: %v", v, err) - } else { - globs = append(globs, g) - } - } - isValid := func(v string) bool { - for _, g := range globs { - if g.Match(v) { - return true - } - } - return false - } + matcher := newValidVolumeMatcher(ctx, cr.input.ValidVolumes) // sanitize binds sanitizedBinds := make([]string, 0, len(hostConfig.Binds)) for _, bind := range hostConfig.Binds { @@ -997,7 +983,7 @@ func (cr *containerReference) sanitizeConfig(ctx context.Context, config *contai sanitizedBinds = append(sanitizedBinds, bind) continue } - if isValid(parsed.Source) { + if matcher.isValid(parsed.Source, mount.Type(parsed.Type)) { sanitizedBinds = append(sanitizedBinds, bind) } else { logger.Warnf("[%s] is not a valid volume, will be ignored", parsed.Source) @@ -1007,7 +993,7 @@ func (cr *containerReference) sanitizeConfig(ctx context.Context, config *contai // sanitize mounts sanitizedMounts := make([]mount.Mount, 0, len(hostConfig.Mounts)) for _, mt := range hostConfig.Mounts { - if isValid(mt.Source) { + if matcher.isValid(mt.Source, mt.Type) { sanitizedMounts = append(sanitizedMounts, mt) } else { logger.Warnf("[%s] is not a valid volume, will be ignored", mt.Source) @@ -1021,3 +1007,129 @@ func (cr *containerReference) sanitizeConfig(ctx context.Context, config *contai return config, hostConfig } + +type validVolumeMatcher struct { + allowAll bool + named []glob.Glob + host []glob.Glob +} + +func newValidVolumeMatcher(ctx context.Context, validVolumes []string) validVolumeMatcher { + logger := common.Logger(ctx) + ret := validVolumeMatcher{ + named: make([]glob.Glob, 0, len(validVolumes)), + host: make([]glob.Glob, 0, len(validVolumes)), + } + + for _, v := range validVolumes { + if v == "**" { + ret.allowAll = true + continue + } + if !isHostVolumePattern(v) { + if g, err := glob.Compile(v); err != nil { + logger.Errorf("create glob from %s error: %v", v, err) + } else { + ret.named = append(ret.named, g) + } + continue + } + normalized, err := normalizeHostVolumePath(v) + if err != nil { + logger.Errorf("normalize volume pattern %s error: %v", v, err) + continue + } + if g, err := glob.Compile(normalized); err != nil { + logger.Errorf("create glob from %s error: %v", normalized, err) + } else { + ret.host = append(ret.host, g) + } + } + + return ret +} + +func (m validVolumeMatcher) isValid(source string, sourceType mount.Type) bool { + if m.allowAll { + return true + } + if isHostVolumeSource(source, sourceType) { + normalized, err := normalizeHostVolumePath(source) + if err != nil { + return false + } + for _, g := range m.host { + if g.Match(normalized) { + return true + } + } + return false + } + for _, g := range m.named { + if g.Match(source) { + return true + } + } + return false +} + +func isHostVolumePattern(pattern string) bool { + return filepath.IsAbs(pattern) || + strings.HasPrefix(pattern, "."+string(filepath.Separator)) || + strings.HasPrefix(pattern, ".."+string(filepath.Separator)) || + strings.Contains(pattern, "/") || + strings.Contains(pattern, `\`) +} + +func isHostVolumeSource(source string, sourceType mount.Type) bool { + if sourceType == mount.TypeBind { + return true + } + if sourceType == mount.TypeVolume { + return false + } + return isHostVolumePattern(source) +} + +func normalizeHostVolumePath(path string) (string, error) { + abs, err := filepath.Abs(path) + if err != nil { + return "", err + } + return evalSymlinksExistingPrefix(abs) +} + +func evalSymlinksExistingPrefix(path string) (string, error) { + resolved, err := filepath.EvalSymlinks(path) + if err == nil { + return filepath.Clean(resolved), nil + } + if !errors.Is(err, os.ErrNotExist) { + return "", err + } + + current := path + var missing []string + for { + _, err := os.Lstat(current) + if err == nil { + resolved, err := filepath.EvalSymlinks(current) + if err != nil { + return "", err + } + for _, name := range slices.Backward(missing) { + resolved = filepath.Join(resolved, name) + } + return filepath.Clean(resolved), nil + } + if !errors.Is(err, os.ErrNotExist) { + return "", err + } + parent := filepath.Dir(current) + if parent == current { + return filepath.Clean(path), nil + } + missing = append(missing, filepath.Base(current)) + current = parent + } +} diff --git a/act/container/docker_run_test.go b/act/container/docker_run_test.go index d8a23fc5..903ad67b 100644 --- a/act/container/docker_run_test.go +++ b/act/container/docker_run_test.go @@ -11,6 +11,8 @@ import ( "errors" "io" "net" + "os" + "path/filepath" "strings" "testing" "time" @@ -375,3 +377,40 @@ func TestCheckVolumes(t *testing.T) { }) } } + +func TestCheckVolumesRejectsEscapingHostPaths(t *testing.T) { + logger, _ := test.NewNullLogger() + ctx := common.WithLogger(context.Background(), logger) + + base := t.TempDir() + allowed := filepath.Join(base, "allowed") + denied := filepath.Join(base, "denied") + require.NoError(t, os.MkdirAll(allowed, 0o700)) + require.NoError(t, os.MkdirAll(denied, 0o700)) + + cr := &containerReference{ + input: &NewContainerInput{ + ValidVolumes: []string{filepath.Join(allowed, "**")}, + }, + } + + escapingPath := allowed + string(filepath.Separator) + ".." + string(filepath.Separator) + "denied" + _, hostConf := cr.sanitizeConfig(ctx, &container.Config{}, &container.HostConfig{ + Binds: []string{escapingPath + ":/mnt"}, + }) + assert.Empty(t, hostConf.Binds) + + linkPath := filepath.Join(allowed, "link") + if err := os.Symlink(denied, linkPath); err != nil { + t.Skipf("cannot create symlink: %v", err) + } + _, hostConf = cr.sanitizeConfig(ctx, &container.Config{}, &container.HostConfig{ + Binds: []string{linkPath + ":/mnt"}, + }) + assert.Empty(t, hostConf.Binds) + + _, hostConf = cr.sanitizeConfig(ctx, &container.Config{}, &container.HostConfig{ + Binds: []string{filepath.Join(linkPath, "missing") + ":/mnt"}, + }) + assert.Empty(t, hostConf.Binds) +} diff --git a/act/container/host_environment.go b/act/container/host_environment.go index 497039bf..ee456a24 100644 --- a/act/container/host_environment.go +++ b/act/container/host_environment.go @@ -37,13 +37,13 @@ type HostEnvironment struct { TmpDir string ToolCache string Workdir string - // BindWorkdir is true when the app runner mounts the workspace on the host and - // deletes the task directory after the job; host teardown must not remove Workdir. - BindWorkdir bool - ActPath string - CleanUp func() - StdOut io.Writer - AllocatePTY bool // allocate a pseudo-TTY for each step's process + // CleanWorkdir means teardown owns Workdir and may delete it. Leave false + // when Workdir points at a caller-owned checkout (e.g. `act` local mode). + CleanWorkdir bool + ActPath string + CleanUp func() + StdOut io.Writer + AllocatePTY bool // allocate a pseudo-TTY for each step's process mu sync.Mutex runningPIDs map[int]struct{} @@ -483,7 +483,7 @@ func (e *HostEnvironment) Remove() common.Executor { logger.Warnf("failed to remove host misc state %s: %v", e.Path, err) errs = append(errs, err) } - if !e.BindWorkdir && e.Workdir != "" { + if e.CleanWorkdir { if err := removePathWithRetry(ctx, e.Workdir); err != nil { logger.Warnf("failed to remove host workspace %s: %v", e.Workdir, err) errs = append(errs, err) diff --git a/act/container/host_environment_test.go b/act/container/host_environment_test.go index a9911d19..945685c9 100644 --- a/act/container/host_environment_test.go +++ b/act/container/host_environment_test.go @@ -141,7 +141,7 @@ func TestHostEnvironmentAllocatePTY(t *testing.T) { } } -func TestHostEnvironmentRemoveCleansWorkdir(t *testing.T) { +func TestHostEnvironmentRemovePreservesWorkdirByDefault(t *testing.T) { logger := logrus.New() ctx := common.WithLogger(context.Background(), logrus.NewEntry(logger)) base := t.TempDir() @@ -152,9 +152,8 @@ func TestHostEnvironmentRemoveCleansWorkdir(t *testing.T) { require.NoError(t, os.MkdirAll(workdir, 0o700)) e := &HostEnvironment{ - Path: path, - Workdir: workdir, - BindWorkdir: false, + Path: path, + Workdir: workdir, CleanUp: func() { _ = os.RemoveAll(miscRoot) }, @@ -162,10 +161,10 @@ func TestHostEnvironmentRemoveCleansWorkdir(t *testing.T) { } require.NoError(t, e.Remove()(ctx)) _, err := os.Stat(workdir) - assert.ErrorIs(t, err, os.ErrNotExist) + require.NoError(t, err) } -func TestHostEnvironmentRemoveSkipsWorkdirWhenBindWorkdir(t *testing.T) { +func TestHostEnvironmentRemoveCleansWorkdirWhenOwned(t *testing.T) { logger := logrus.New() ctx := common.WithLogger(context.Background(), logrus.NewEntry(logger)) base := t.TempDir() @@ -176,9 +175,9 @@ func TestHostEnvironmentRemoveSkipsWorkdirWhenBindWorkdir(t *testing.T) { require.NoError(t, os.MkdirAll(workdir, 0o700)) e := &HostEnvironment{ - Path: path, - Workdir: workdir, - BindWorkdir: true, + Path: path, + Workdir: workdir, + CleanWorkdir: true, CleanUp: func() { _ = os.RemoveAll(miscRoot) }, @@ -186,5 +185,5 @@ func TestHostEnvironmentRemoveSkipsWorkdirWhenBindWorkdir(t *testing.T) { } require.NoError(t, e.Remove()(ctx)) _, err := os.Stat(workdir) - require.NoError(t, err) + assert.ErrorIs(t, err, os.ErrNotExist) } diff --git a/act/runner/run_context.go b/act/runner/run_context.go index 23a0d67e..70919361 100644 --- a/act/runner/run_context.go +++ b/act/runner/run_context.go @@ -220,12 +220,12 @@ func (rc *RunContext) startHostEnvironment() common.Executor { } toolCache := filepath.Join(cacheDir, "tool_cache") rc.JobContainer = &container.HostEnvironment{ - Path: path, - TmpDir: runnerTmp, - ToolCache: toolCache, - Workdir: rc.Config.Workdir, - BindWorkdir: rc.Config.BindWorkdir, - ActPath: actPath, + Path: path, + TmpDir: runnerTmp, + ToolCache: toolCache, + Workdir: rc.Config.Workdir, + CleanWorkdir: rc.Config.CleanWorkdir, + ActPath: actPath, CleanUp: func() { os.RemoveAll(miscpath) }, diff --git a/act/runner/runner.go b/act/runner/runner.go index 0da97e42..cb389ecd 100644 --- a/act/runner/runner.go +++ b/act/runner/runner.go @@ -73,6 +73,7 @@ type Config struct { EventJSON string // the content of JSON file to use for event.json in containers, overrides EventPath ContainerNamePrefix string // the prefix of container name ContainerMaxLifetime time.Duration // the max lifetime of job containers + CleanWorkdir bool // remove host executor workdir on teardown DefaultActionInstance string // the default actions web site PlatformPicker func(labels []string) string // platform picker, it will take precedence over Platforms if isn't nil JobLoggerLevel *log.Level // the level of job logger @@ -91,6 +92,17 @@ func (c Config) GetToken() string { return token } +// 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" +} + type caller struct { runContext *RunContext diff --git a/act/runner/step_action_remote.go b/act/runner/step_action_remote.go index e842ca97..fdddebad 100644 --- a/act/runner/step_action_remote.go +++ b/act/runner/step_action_remote.go @@ -113,9 +113,10 @@ func (sar *stepActionRemote) prepareActionExecutor() common.Executor { } actionDir := fmt.Sprintf("%s/%s", sar.RunContext.ActionCacheDir(), sar.Step.UsesHash()) - token := getGitCloneToken(sar.getRunContext().Config, sar.remoteAction.CloneURL(sar.RunContext.Config.DefaultActionInstance)) + defaultActionURL := sar.RunContext.Config.DefaultActionURL() + token := getGitCloneToken(sar.getRunContext().Config, sar.remoteAction.CloneURL(defaultActionURL)) gitClone := stepActionRemoteNewCloneExecutor(git.NewGitCloneExecutorInput{ - URL: sar.remoteAction.CloneURL(sar.RunContext.Config.DefaultActionInstance), + URL: sar.remoteAction.CloneURL(defaultActionURL), Ref: sar.remoteAction.Ref, Dir: actionDir, Token: token, @@ -274,7 +275,7 @@ func (sar *stepActionRemote) cloneSkipTLS() bool { if sar.remoteAction.URL == "" { // Empty URL means the default action instance should be used // Return true if the URL of the Gitea instance is the same as the URL of the default action instance - return sar.RunContext.Config.DefaultActionInstance == sar.RunContext.Config.GitHubInstance + return sar.RunContext.Config.DefaultActionURL() == sar.RunContext.Config.GitHubInstance } // Return true if the URL of the remote action is the same as the URL of the Gitea instance return sar.remoteAction.URL == sar.RunContext.Config.GitHubInstance diff --git a/act/runner/step_action_remote_test.go b/act/runner/step_action_remote_test.go index 0f0c9db5..0531d0ea 100644 --- a/act/runner/step_action_remote_test.go +++ b/act/runner/step_action_remote_test.go @@ -20,6 +20,7 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/mock" + "github.com/stretchr/testify/require" "go.yaml.in/yaml/v4" ) @@ -434,6 +435,57 @@ func TestStepActionRemotePreThroughActionToken(t *testing.T) { } } +func TestStepActionRemoteUsesGitHubInstanceWhenDefaultActionInstanceEmpty(t *testing.T) { + ctx := context.Background() + + var actualURL string + sarm := &stepActionRemoteMocks{} + + origStepAtionRemoteNewCloneExecutor := stepActionRemoteNewCloneExecutor + stepActionRemoteNewCloneExecutor = func(input git.NewGitCloneExecutorInput) common.Executor { + return func(ctx context.Context) error { + actualURL = input.URL + return nil + } + } + defer func() { + stepActionRemoteNewCloneExecutor = origStepAtionRemoteNewCloneExecutor + }() + + sar := &stepActionRemote{ + Step: &model.Step{ + Uses: "actions/setup-go@v4", + }, + RunContext: &RunContext{ + Config: &Config{ + GitHubInstance: "gitea.example", + DefaultActionInstance: "", + ActionCacheDir: t.TempDir(), + }, + Run: &model.Run{ + JobID: "1", + Workflow: &model.Workflow{ + Jobs: map[string]*model.Job{ + "1": {}, + }, + }, + }, + }, + readAction: sarm.readAction, + } + + suffixMatcher := func(suffix string) any { + return mock.MatchedBy(func(actionDir string) bool { + return strings.HasSuffix(actionDir, suffix) + }) + } + sarm.On("readAction", sar.Step, suffixMatcher(sar.Step.UsesHash()), "", mock.Anything, mock.Anything).Return(&model.Action{}, nil) + + require.NoError(t, sar.prepareActionExecutor()(ctx)) + assert.Equal(t, "https://gitea.example/actions/setup-go", actualURL) + sarm.AssertExpectations(t) +} + func TestStepActionRemotePost(t *testing.T) { table := []struct { name string diff --git a/internal/app/run/runner.go b/internal/app/run/runner.go index 6f83e560..6d72415f 100644 --- a/internal/app/run/runner.go +++ b/internal/app/run/runner.go @@ -363,6 +363,7 @@ func (r *Runner) run(ctx context.Context, task *runnerv1.Task, reporter *report. EventJSON: string(eventJSON), ContainerNamePrefix: fmt.Sprintf("GITEA-ACTIONS-TASK-%d", task.Id), ContainerMaxLifetime: maxLifetime, + CleanWorkdir: true, ContainerNetworkMode: container.NetworkMode(r.cfg.Container.Network), ContainerOptions: r.cfg.Container.Options, ContainerDaemonSocket: r.cfg.Container.DockerHost, From cf7e29c10d2444f8074457a6399d86bdca688906 Mon Sep 17 00:00:00 2001 From: Jacob Alberty Date: Sun, 17 May 2026 13:00:17 +0000 Subject: [PATCH 03/18] fix(parse_env_file): support env-file lines larger than 64 KiB (#974) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit My builds kept flaking out with errors like `invalid format delimiter 'ghadelimiter_...' not found before end of file` or just strange failures in the complete job. After some digging I found an issue in `parseEnvFile` and have tested this fix against the test case presented. - `parseEnvFile` reads `$GITHUB_ENV` / `$GITHUB_OUTPUT` with a `bufio.Scanner` using the default 64 KiB token size, and never checks `s.Err()`. - Any action that writes a multi-line value with a single line >64 KiB silently aborts the scan with `bufio.ErrTooLong`, which surfaces as the misleading `"invalid format delimiter 'ghadelimiter_…' not found before end of file"`. - Real-world trigger: `docker/build-push-action`'s `metadata` output embeds the full `GITHUB_EVENT_PATH` payload via buildx provenance; a long PR description (e.g. a Renovate dependency table) puts the body field on one JSON-escaped line well past 64 KiB. - Raise the scanner buffer to 1 MiB so realistic outputs parse. ### Reproduction Test this in an action. This removes the `docker/build-push-action` aspect and reproduces it directly. ```yaml jobs: repro: runs-on: ubuntu-latest steps: - id: big run: | { echo 'value<> "$GITHUB_OUTPUT" ``` --------- Co-authored-by: Nicolas Co-authored-by: silverwind Reviewed-on: https://gitea.com/gitea/runner/pulls/974 Reviewed-by: Lunny Xiao Reviewed-by: silverwind <2021+silverwind@noreply.gitea.com> Co-authored-by: Jacob Alberty Co-committed-by: Jacob Alberty --- act/container/parse_env_file.go | 8 +++ act/container/parse_env_file_test.go | 75 ++++++++++++++++++++++++++++ 2 files changed, 83 insertions(+) create mode 100644 act/container/parse_env_file_test.go diff --git a/act/container/parse_env_file.go b/act/container/parse_env_file.go index ec27807f..bfa261ca 100644 --- a/act/container/parse_env_file.go +++ b/act/container/parse_env_file.go @@ -29,6 +29,8 @@ func parseEnvFile(e Container, srcPath string, env *map[string]string) common.Ex return err } s := bufio.NewScanner(reader) + // Default 64 KiB max token size is too small for realistic env-file lines; allow up to 16 MiB. + s.Buffer(make([]byte, 0, 64*1024), 16*1024*1024) for s.Scan() { line := s.Text() singleLineEnv := strings.Index(line, "=") @@ -50,6 +52,9 @@ func parseEnvFile(e Container, srcPath string, env *map[string]string) common.Ex } multiLineEnvContent += content } + if err := s.Err(); err != nil { + return fmt.Errorf("reading env file: %w", err) + } if !delimiterFound { return fmt.Errorf("invalid format delimiter '%v' not found before end of file", multiLineEnvDelimiter) } @@ -58,6 +63,9 @@ func parseEnvFile(e Container, srcPath string, env *map[string]string) common.Ex return fmt.Errorf("invalid format '%v', expected a line with '=' or '<<'", line) } } + if err := s.Err(); err != nil { + return fmt.Errorf("reading env file: %w", err) + } env = &localEnv return nil } diff --git a/act/container/parse_env_file_test.go b/act/container/parse_env_file_test.go new file mode 100644 index 00000000..6a8525a6 --- /dev/null +++ b/act/container/parse_env_file_test.go @@ -0,0 +1,75 @@ +// Copyright 2026 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package container + +import ( + "bufio" + "context" + "os" + "path/filepath" + "strings" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func newTestHostEnv(t *testing.T) (*HostEnvironment, string) { + t.Helper() + e := &HostEnvironment{Path: t.TempDir()} + return e, filepath.Join(e.Path, "envfile") +} + +func TestParseEnvFileSingleLine(t *testing.T) { + e, envPath := newTestHostEnv(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())) + assert.Equal(t, "bar", env["FOO"]) + assert.Equal(t, "qux", env["BAZ"]) +} + +func TestParseEnvFileMultiLine(t *testing.T) { + e, envPath := newTestHostEnv(t) + content := "FOO< Date: Sun, 17 May 2026 18:15:19 +0000 Subject: [PATCH 04/18] Add OCI `source` and `version` labels to images (#975) Adds `org.opencontainers.image.source` and `org.opencontainers.image.version` labels to all three image variants (`basic`, `dind`, `dind-rootless`). - `source` lets tools like renovate retrieve release notes from the source repo. - `version` exposes the build version on the image itself. Both `release-tag` and `release-nightly` workflows pass `VERSION` as a build arg so the label reflects the actual git tag (or `git describe` output for nightly). --- This PR was written with the help of Claude Opus 4.7 --------- Reviewed-on: https://gitea.com/gitea/runner/pulls/975 Reviewed-by: Nicolas Co-authored-by: silverwind Co-committed-by: silverwind --- .gitea/workflows/release-nightly.yml | 7 +++++++ .gitea/workflows/release-tag.yml | 2 ++ Dockerfile | 16 ++++++++++++++++ 3 files changed, 25 insertions(+) diff --git a/.gitea/workflows/release-nightly.yml b/.gitea/workflows/release-nightly.yml index 9b8879b2..75e28589 100644 --- a/.gitea/workflows/release-nightly.yml +++ b/.gitea/workflows/release-nightly.yml @@ -71,6 +71,11 @@ jobs: - name: Echo the tag run: echo "${{ env.DOCKER_ORG }}/runner:nightly${{ matrix.variant.tag_suffix }}" + - name: Get Meta + id: meta + run: | + echo REPO_VERSION=$(git describe --tags --always | sed 's/-/+/' | sed 's/^v//') >> $GITHUB_OUTPUT + - name: Build and push uses: docker/build-push-action@v7 with: @@ -83,3 +88,5 @@ jobs: push: true tags: | ${{ env.DOCKER_ORG }}/runner:nightly${{ matrix.variant.tag_suffix }} + build-args: | + VERSION=${{ steps.meta.outputs.REPO_VERSION }} diff --git a/.gitea/workflows/release-tag.yml b/.gitea/workflows/release-tag.yml index aef461d8..0d054ee9 100644 --- a/.gitea/workflows/release-tag.yml +++ b/.gitea/workflows/release-tag.yml @@ -96,3 +96,5 @@ jobs: linux/arm64 push: true tags: ${{ steps.docker_meta.outputs.tags }} + build-args: | + VERSION=${{ steps.docker_meta.outputs.version }} diff --git a/Dockerfile b/Dockerfile index 707e3be8..63bfc7ba 100644 --- a/Dockerfile +++ b/Dockerfile @@ -19,6 +19,11 @@ RUN make clean && make build # FROM docker:29-dind AS dind +ARG VERSION=dev + +LABEL org.opencontainers.image.source="https://gitea.com/gitea/runner" +LABEL org.opencontainers.image.version="${VERSION}" + RUN apk add --no-cache s6 bash git tzdata COPY --from=builder /opt/src/runner/gitea-runner /usr/local/bin/gitea-runner @@ -34,6 +39,11 @@ ENTRYPOINT ["s6-svscan","/etc/s6"] # FROM docker:29-dind-rootless AS dind-rootless +ARG VERSION=dev + +LABEL org.opencontainers.image.source="https://gitea.com/gitea/runner" +LABEL org.opencontainers.image.version="${VERSION}" + USER root RUN apk add --no-cache s6 bash git tzdata @@ -54,6 +64,12 @@ ENTRYPOINT ["s6-svscan","/etc/s6"] # # FROM alpine AS basic + +ARG VERSION=dev + +LABEL org.opencontainers.image.source="https://gitea.com/gitea/runner" +LABEL org.opencontainers.image.version="${VERSION}" + RUN apk add --no-cache tini bash git tzdata COPY --from=builder /opt/src/runner/gitea-runner /usr/local/bin/gitea-runner From 60239288767564a19dbc3d380f5bf2519eeb0a69 Mon Sep 17 00:00:00 2001 From: Nicolas Date: Mon, 18 May 2026 02:22:04 +0000 Subject: [PATCH 05/18] Fix token use with schemaless Gitea instance (#977) Fixes #973 ## Summary - Normalize schemaless `--gitea-instance` values before comparing clone URL hosts - Add regression tests for `GITEA_TOKEN` use with private action/reusable workflow clones on the same instance --------- Co-authored-by: silverwind <2021+silverwind@noreply.gitea.com> Co-authored-by: silverwind Reviewed-on: https://gitea.com/gitea/runner/pulls/977 Reviewed-by: Lunny Xiao Reviewed-by: silverwind <2021+silverwind@noreply.gitea.com> Co-authored-by: Nicolas Co-committed-by: Nicolas --- act/runner/reusable_workflow.go | 5 +++ act/runner/reusable_workflow_test.go | 59 ++++++++++++++++++++++++++++ 2 files changed, 64 insertions(+) diff --git a/act/runner/reusable_workflow.go b/act/runner/reusable_workflow.go index 590ebc82..effb74db 100644 --- a/act/runner/reusable_workflow.go +++ b/act/runner/reusable_workflow.go @@ -308,6 +308,11 @@ func getGitCloneToken(conf *Config, cloneURL string) string { // 1. cloneURL is from the same Gitea instance that the runner is registered to // 2. the cloneURL does not have basic auth embedded func shouldCloneURLUseToken(instanceURL, cloneURL string) bool { + if !strings.HasPrefix(instanceURL, "http://") && + !strings.HasPrefix(instanceURL, "https://") { + instanceURL = "https://" + instanceURL + } + u1, err1 := url.Parse(instanceURL) u2, err2 := url.Parse(cloneURL) if err1 != nil || err2 != nil { diff --git a/act/runner/reusable_workflow_test.go b/act/runner/reusable_workflow_test.go index 8e618698..4e5d074c 100644 --- a/act/runner/reusable_workflow_test.go +++ b/act/runner/reusable_workflow_test.go @@ -123,6 +123,65 @@ func TestNewReusableWorkflowExecutorHoldsCloneLock(t *testing.T) { } } +func TestGetGitCloneTokenWithSchemalessGiteaInstance(t *testing.T) { + conf := &Config{ + GitHubInstance: "gitea.example.net", + Secrets: map[string]string{ + "GITEA_TOKEN": "token-value", + }, + } + + token := getGitCloneToken(conf, "https://gitea.example.net/actions/tools") + + require.Equal(t, "token-value", token) +} + +func TestShouldCloneURLUseToken(t *testing.T) { + tests := []struct { + name string + instanceURL string + cloneURL string + want bool + }{ + { + name: "same host with schemaless instance", + instanceURL: "gitea.example.net", + cloneURL: "https://gitea.example.net/actions/tools", + want: true, + }, + { + name: "same host with schemaless instance and port", + instanceURL: "gitea.example.net:3000", + cloneURL: "https://gitea.example.net:3000/actions/tools", + want: true, + }, + { + name: "different host", + instanceURL: "gitea.example.net", + cloneURL: "https://github.com/actions/tools", + want: false, + }, + { + name: "embedded basic auth", + instanceURL: "gitea.example.net", + cloneURL: "https://user:pass@gitea.example.net/actions/tools", + want: false, + }, + { + name: "invalid clone URL", + instanceURL: "gitea.example.net", + cloneURL: "://gitea.example.net/actions/tools", + want: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + require.Equal(t, tt.want, shouldCloneURLUseToken(tt.instanceURL, tt.cloneURL)) + }) + } +} + func gitMust(t *testing.T, dir string, args ...string) { t.Helper() cmd := exec.Command("git", args...) From 9e738c203c1662167d4ff9376e6fb31e83d13b5e Mon Sep 17 00:00:00 2001 From: Renovate Bot Date: Tue, 19 May 2026 16:18:34 +0000 Subject: [PATCH 06/18] fix(deps): update module github.com/go-git/go-git/v5 to v5.19.1 (#980) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This PR contains the following updates: | Package | Change | [Age](https://docs.renovatebot.com/merge-confidence/) | [Confidence](https://docs.renovatebot.com/merge-confidence/) | |---|---|---|---| | [github.com/go-git/go-git/v5](https://github.com/go-git/go-git) | `v5.19.0` → `v5.19.1` | ![age](https://developer.mend.io/api/mc/badges/age/go/github.com%2fgo-git%2fgo-git%2fv5/v5.19.1?slim=true) | ![confidence](https://developer.mend.io/api/mc/badges/confidence/go/github.com%2fgo-git%2fgo-git%2fv5/v5.19.0/v5.19.1?slim=true) | --- ### Release Notes
go-git/go-git (github.com/go-git/go-git/v5) ### [`v5.19.1`](https://github.com/go-git/go-git/releases/tag/v5.19.1) [Compare Source](https://github.com/go-git/go-git/compare/v5.19.0...v5.19.1) #### What's Changed - v5: plumbing: transport/ssh, Shell-quote path by [@​hiddeco](https://github.com/hiddeco) in [#​2068](https://github.com/go-git/go-git/pull/2068) - v5: git: submodule, Fix relative URL resolution by [@​hiddeco](https://github.com/hiddeco) in [#​2070](https://github.com/go-git/go-git/pull/2070) - v5: git: submodule, canonical remote for relative URLs by [@​hiddeco](https://github.com/hiddeco) in [#​2074](https://github.com/go-git/go-git/pull/2074) - v5: git: submodule, error on remote without URLs by [@​hiddeco](https://github.com/hiddeco) in [#​2078](https://github.com/go-git/go-git/pull/2078) - v5: plumbing: format/idxfile, Validate offset64 indices by [@​hiddeco](https://github.com/hiddeco) in [#​2084](https://github.com/go-git/go-git/pull/2084) - v5: \*: Reject malformed variable-length integers by [@​hiddeco](https://github.com/hiddeco) in [#​2092](https://github.com/go-git/go-git/pull/2092) - v5: plumbing: format/packfile, Tighten delta validation by [@​hiddeco](https://github.com/hiddeco) in [#​2091](https://github.com/go-git/go-git/pull/2091) - v5: Add `worktreeFilesystem` wrapper for worktree and hardening by [@​hiddeco](https://github.com/hiddeco) in [#​2100](https://github.com/go-git/go-git/pull/2100) - v5: config: validate submodule names by [@​hiddeco](https://github.com/hiddeco) in [#​2082](https://github.com/go-git/go-git/pull/2082) - build: Update module github.com/go-git/go-git/v5 to v5.19.0 \[SECURITY] (releases/v5.x) by [@​go-git-renovate](https://github.com/go-git-renovate)\[bot] in [#​2111](https://github.com/go-git/go-git/pull/2111) - v5: git: Allow MkdirAll on worktree-root paths by [@​hiddeco](https://github.com/hiddeco) in [#​2117](https://github.com/go-git/go-git/pull/2117) - v5: git: Stop validating symlink target paths by [@​pjbgf](https://github.com/pjbgf) in [#​2116](https://github.com/go-git/go-git/pull/2116) - v5: plumbing: format decoder input bounds and contracts by [@​hiddeco](https://github.com/hiddeco) in [#​2125](https://github.com/go-git/go-git/pull/2125) - plumbing: format/packfile, cap delta chain depth in parser by [@​pjbgf](https://github.com/pjbgf) in [#​2137](https://github.com/go-git/go-git/pull/2137) **Full Changelog**:
--- ### Configuration 📅 **Schedule**: (UTC) - Branch creation - At any time (no schedule defined) - Automerge - At any time (no schedule defined) 🚦 **Automerge**: Disabled by config. Please merge this manually once you are satisfied. ♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the rebase/retry checkbox. 🔕 **Ignore**: Close this PR and you won't be reminded about this update again. --- - [ ] If you want to rebase/retry this PR, check this box --- This PR has been generated by [Mend Renovate](https://github.com/renovatebot/renovate). Reviewed-on: https://gitea.com/gitea/runner/pulls/980 Reviewed-by: Lunny Xiao Co-authored-by: Renovate Bot Co-committed-by: Renovate Bot --- go.mod | 2 +- go.sum | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/go.mod b/go.mod index 478b65b3..f5f6eff5 100644 --- a/go.mod +++ b/go.mod @@ -14,7 +14,7 @@ require ( github.com/docker/cli v29.5.0+incompatible github.com/docker/go-connections v0.7.0 github.com/go-git/go-billy/v5 v5.9.0 - github.com/go-git/go-git/v5 v5.19.0 + github.com/go-git/go-git/v5 v5.19.1 github.com/gobwas/glob v0.2.3 github.com/google/go-cmp v0.7.0 github.com/joho/godotenv v1.5.1 diff --git a/go.sum b/go.sum index 4e5e50f5..6bf415f2 100644 --- a/go.sum +++ b/go.sum @@ -75,6 +75,8 @@ github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399 h1:eMj 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.0 h1:+WkVUQZSy/F1Gb13udrMKjIM2PrzsNfDKFSfo5tkMtc= github.com/go-git/go-git/v5 v5.19.0/go.mod h1:Pb1v0c7/g8aGQJwx9Us09W85yGoyvSwuhEGMH7zjDKQ= +github.com/go-git/go-git/v5 v5.19.1 h1:nX27AnaU43/K5bKktKwgBmR9lawoYVe1Ckg0rgzzN00= +github.com/go-git/go-git/v5 v5.19.1/go.mod h1:Pb1v0c7/g8aGQJwx9Us09W85yGoyvSwuhEGMH7zjDKQ= github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= From 10475db58a83ba30a8975c0bb96fabf93e526521 Mon Sep 17 00:00:00 2001 From: Renovate Bot Date: Tue, 19 May 2026 16:27:04 +0000 Subject: [PATCH 07/18] fix(deps): update module github.com/docker/cli to v29.5.1+incompatible (#979) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This PR contains the following updates: | Package | Change | [Age](https://docs.renovatebot.com/merge-confidence/) | [Confidence](https://docs.renovatebot.com/merge-confidence/) | |---|---|---|---| | [github.com/docker/cli](https://github.com/docker/cli) | `v29.5.0+incompatible` → `v29.5.1+incompatible` | ![age](https://developer.mend.io/api/mc/badges/age/go/github.com%2fdocker%2fcli/v29.5.1+incompatible?slim=true) | ![confidence](https://developer.mend.io/api/mc/badges/confidence/go/github.com%2fdocker%2fcli/v29.5.0+incompatible/v29.5.1+incompatible?slim=true) | --- ### Release Notes
docker/cli (github.com/docker/cli) ### [`v29.5.1+incompatible`](https://github.com/docker/cli/compare/v29.5.0...v29.5.1) [Compare Source](https://github.com/docker/cli/compare/v29.5.0...v29.5.1)
--- ### Configuration 📅 **Schedule**: (UTC) - Branch creation - At any time (no schedule defined) - Automerge - At any time (no schedule defined) 🚦 **Automerge**: Disabled by config. Please merge this manually once you are satisfied. ♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the rebase/retry checkbox. 🔕 **Ignore**: Close this PR and you won't be reminded about this update again. --- - [ ] If you want to rebase/retry this PR, check this box --- This PR has been generated by [Mend Renovate](https://github.com/renovatebot/renovate). Reviewed-on: https://gitea.com/gitea/runner/pulls/979 Reviewed-by: silverwind <2021+silverwind@noreply.gitea.com> Co-authored-by: Renovate Bot Co-committed-by: Renovate Bot --- go.mod | 2 +- go.sum | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/go.mod b/go.mod index f5f6eff5..ba9300fc 100644 --- a/go.mod +++ b/go.mod @@ -11,7 +11,7 @@ require ( github.com/containerd/errdefs v1.0.0 github.com/creack/pty v1.1.24 github.com/distribution/reference v0.6.0 - github.com/docker/cli v29.5.0+incompatible + github.com/docker/cli v29.5.1+incompatible github.com/docker/go-connections v0.7.0 github.com/go-git/go-billy/v5 v5.9.0 github.com/go-git/go-git/v5 v5.19.1 diff --git a/go.sum b/go.sum index 6bf415f2..4715ef37 100644 --- a/go.sum +++ b/go.sum @@ -51,6 +51,8 @@ github.com/docker/cli v29.4.3+incompatible h1:u+UliYm2J/rYrIh2FqHQg32neRG8GjbvNu github.com/docker/cli v29.4.3+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8= github.com/docker/cli v29.5.0+incompatible h1:FPUvKJoKpeP4Njz8NrQdeUN8o247P7ndTiILtaP5/l4= github.com/docker/cli v29.5.0+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8= +github.com/docker/cli v29.5.1+incompatible h1:NiufLAJoRcPauFoBNYthfuM4REFwM8H2h9xnLABNHGs= +github.com/docker/cli v29.5.1+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8= github.com/docker/docker-credential-helpers v0.9.6 h1:cT2PbRPSlnMmNTfT2TDMXRyQ1KMWHG7xoTLBcn1ZNv0= github.com/docker/docker-credential-helpers v0.9.6/go.mod h1:v1S+hepowrQXITkEfw6o4+BMbGot02wiKpzWhGUZK6c= github.com/docker/go-connections v0.7.0 h1:6SsRfJddP22WMrCkj19x9WKjEDTB+ahsdiGYf0mN39c= From fab9714f9a8a76c3435e38ee59df9e284fc03146 Mon Sep 17 00:00:00 2001 From: silverwind Date: Tue, 19 May 2026 16:49:15 +0000 Subject: [PATCH 08/18] Remove stale Gitea 1.20 compatibility shims (#978) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The runner already enforces Gitea v1.21+ (see the `connect.CodeUnimplemented` check in `daemon.go`), so several shims kept for v1.20 compatibility have been dead since 2023: - `compatibleWithOldEnvs` — the `GITEA_DEBUG`, `GITEA_TRACE`, `GITEA_RUNNER_CAPACITY`, `GITEA_RUNNER_FILE`, `GITEA_RUNNER_ENVIRON`, `GITEA_RUNNER_ENV_FILE` env vars (superseded by the config file) - `VersionHeader` (`x-runner-version`) and the `version` param of `client.New` - `AgentLabels` field in `RegisterRequest` (replaced by `Labels`) Also replaces a verbose `strings.TrimRightFunc` closure with `strings.TrimRight(s, "\r\n")` in the log row parser. --- This PR was written with the help of Claude Opus 4.7 --------- Co-authored-by: Nicolas Reviewed-on: https://gitea.com/gitea/runner/pulls/978 Reviewed-by: Nicolas Co-authored-by: silverwind Co-committed-by: silverwind --- internal/app/cmd/daemon.go | 1 - internal/app/cmd/register.go | 12 +++--- internal/pkg/client/header.go | 2 - internal/pkg/client/http.go | 6 +-- internal/pkg/config/config.go | 1 - internal/pkg/config/deprecated.go | 62 ------------------------------- internal/pkg/report/reporter.go | 2 +- 7 files changed, 7 insertions(+), 79 deletions(-) delete mode 100644 internal/pkg/config/deprecated.go diff --git a/internal/app/cmd/daemon.go b/internal/app/cmd/daemon.go index aaac6401..247d77f0 100644 --- a/internal/app/cmd/daemon.go +++ b/internal/app/cmd/daemon.go @@ -132,7 +132,6 @@ func runDaemon(ctx context.Context, daemArgs *daemonArgs, configFile *string) fu cfg.Runner.Insecure, reg.UUID, reg.Token, - ver.Version(), ) runner := run.NewRunner(cfg, reg, cli) diff --git a/internal/app/cmd/register.go b/internal/app/cmd/register.go index ea4c0ee9..c0a1950e 100644 --- a/internal/app/cmd/register.go +++ b/internal/app/cmd/register.go @@ -325,7 +325,6 @@ func doRegister(ctx context.Context, cfg *config.Config, inputs *registerInputs) cfg.Runner.Insecure, "", "", - ver.Version(), ) for { @@ -366,12 +365,11 @@ func doRegister(ctx context.Context, cfg *config.Config, inputs *registerInputs) } // register new runner. resp, err := cli.Register(ctx, connect.NewRequest(&runnerv1.RegisterRequest{ - Name: reg.Name, - Token: reg.Token, - Version: ver.Version(), - AgentLabels: ls, // Could be removed after Gitea 1.20 - Labels: ls, - Ephemeral: reg.Ephemeral, + Name: reg.Name, + Token: reg.Token, + Version: ver.Version(), + Labels: ls, + Ephemeral: reg.Ephemeral, })) if err != nil { log.WithError(err).Error("poller: cannot register new runner") diff --git a/internal/pkg/client/header.go b/internal/pkg/client/header.go index 24844fa2..ab29a558 100644 --- a/internal/pkg/client/header.go +++ b/internal/pkg/client/header.go @@ -6,6 +6,4 @@ package client const ( UUIDHeader = "x-runner-uuid" TokenHeader = "x-runner-token" - // Deprecated: could be removed after Gitea 1.20 released - VersionHeader = "x-runner-version" ) diff --git a/internal/pkg/client/http.go b/internal/pkg/client/http.go index d976d12c..0b9a0955 100644 --- a/internal/pkg/client/http.go +++ b/internal/pkg/client/http.go @@ -31,7 +31,7 @@ func getHTTPClient(endpoint string, insecure bool) *http.Client { } // New returns a new runner client. -func New(endpoint string, insecure bool, uuid, token, version string, opts ...connect.ClientOption) *HTTPClient { +func New(endpoint string, insecure bool, uuid, token string, opts ...connect.ClientOption) *HTTPClient { baseURL := strings.TrimRight(endpoint, "/") + "/api/actions" opts = append(opts, connect.WithInterceptors(connect.UnaryInterceptorFunc(func(next connect.UnaryFunc) connect.UnaryFunc { @@ -42,10 +42,6 @@ func New(endpoint string, insecure bool, uuid, token, version string, opts ...co if token != "" { req.Header().Set(TokenHeader, token) } - // TODO: version will be removed from request header after Gitea 1.20 released. - if version != "" { - req.Header().Set(VersionHeader, version) - } return next(ctx, req) } }))) diff --git a/internal/pkg/config/config.go b/internal/pkg/config/config.go index 4f5b11d5..efad73de 100644 --- a/internal/pkg/config/config.go +++ b/internal/pkg/config/config.go @@ -109,7 +109,6 @@ func LoadDefault(file string) (*Config, error) { return nil, fmt.Errorf("parse config file %q for defaults metadata: %w", file, err) } } - compatibleWithOldEnvs(file != "", cfg) if cfg.Runner.EnvFile != "" { if stat, err := os.Stat(cfg.Runner.EnvFile); err == nil && !stat.IsDir() { diff --git a/internal/pkg/config/deprecated.go b/internal/pkg/config/deprecated.go deleted file mode 100644 index b5051aa0..00000000 --- a/internal/pkg/config/deprecated.go +++ /dev/null @@ -1,62 +0,0 @@ -// Copyright 2023 The Gitea Authors. All rights reserved. -// SPDX-License-Identifier: MIT - -package config - -import ( - "os" - "strconv" - "strings" - - log "github.com/sirupsen/logrus" -) - -// Deprecated: could be removed in the future. TODO: remove it when Gitea 1.20.0 is released. -// Be compatible with old envs. -func compatibleWithOldEnvs(fileUsed bool, cfg *Config) { - handleEnv := func(key string) (string, bool) { - if v, ok := os.LookupEnv(key); ok { - if fileUsed { - log.Warnf("env %s has been ignored because config file is used", key) - return "", false - } - log.Warnf("env %s will be deprecated, please use config file instead", key) - return v, true - } - return "", false - } - - if v, ok := handleEnv("GITEA_DEBUG"); ok { - if b, _ := strconv.ParseBool(v); b { - cfg.Log.Level = "debug" - } - } - if v, ok := handleEnv("GITEA_TRACE"); ok { - if b, _ := strconv.ParseBool(v); b { - cfg.Log.Level = "trace" - } - } - if v, ok := handleEnv("GITEA_RUNNER_CAPACITY"); ok { - if i, _ := strconv.Atoi(v); i > 0 { - cfg.Runner.Capacity = i - } - } - if v, ok := handleEnv("GITEA_RUNNER_FILE"); ok { - cfg.Runner.File = v - } - if v, ok := handleEnv("GITEA_RUNNER_ENVIRON"); ok { - splits := strings.Split(v, ",") - if cfg.Runner.Envs == nil { - cfg.Runner.Envs = map[string]string{} - } - for _, split := range splits { - kv := strings.SplitN(split, ":", 2) - if len(kv) == 2 && kv[0] != "" { - cfg.Runner.Envs[kv[0]] = kv[1] - } - } - } - if v, ok := handleEnv("GITEA_RUNNER_ENV_FILE"); ok { - cfg.Runner.EnvFile = v - } -} diff --git a/internal/pkg/report/reporter.go b/internal/pkg/report/reporter.go index 49e839f2..d86f03a6 100644 --- a/internal/pkg/report/reporter.go +++ b/internal/pkg/report/reporter.go @@ -639,7 +639,7 @@ func (r *Reporter) handleCommand(originalContent, command, value string) *string } func (r *Reporter) parseLogRow(entry *log.Entry) *runnerv1.LogRow { - content := strings.TrimRightFunc(entry.Message, func(r rune) bool { return r == '\r' || r == '\n' }) + content := strings.TrimRight(entry.Message, "\r\n") matches := cmdRegex.FindStringSubmatch(content) if matches != nil { From 2208e7ec63c3266fd3c566810d697ffa49bb10be Mon Sep 17 00:00:00 2001 From: Vi Date: Wed, 20 May 2026 14:09:39 +0000 Subject: [PATCH 09/18] feat: add cache.offline_mode to reuse cached actions (#966) Co-authored-by: silverwind Co-authored-by: silverwind <2021+silverwind@noreply.gitea.com> Co-authored-by: TKaxv_7S <56359+tkaxv_7s@noreply.gitea.com> Co-authored-by: techknowlogick Co-authored-by: TKaxv_7S <954067342@qq.com> Co-authored-by: TKaxv_7S Reviewed-on: https://gitea.com/gitea/runner/pulls/966 Reviewed-by: Nicolas Reviewed-by: silverwind <2021+silverwind@noreply.gitea.com> Co-authored-by: Vi Co-committed-by: Vi --- act/common/git/git.go | 91 ++++++++++++++----------- act/common/git/git_test.go | 48 +++++++++++++ act/runner/runner.go | 2 +- internal/app/run/runner.go | 9 +-- internal/pkg/config/config.example.yaml | 3 + internal/pkg/config/config.go | 1 + 6 files changed, 108 insertions(+), 46 deletions(-) diff --git a/act/common/git/git.go b/act/common/git/git.go index 3ebe9724..f8d22c3d 100644 --- a/act/common/git/git.go +++ b/act/common/git/git.go @@ -243,47 +243,50 @@ type NewGitCloneExecutorInput struct { InsecureSkipTLS bool } -// CloneIfRequired ... -func CloneIfRequired(ctx context.Context, refName plumbing.ReferenceName, input NewGitCloneExecutorInput, logger log.FieldLogger) (*git.Repository, error) { +// CloneIfRequired returns the repository and a boolean indicating whether an existing local clone was reused. +func CloneIfRequired(ctx context.Context, refName plumbing.ReferenceName, input NewGitCloneExecutorInput, logger log.FieldLogger) (*git.Repository, bool, error) { r, err := git.PlainOpen(input.Dir) - if err != nil { - var progressWriter io.Writer - if isatty.IsTerminal(os.Stdout.Fd()) || isatty.IsCygwinTerminal(os.Stdout.Fd()) { - if entry, ok := logger.(*log.Entry); ok { - progressWriter = entry.WriterLevel(log.DebugLevel) - } else if lgr, ok := logger.(*log.Logger); ok { - progressWriter = lgr.WriterLevel(log.DebugLevel) - } else { - log.Errorf("Unable to get writer from logger (type=%T)", logger) - progressWriter = os.Stdout - } - } + if err == nil { + // Reuse existing clone + return r, true, nil + } - cloneOptions := git.CloneOptions{ - URL: input.URL, - Progress: progressWriter, - - InsecureSkipTLS: input.InsecureSkipTLS, // For Gitea - } - if input.Token != "" { - cloneOptions.Auth = &http.BasicAuth{ - Username: "token", - Password: input.Token, - } - } - - r, err = git.PlainCloneContext(ctx, input.Dir, false, &cloneOptions) - if err != nil { - logger.Errorf("Unable to clone %v %s: %v", input.URL, refName, err) - return nil, err - } - - if err = os.Chmod(input.Dir, 0o755); err != nil { - return nil, err + var progressWriter io.Writer + if isatty.IsTerminal(os.Stdout.Fd()) || isatty.IsCygwinTerminal(os.Stdout.Fd()) { + if entry, ok := logger.(*log.Entry); ok { + progressWriter = entry.WriterLevel(log.DebugLevel) + } else if lgr, ok := logger.(*log.Logger); ok { + progressWriter = lgr.WriterLevel(log.DebugLevel) + } else { + log.Errorf("Unable to get writer from logger (type=%T)", logger) + progressWriter = os.Stdout } } - return r, nil + cloneOptions := git.CloneOptions{ + URL: input.URL, + Progress: progressWriter, + + InsecureSkipTLS: input.InsecureSkipTLS, // For Gitea + } + if input.Token != "" { + cloneOptions.Auth = &http.BasicAuth{ + Username: "token", + Password: input.Token, + } + } + + r, err = git.PlainCloneContext(ctx, input.Dir, false, &cloneOptions) + if err != nil { + logger.Errorf("Unable to clone %v %s: %v", input.URL, refName, err) + return nil, false, err + } + + if err = os.Chmod(input.Dir, 0o755); err != nil { + return nil, false, err + } + + return r, false, nil } func gitOptions(token string) (fetchOptions git.FetchOptions, pullOptions git.PullOptions) { @@ -313,7 +316,7 @@ func NewGitCloneExecutor(input NewGitCloneExecutorInput) common.Executor { defer AcquireCloneLock(input.Dir)() refName := plumbing.ReferenceName("refs/heads/" + input.Ref) - r, err := CloneIfRequired(ctx, refName, input, logger) + r, reused, err := CloneIfRequired(ctx, refName, input, logger) if err != nil { return err } @@ -338,10 +341,10 @@ func NewGitCloneExecutor(input NewGitCloneExecutorInput) common.Executor { var hash *plumbing.Hash rev := plumbing.Revision(input.Ref) if hash, err = r.ResolveRevision(rev); err != nil { + // ResolveRevision returns a nil hash on error, and a branch ref legitimately fails + // here (no local refs/heads/); the duck-typing below resolves it. logger.Errorf("Unable to resolve %s: %v", input.Ref, err) - } - - if hash.String() != input.Ref && strings.HasPrefix(hash.String(), input.Ref) { + } else if hash.String() != input.Ref && strings.HasPrefix(hash.String(), input.Ref) { return &Error{ err: ErrShortRef, commit: hash.String(), @@ -392,12 +395,18 @@ func NewGitCloneExecutor(input NewGitCloneExecutorInput) common.Executor { return err } } + + reusedMsg := "" + if !isOfflineMode { if err = w.Pull(&pullOptions); err != nil && err != git.NoErrAlreadyUpToDate { logger.Debugf("Unable to pull %s: %v", refName, err) } + } else if reused { + reusedMsg = " (reused in offline mode)" } - logger.Debugf("Cloned %s to %s", input.URL, input.Dir) + + logger.Debugf("Cloned %s to %s%s", input.URL, input.Dir, reusedMsg) if hash.String() != input.Ref && refType == "branch" { logger.Debugf("Provided ref is not a sha. Updating branch ref after pull") diff --git a/act/common/git/git_test.go b/act/common/git/git_test.go index 710674ca..86ed1af3 100644 --- a/act/common/git/git_test.go +++ b/act/common/git/git_test.go @@ -279,6 +279,54 @@ func TestGitCloneExecutorNonFastForwardRef(t *testing.T) { assert.Equal(t, "second", strings.TrimSpace(string(out)), "working tree should be at the latest commit") } +func TestGitCloneExecutorOfflineMode(t *testing.T) { + gitConfig() + + // Build a local "remote" with a single commit on main. + remoteDir := t.TempDir() + require.NoError(t, gitCmd("init", "--bare", "--initial-branch=main", remoteDir)) + workDir := t.TempDir() + require.NoError(t, gitCmd("clone", remoteDir, workDir)) + require.NoError(t, gitCmd("-C", workDir, "checkout", "-b", "main")) + require.NoError(t, gitCmd("-C", workDir, "commit", "--allow-empty", "-m", "initial")) + require.NoError(t, gitCmd("-C", workDir, "push", "-u", "origin", "main")) + + // Prime the cache with an online clone of main. + cacheDir := t.TempDir() + require.NoError(t, NewGitCloneExecutor(NewGitCloneExecutorInput{ + URL: remoteDir, + Ref: "main", + Dir: cacheDir, + })(context.Background())) + + t.Run("cached branch resolves without fetching", func(t *testing.T) { + // Offline reuse of a cached branch must succeed even though ResolveRevision(input.Ref) + // finds no local refs/heads/. + err := NewGitCloneExecutor(NewGitCloneExecutorInput{ + URL: remoteDir, + Ref: "main", + Dir: cacheDir, + OfflineMode: true, + })(context.Background()) + require.NoError(t, err) + + out, err := exec.Command("git", "-C", cacheDir, "log", "--oneline", "-1", "--format=%s").Output() + require.NoError(t, err) + assert.Equal(t, "initial", strings.TrimSpace(string(out))) + }) + + t.Run("unresolvable cached ref returns error", func(t *testing.T) { + // The ref was never cached; offline mode cannot resolve it and must return an error. + err := NewGitCloneExecutor(NewGitCloneExecutorInput{ + URL: remoteDir, + Ref: "never-fetched", + Dir: cacheDir, + OfflineMode: true, + })(context.Background()) + require.Error(t, err) + }) +} + func gitConfig() { if os.Getenv("GITHUB_ACTIONS") == "true" { var err error diff --git a/act/runner/runner.go b/act/runner/runner.go index cb389ecd..1dda2b18 100644 --- a/act/runner/runner.go +++ b/act/runner/runner.go @@ -30,7 +30,7 @@ type Config struct { Actor string // the user that triggered the event Workdir string // path to working directory ActionCacheDir string // path used for caching action contents - ActionOfflineMode bool // when offline, use caching action contents + ActionOfflineMode bool // when offline, use cached action contents BindWorkdir bool // bind the workdir to the job container EventName string // name of event to run EventPath string // path to JSON file to use for event.json in containers diff --git a/internal/app/run/runner.go b/internal/app/run/runner.go index 6d72415f..23e648cf 100644 --- a/internal/app/run/runner.go +++ b/internal/app/run/runner.go @@ -344,10 +344,11 @@ func (r *Runner) run(ctx context.Context, task *runnerv1.Task, reporter *report. runnerConfig := &runner.Config{ // On Linux, Workdir will be like "///" // On Windows, Workdir will be like "\\\" - Workdir: workdir, - BindWorkdir: r.cfg.Container.BindWorkdir, - ActionCacheDir: filepath.FromSlash(r.cfg.Host.WorkdirParent), - AllocatePTY: r.cfg.Runner.AllocatePTY, + Workdir: workdir, + BindWorkdir: r.cfg.Container.BindWorkdir, + ActionCacheDir: filepath.FromSlash(r.cfg.Host.WorkdirParent), + AllocatePTY: r.cfg.Runner.AllocatePTY, + ActionOfflineMode: r.cfg.Cache.OfflineMode, ReuseContainers: false, ForcePull: r.cfg.Container.ForcePull, diff --git a/internal/pkg/config/config.example.yaml b/internal/pkg/config/config.example.yaml index 24fc478d..53d136a0 100644 --- a/internal/pkg/config/config.example.yaml +++ b/internal/pkg/config/config.example.yaml @@ -102,6 +102,9 @@ cache: # (or `gitea-runner cache-server`) is in use: the runner pre-registers each job's ACTIONS_RUNTIME_TOKEN with the # cache-server, and the cache-server enforces bearer auth + per-repo cache isolation. external_secret: "" + # When true, reuse a cached action instead of fetching from the remote on every job. Note: a moved tag + # (e.g. a re-tagged "v6") or an updated branch stays at the cached commit until its cache entry is removed. + offline_mode: false container: # Specifies the network to which the container will connect. diff --git a/internal/pkg/config/config.go b/internal/pkg/config/config.go index efad73de..5f573ff7 100644 --- a/internal/pkg/config/config.go +++ b/internal/pkg/config/config.go @@ -52,6 +52,7 @@ type Cache struct { Port uint16 `yaml:"port"` // Port specifies the caching port. ExternalServer string `yaml:"external_server"` // ExternalServer specifies the URL of external cache server ExternalSecret string `yaml:"external_secret"` // ExternalSecret is a shared secret between this runner and an external gitea-runner cache-server, enabling per-job ACTIONS_RUNTIME_TOKEN authentication and repo scoping over the network. Leave empty to keep the legacy unauthenticated behavior. + OfflineMode bool `yaml:"offline_mode"` // OfflineMode reuses a cached action without fetching from the remote; a moved tag or branch stays at the cached commit until the cache entry is removed. } // Container represents the configuration for the container. From 4317662a38da60be2bdb8c0f73d70d696c36ecd3 Mon Sep 17 00:00:00 2001 From: Nicolas Date: Wed, 20 May 2026 20:25:44 +0000 Subject: [PATCH 10/18] update docker cli to v29.5.2 (#984) Fixes #981 Reviewed-on: https://gitea.com/gitea/runner/pulls/984 Reviewed-by: Zettat123 <39446+zettat123@noreply.gitea.com> Reviewed-by: Lunny Xiao Co-authored-by: Nicolas Co-committed-by: Nicolas --- go.mod | 2 +- go.sum | 10 ++-------- 2 files changed, 3 insertions(+), 9 deletions(-) diff --git a/go.mod b/go.mod index ba9300fc..347d4d98 100644 --- a/go.mod +++ b/go.mod @@ -11,7 +11,7 @@ require ( github.com/containerd/errdefs v1.0.0 github.com/creack/pty v1.1.24 github.com/distribution/reference v0.6.0 - github.com/docker/cli v29.5.1+incompatible + github.com/docker/cli v29.5.2+incompatible github.com/docker/go-connections v0.7.0 github.com/go-git/go-billy/v5 v5.9.0 github.com/go-git/go-git/v5 v5.19.1 diff --git a/go.sum b/go.sum index 4715ef37..48f52fbb 100644 --- a/go.sum +++ b/go.sum @@ -47,12 +47,8 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk= github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E= -github.com/docker/cli v29.4.3+incompatible h1:u+UliYm2J/rYrIh2FqHQg32neRG8GjbvNuwQRTzGspU= -github.com/docker/cli v29.4.3+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8= -github.com/docker/cli v29.5.0+incompatible h1:FPUvKJoKpeP4Njz8NrQdeUN8o247P7ndTiILtaP5/l4= -github.com/docker/cli v29.5.0+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8= -github.com/docker/cli v29.5.1+incompatible h1:NiufLAJoRcPauFoBNYthfuM4REFwM8H2h9xnLABNHGs= -github.com/docker/cli v29.5.1+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8= +github.com/docker/cli v29.5.2+incompatible h1:ubykJ1Y8LmNRGJ2BuMQ0kHOt/RO1YzGNswqWMJgivuQ= +github.com/docker/cli v29.5.2+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8= github.com/docker/docker-credential-helpers v0.9.6 h1:cT2PbRPSlnMmNTfT2TDMXRyQ1KMWHG7xoTLBcn1ZNv0= github.com/docker/docker-credential-helpers v0.9.6/go.mod h1:v1S+hepowrQXITkEfw6o4+BMbGot02wiKpzWhGUZK6c= github.com/docker/go-connections v0.7.0 h1:6SsRfJddP22WMrCkj19x9WKjEDTB+ahsdiGYf0mN39c= @@ -75,8 +71,6 @@ github.com/go-git/go-billy/v5 v5.9.0 h1:jItGXszUDRtR/AlferWPTMN4j38BQ88XnXKbilmm github.com/go-git/go-billy/v5 v5.9.0/go.mod h1:jCnQMLj9eUgGU7+ludSTYoZL/GGmii14RxKFj7ROgHw= 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.0 h1:+WkVUQZSy/F1Gb13udrMKjIM2PrzsNfDKFSfo5tkMtc= -github.com/go-git/go-git/v5 v5.19.0/go.mod h1:Pb1v0c7/g8aGQJwx9Us09W85yGoyvSwuhEGMH7zjDKQ= github.com/go-git/go-git/v5 v5.19.1 h1:nX27AnaU43/K5bKktKwgBmR9lawoYVe1Ckg0rgzzN00= github.com/go-git/go-git/v5 v5.19.1/go.mod h1:Pb1v0c7/g8aGQJwx9Us09W85yGoyvSwuhEGMH7zjDKQ= github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= From 7b5ebe9618c34ecd0bee180268837f619c889ba6 Mon Sep 17 00:00:00 2001 From: Renovate Bot Date: Thu, 21 May 2026 00:32:31 +0000 Subject: [PATCH 11/18] fix(deps): update module connectrpc.com/connect to v1.20.0 (#985) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This PR contains the following updates: | Package | Change | [Age](https://docs.renovatebot.com/merge-confidence/) | [Confidence](https://docs.renovatebot.com/merge-confidence/) | |---|---|---|---| | [connectrpc.com/connect](https://github.com/connectrpc/connect-go) | `v1.19.2` → `v1.20.0` | ![age](https://developer.mend.io/api/mc/badges/age/go/connectrpc.com%2fconnect/v1.20.0?slim=true) | ![confidence](https://developer.mend.io/api/mc/badges/confidence/go/connectrpc.com%2fconnect/v1.19.2/v1.20.0?slim=true) | --- ### Release Notes
connectrpc/connect-go (connectrpc.com/connect) ### [`v1.20.0`](https://github.com/connectrpc/connect-go/releases/tag/v1.20.0) [Compare Source](https://github.com/connectrpc/connect-go/compare/v1.19.2...v1.20.0) #### What's Changed ##### Other changes - Bump minimum supported Go version to 1.25 by [@​jonbodner-buf](https://github.com/jonbodner-buf) in [#​922](https://github.com/connectrpc/connect-go/issues/922) - Update Unary-Get query parameter order to match spec recommendation by [@​oliversun9](https://github.com/oliversun9) in [#​926](https://github.com/connectrpc/connect-go/issues/926) #### New Contributors - [@​jonbodner-buf](https://github.com/jonbodner-buf) made their first contribution in [#​922](https://github.com/connectrpc/connect-go/issues/922) **Full Changelog**:
--- ### Configuration 📅 **Schedule**: (UTC) - Branch creation - At any time (no schedule defined) - Automerge - At any time (no schedule defined) 🚦 **Automerge**: Disabled by config. Please merge this manually once you are satisfied. ♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the rebase/retry checkbox. 🔕 **Ignore**: Close this PR and you won't be reminded about this update again. --- - [ ] If you want to rebase/retry this PR, check this box --- This PR has been generated by [Mend Renovate](https://github.com/renovatebot/renovate). Reviewed-on: https://gitea.com/gitea/runner/pulls/985 Reviewed-by: silverwind <2021+silverwind@noreply.gitea.com> Co-authored-by: Renovate Bot Co-committed-by: Renovate Bot --- go.mod | 2 +- go.sum | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/go.mod b/go.mod index 347d4d98..26bff317 100644 --- a/go.mod +++ b/go.mod @@ -4,7 +4,7 @@ go 1.26.0 require ( code.gitea.io/actions-proto-go v0.4.1 - connectrpc.com/connect v1.19.2 + connectrpc.com/connect v1.20.0 dario.cat/mergo v1.0.2 github.com/Masterminds/semver v1.5.0 github.com/avast/retry-go/v5 v5.0.0 diff --git a/go.sum b/go.sum index 48f52fbb..bf724ba9 100644 --- a/go.sum +++ b/go.sum @@ -2,6 +2,8 @@ code.gitea.io/actions-proto-go v0.4.1 h1:l0EYhjsgpUe/1VABo2eK7zcoNX2W44WOnb0MSLr code.gitea.io/actions-proto-go v0.4.1/go.mod h1:mn7Wkqz6JbnTOHQpot3yDeHx+O5C9EGhMEE+htvHBas= connectrpc.com/connect v1.19.2 h1:McQ83FGdzL+t60peksi0gXC7MQ/iLKgLduAnThbM0mo= connectrpc.com/connect v1.19.2/go.mod h1:tN20fjdGlewnSFeZxLKb0xwIZ6ozc3OQs2hTXy4du9w= +connectrpc.com/connect v1.20.0 h1:6TNDAB+WeNd2uolWNlYczB5E0KNNaVMNUEx8JEUsPmQ= +connectrpc.com/connect v1.20.0/go.mod h1:A2ygJrukXwWy32vkCAAHNVguZrqZ+jeZ9rGRnGR4dN4= cyphar.com/go-pathrs v0.2.3 h1:0pH8gep37wB0BgaXrEaN1OtZhUMeS7VvaejSr6i822o= cyphar.com/go-pathrs v0.2.3/go.mod h1:y8f1EMG7r+hCuFf/rXsKqMJrJAUoADZGNh5/vZPKcGc= dario.cat/mergo v1.0.2 h1:85+piFYR1tMbRrLcDwR18y4UKJ3aH1Tbzi24VRW1TK8= From b30204aa9432b0bc1da46fcfca8438b76a68e222 Mon Sep 17 00:00:00 2001 From: silverwind Date: Thu, 21 May 2026 15:19:01 +0000 Subject: [PATCH 12/18] fix: clean up job network and container when container start fails (#986) The teardown that removes a job's per-job network and container runs as a `Finally` on the step pipeline in `newJobExecutor`, which only executes after a successful start. When the start itself fails (e.g. a `docker cp` error from a buggy daemon), that `Finally` is skipped, so the network and container leak until Docker's address pool is exhausted and later jobs can no longer create networks. This tears them down in `startContainer` when the start returns an error, reusing the existing `cleanUpJobContainer` teardown. Exposed by the daemon regression in https://gitea.com/gitea/runner/issues/981, where every failed `docker cp` leaked a per-job network. --- This PR was written with the help of Claude Opus 4.7 Reviewed-on: https://gitea.com/gitea/runner/pulls/986 Reviewed-by: Nicolas Co-authored-by: silverwind Co-committed-by: silverwind --- act/runner/run_context.go | 28 +++++++++++++++++-- act/runner/run_context_test.go | 51 ++++++++++++++++++++++++++++++++++ 2 files changed, 77 insertions(+), 2 deletions(-) diff --git a/act/runner/run_context.go b/act/runner/run_context.go index 70919361..37aa7e52 100644 --- a/act/runner/run_context.go +++ b/act/runner/run_context.go @@ -601,10 +601,34 @@ func (rc *RunContext) interpolateOutputs() common.Executor { func (rc *RunContext) startContainer() common.Executor { return func(ctx context.Context) error { + var err error if rc.IsHostEnv(ctx) { - return rc.startHostEnvironment()(ctx) + err = rc.startHostEnvironment()(ctx) + } else { + err = rc.startJobContainer()(ctx) } - return rc.startJobContainer()(ctx) + if err != nil { + // The job executor's teardown only runs after a successful start, so a failed + // start would otherwise leak the per-job network and container. + rc.cleanupFailedStart(ctx) + } + return err + } +} + +func (rc *RunContext) cleanupFailedStart(ctx context.Context) { + if rc.cleanUpJobContainer == nil { + return + } + cleanCtx := ctx + if ctx.Err() != nil { + // the start likely failed because ctx was cancelled, detach so teardown still runs + var cancel context.CancelFunc + cleanCtx, cancel = context.WithTimeout(common.WithLogger(context.Background(), common.Logger(ctx)), time.Minute) + defer cancel() + } + if err := rc.cleanUpJobContainer(cleanCtx); err != nil { + common.Logger(ctx).Errorf("Error while cleaning up after failed container start for job %s: %v", rc.JobName, err) } } diff --git a/act/runner/run_context_test.go b/act/runner/run_context_test.go index b2e2e087..22e993c9 100644 --- a/act/runner/run_context_test.go +++ b/act/runner/run_context_test.go @@ -19,6 +19,7 @@ import ( log "github.com/sirupsen/logrus" assert "github.com/stretchr/testify/assert" + require "github.com/stretchr/testify/require" yaml "go.yaml.in/yaml/v4" ) @@ -659,3 +660,53 @@ func TestPrintStartJobContainerGroupGolden(t *testing.T) { }, "\n") assert.Equal(t, want, buf.String()) } + +func TestRunContext_cleanupFailedStart(t *testing.T) { + type ctxKey string + const sentinel = ctxKey("sentinel") + + // the fresh context is cancelled via defer on return, so capture state inside the stub + type capture struct { + calls int + err error + sentinel any + } + newRC := func(c *capture) *RunContext { + return &RunContext{ + JobName: "job", + cleanUpJobContainer: func(ctx context.Context) error { + c.calls++ + c.err = ctx.Err() + c.sentinel = ctx.Value(sentinel) + return nil + }, + } + } + + t.Run("runs teardown on the live context", func(t *testing.T) { + var c capture + ctx := context.WithValue(context.Background(), sentinel, "v") + + newRC(&c).cleanupFailedStart(ctx) + + assert.Equal(t, 1, c.calls) + require.NoError(t, c.err) + assert.Equal(t, "v", c.sentinel) + }) + + t.Run("falls back to a fresh context when the input is done", func(t *testing.T) { + var c capture + ctx, cancel := context.WithCancel(context.WithValue(context.Background(), sentinel, "v")) + cancel() + + newRC(&c).cleanupFailedStart(ctx) + + assert.Equal(t, 1, c.calls) + require.NoError(t, c.err) + assert.Nil(t, c.sentinel) + }) + + t.Run("no-op when there is nothing to clean up", func(t *testing.T) { + assert.NotPanics(t, func() { (&RunContext{}).cleanupFailedStart(context.Background()) }) + }) +} From d6fbe757218254ec764a4bfa573303b1292f861c Mon Sep 17 00:00:00 2001 From: Nicolas Date: Thu, 21 May 2026 19:13:43 +0000 Subject: [PATCH 13/18] ci: add PR title linting against Conventional Commits (#988) Lint PR titles Reviewed-on: https://gitea.com/gitea/runner/pulls/988 Reviewed-by: Lunny Xiao --- .gitea/workflows/pull-pr-title.yml | 27 +++++++++++++++++++++++++++ Makefile | 4 ++++ tools/lint-pr-title.ts | 19 +++++++++++++++++++ 3 files changed, 50 insertions(+) create mode 100644 .gitea/workflows/pull-pr-title.yml create mode 100644 tools/lint-pr-title.ts diff --git a/.gitea/workflows/pull-pr-title.yml b/.gitea/workflows/pull-pr-title.yml new file mode 100644 index 00000000..9ae1d9a9 --- /dev/null +++ b/.gitea/workflows/pull-pr-title.yml @@ -0,0 +1,27 @@ +name: pr-title + +on: + pull_request: + types: + - opened + - edited + - reopened + - synchronize + - ready_for_review + +permissions: + contents: read + +jobs: + lint-pr-title: + if: github.event.pull_request.draft == false + runs-on: ubuntu-latest + timeout-minutes: 5 + steps: + - uses: actions/checkout@v6 + - uses: actions/setup-node@v4 + with: + node-version: 24 + - run: make lint-pr-title + env: + PR_TITLE: ${{ github.event.pull_request.title }} diff --git a/Makefile b/Makefile index caaeb70c..693685e1 100644 --- a/Makefile +++ b/Makefile @@ -118,6 +118,10 @@ lint-go: ## lint go files lint-go-fix: ## lint go files and fix issues $(GO) run $(GOLANGCI_LINT_PACKAGE) run --fix +.PHONY: lint-pr-title +lint-pr-title: ## lint PR title against Conventional Commits (set PR_TITLE=...) + @node ./tools/lint-pr-title.ts + .PHONY: security-check security-check: deps-tools GOEXPERIMENT= $(GO) run $(GOVULNCHECK_PACKAGE) -show color ./... || true diff --git a/tools/lint-pr-title.ts b/tools/lint-pr-title.ts new file mode 100644 index 00000000..a63defe9 --- /dev/null +++ b/tools/lint-pr-title.ts @@ -0,0 +1,19 @@ +#!/usr/bin/env node +import {env, exit} from 'node:process'; + +const allowedTypes = 'build, chore, ci, docs, enhance, feat, fix, perf, refactor, revert, style, test'; +const title = env.PR_TITLE; + +if (!title) { + console.error('Missing PR_TITLE'); + exit(1); +} + +const validTitlePattern = new RegExp(`^(${allowedTypes.replaceAll(', ', '|')})(\\([\\w.-]+\\))?(!)?: .+$`); + +if (!validTitlePattern.test(title)) { + console.error(`Invalid PR title: ${title}`); + console.error('Expected format: type(scope): subject'); + console.error(`Allowed types: ${allowedTypes}`); + exit(1); +} From 0e0c54b272a2e31bd5f5ab34b5e53de42bb8e1a1 Mon Sep 17 00:00:00 2001 From: silverwind Date: Thu, 21 May 2026 19:16:23 +0000 Subject: [PATCH 14/18] test: make TestRunEvent integration suite runnable locally (#987) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The `TestRunEvent*` integration tests are skipped in CI (`make test` runs `-short`), which hid several breakages that make them fail when run locally: - `runTest` built the runner `Config` without `ContainerMaxLifetime`, so the job container ran `/bin/sleep 0` and exited immediately — every step failed with "container is not running". Set it to 1h. - The root `.gitignore`'s unscoped `.env` and `dist` rules shadowed fixtures under `testdata/`. Anchored `dist` → `/dist` (the goreleaser output) and un-ignored `testdata/secrets/.env`. - Added the missing `testdata/secrets/.env` fixture for `TestRunEventSecrets`. - The `node24` local action referenced a `dist/index.js` bundle that was never committed (and was gitignored). Made the fixture self-contained (dependency-free ESM, `main: index.js`) so it runs without an `ncc` build. If you'd rather keep the `@actions/core`-based action and commit the built bundle instead, happy to switch. Network-dependent subtests (remote `uses:`/composite actions) are out of scope. --- This PR was written with the help of Claude Opus 4.7 --------- Co-authored-by: Nicolas Co-authored-by: Lunny Xiao Reviewed-on: https://gitea.com/gitea/runner/pulls/987 Reviewed-by: Nicolas Co-authored-by: silverwind Co-committed-by: silverwind --- .gitignore | 3 ++- act/runner/runner_test.go | 2 ++ act/runner/testdata/actions/node24/action.yml | 2 +- act/runner/testdata/actions/node24/index.js | 21 +++++++++++-------- .../testdata/actions/node24/package.json | 20 ++---------------- act/runner/testdata/secrets/.env | 2 ++ 6 files changed, 21 insertions(+), 29 deletions(-) create mode 100644 act/runner/testdata/secrets/.env diff --git a/.gitignore b/.gitignore index b7de5a70..14621da8 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,6 @@ /gitea-runner .env +!/act/runner/testdata/secrets/.env .runner coverage.txt /config.yaml @@ -10,4 +11,4 @@ coverage.txt .vscode __debug_bin # gorelease binary folder -dist +/dist diff --git a/act/runner/runner_test.go b/act/runner/runner_test.go index b61a16cb..53cac993 100644 --- a/act/runner/runner_test.go +++ b/act/runner/runner_test.go @@ -15,6 +15,7 @@ import ( "runtime" "strings" "testing" + "time" "gitea.com/gitea/runner/act/common" "gitea.com/gitea/runner/act/model" @@ -192,6 +193,7 @@ func (j *TestJobFileInfo) runTest(ctx context.Context, t *testing.T, cfg *Config Inputs: cfg.Inputs, GitHubInstance: "github.com", ContainerArchitecture: cfg.ContainerArchitecture, + ContainerMaxLifetime: time.Hour, Matrix: cfg.Matrix, ActionCache: cfg.ActionCache, } diff --git a/act/runner/testdata/actions/node24/action.yml b/act/runner/testdata/actions/node24/action.yml index 522b1faf..68e88844 100644 --- a/act/runner/testdata/actions/node24/action.yml +++ b/act/runner/testdata/actions/node24/action.yml @@ -10,4 +10,4 @@ outputs: description: 'The time we greeted you' runs: using: 'node24' - main: 'dist/index.js' + main: 'index.js' diff --git a/act/runner/testdata/actions/node24/index.js b/act/runner/testdata/actions/node24/index.js index 5aa4a837..730fa993 100644 --- a/act/runner/testdata/actions/node24/index.js +++ b/act/runner/testdata/actions/node24/index.js @@ -1,11 +1,14 @@ -import {getInput, setOutput, setFailed} from '@actions/core'; -import {context} from '@actions/github'; +import {appendFileSync, readFileSync} from 'node:fs'; -try { - const nameToGreet = getInput('who-to-greet'); - console.log(`Hello ${nameToGreet}!`); - setOutput('time', (new Date()).toTimeString()); - console.log(`The event payload: ${JSON.stringify(context.payload, undefined, 2)}`); -} catch (error) { - setFailed(error.message); +const nameToGreet = process.env['INPUT_WHO-TO-GREET'] || 'World'; +console.log(`Hello ${nameToGreet}!`); + +if (process.env.GITHUB_OUTPUT) { + appendFileSync(process.env.GITHUB_OUTPUT, `time=${new Date().toTimeString()}\n`); } + +let payload = {}; +if (process.env.GITHUB_EVENT_PATH) { + payload = JSON.parse(readFileSync(process.env.GITHUB_EVENT_PATH, 'utf8')); +} +console.log(`The event payload: ${JSON.stringify(payload, undefined, 2)}`); diff --git a/act/runner/testdata/actions/node24/package.json b/act/runner/testdata/actions/node24/package.json index 8603ec31..3dbda4aa 100644 --- a/act/runner/testdata/actions/node24/package.json +++ b/act/runner/testdata/actions/node24/package.json @@ -1,21 +1,5 @@ { "name": "node24", - "version": "1.0.0", - "description": "", - "main": "index.js", - "type": "module", - "scripts": { - "build": "ncc build index.js" - }, - "license": "ISC", - "dependencies": { - "@actions/core": "^3.0.1", - "@actions/github": "^9.1.1" - }, - "devDependencies": { - "@vercel/ncc": "^0.38.4" - }, - "engines": { - "node": ">=24" - } + "private": true, + "type": "module" } diff --git a/act/runner/testdata/secrets/.env b/act/runner/testdata/secrets/.env new file mode 100644 index 00000000..3b66cf2a --- /dev/null +++ b/act/runner/testdata/secrets/.env @@ -0,0 +1,2 @@ +HELLO=WORLD +MULTILINE_ENV="foo\nbar\nbaz" From 1c62c0635f38c8663137a2267cbe2ca7ade9182c Mon Sep 17 00:00:00 2001 From: Renovate Bot Date: Fri, 22 May 2026 06:28:26 +0000 Subject: [PATCH 15/18] chore(deps): update actions/setup-node action to v6 (#991) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This PR contains the following updates: | Package | Type | Update | Change | |---|---|---|---| | [actions/setup-node](https://github.com/actions/setup-node) | action | major | `v4` → `v6` | --- ### Release Notes
actions/setup-node (actions/setup-node) ### [`v6.4.0`](https://github.com/actions/setup-node/releases/tag/v6.4.0) [Compare Source](https://github.com/actions/setup-node/compare/v6.3.0...v6.4.0) #### What's Changed ##### Dependency updates: - Upgrade [@​actions](https://github.com/actions) dependencies by [@​Copilot](https://github.com/Copilot) in [#​1525](https://github.com/actions/setup-node/pull/1525) - Update Node.js versions in versions.yml and bump package to v6.4.0 by [@​priya-kinthali](https://github.com/priya-kinthali) in [#​1533](https://github.com/actions/setup-node/pull/1533) #### New Contributors - [@​Copilot](https://github.com/Copilot) made their first contribution in [#​1525](https://github.com/actions/setup-node/pull/1525) **Full Changelog**: ### [`v6.3.0`](https://github.com/actions/setup-node/releases/tag/v6.3.0) [Compare Source](https://github.com/actions/setup-node/compare/v6.2.0...v6.3.0) #### What's Changed ##### Enhancements: - Support parsing `devEngines` field by [@​susnux](https://github.com/susnux) in [#​1283](https://github.com/actions/setup-node/pull/1283) > When using node-version-file: package.json, setup-node now prefers devEngines.runtime over engines.node. ##### Dependency updates: - Fix npm audit issues by [@​gowridurgad](https://github.com/gowridurgad) in [#​1491](https://github.com/actions/setup-node/pull/1491) - Replace uuid with crypto.randomUUID() by [@​trivikr](https://github.com/trivikr) in [#​1378](https://github.com/actions/setup-node/pull/1378) - Upgrade minimatch from 3.1.2 to 3.1.5 by [@​dependabot](https://github.com/dependabot) in [#​1498](https://github.com/actions/setup-node/pull/1498) ##### Bug fixes: - Remove hardcoded bearer for mirror-url [@​marco-ippolito](https://github.com/marco-ippolito) in [#​1467](https://github.com/actions/setup-node/pull/1467) - Scope test lockfiles by package manager and update cache tests by [@​gowridurgad](https://github.com/gowridurgad) in [#​1495](https://github.com/actions/setup-node/pull/1495) #### New Contributors - [@​susnux](https://github.com/susnux) made their first contribution in [#​1283](https://github.com/actions/setup-node/pull/1283) **Full Changelog**: ### [`v6.2.0`](https://github.com/actions/setup-node/releases/tag/v6.2.0) [Compare Source](https://github.com/actions/setup-node/compare/v6.1.0...v6.2.0) #### What's Changed ##### Documentation - Documentation update related to absence of Lockfile by [@​mahabaleshwars](https://github.com/mahabaleshwars) in [#​1454](https://github.com/actions/setup-node/pull/1454) - Correct mirror option typos by [@​MikeMcC399](https://github.com/MikeMcC399) in [#​1442](https://github.com/actions/setup-node/pull/1442) - Readme update on checkout version v6 by [@​deining](https://github.com/deining) in [#​1446](https://github.com/actions/setup-node/pull/1446) - Readme typo fixes [@​munyari](https://github.com/munyari) in [#​1226](https://github.com/actions/setup-node/pull/1226) - Advanced document update on checkout version v6 by [@​aparnajyothi-y](https://github.com/aparnajyothi-y) in [#​1468](https://github.com/actions/setup-node/pull/1468) ##### Dependency updates: - Upgrade [@​actions/cache](https://github.com/actions/cache) to v5.0.1 by [@​salmanmkc](https://github.com/salmanmkc) in [#​1449](https://github.com/actions/setup-node/pull/1449) #### New Contributors - [@​mahabaleshwars](https://github.com/mahabaleshwars) made their first contribution in [#​1454](https://github.com/actions/setup-node/pull/1454) - [@​MikeMcC399](https://github.com/MikeMcC399) made their first contribution in [#​1442](https://github.com/actions/setup-node/pull/1442) - [@​deining](https://github.com/deining) made their first contribution in [#​1446](https://github.com/actions/setup-node/pull/1446) - [@​munyari](https://github.com/munyari) made their first contribution in [#​1226](https://github.com/actions/setup-node/pull/1226) **Full Changelog**: ### [`v6.1.0`](https://github.com/actions/setup-node/releases/tag/v6.1.0) [Compare Source](https://github.com/actions/setup-node/compare/v6...v6.1.0) #### What's Changed ##### Enhancement: - Remove always-auth configuration handling by [@​priyagupta108](https://github.com/priyagupta108) in [#​1436](https://github.com/actions/setup-node/pull/1436) ##### Dependency updates: - Upgrade [@​actions/cache](https://github.com/actions/cache) from 4.0.3 to 4.1.0 by [@​dependabot](https://github.com/dependabot)\[bot] in [#​1384](https://github.com/actions/setup-node/pull/1384) - Upgrade actions/checkout from 5 to 6 by [@​dependabot](https://github.com/dependabot)\[bot] in [#​1439](https://github.com/actions/setup-node/pull/1439) - Upgrade js-yaml from 3.14.1 to 3.14.2 by [@​dependabot](https://github.com/dependabot)\[bot] in [#​1435](https://github.com/actions/setup-node/pull/1435) ##### Documentation update: - Add example for restore-only cache in documentation by [@​aparnajyothi-y](https://github.com/aparnajyothi-y) in [#​1419](https://github.com/actions/setup-node/pull/1419) **Full Changelog**: ### [`v6.0.0`](https://github.com/actions/setup-node/releases/tag/v6.0.0) [Compare Source](https://github.com/actions/setup-node/compare/v6...v6) #### What's Changed **Breaking Changes** - Limit automatic caching to npm, update workflows and documentation by [@​priyagupta108](https://github.com/priyagupta108) in [#​1374](https://github.com/actions/setup-node/pull/1374) **Dependency Upgrades** - Upgrade ts-jest from 29.1.2 to 29.4.1 and document breaking changes in v5 by [@​dependabot](https://github.com/dependabot)\[bot] in [#​1336](https://github.com/actions/setup-node/pull/1336) - Upgrade prettier from 2.8.8 to 3.6.2 by [@​dependabot](https://github.com/dependabot)\[bot] in [#​1334](https://github.com/actions/setup-node/pull/1334) - Upgrade actions/publish-action from 0.3.0 to 0.4.0 by [@​dependabot](https://github.com/dependabot)\[bot] in [#​1362](https://github.com/actions/setup-node/pull/1362) **Full Changelog**: ### [`v6`](https://github.com/actions/setup-node/compare/v5.0.0...v6) [Compare Source](https://github.com/actions/setup-node/compare/v5.0.0...v6) ### [`v5.0.0`](https://github.com/actions/setup-node/releases/tag/v5.0.0) [Compare Source](https://github.com/actions/setup-node/compare/v5.0.0...v5.0.0) #### What's Changed ##### Breaking Changes - Enhance caching in setup-node with automatic package manager detection by [@​priya-kinthali](https://github.com/priya-kinthali) in [#​1348](https://github.com/actions/setup-node/pull/1348) This update, introduces automatic caching when a valid `packageManager` field is present in your `package.json`. This aims to improve workflow performance and make dependency management more seamless. To disable this automatic caching, set `package-manager-cache: false` ```yaml steps: - uses: actions/checkout@v5 - uses: actions/setup-node@v5 with: package-manager-cache: false ``` - Upgrade action to use node24 by [@​salmanmkc](https://github.com/salmanmkc) in [#​1325](https://github.com/actions/setup-node/pull/1325) Make sure your runner is on version v2.327.1 or later to ensure compatibility with this release. [See Release Notes](https://github.com/actions/runner/releases/tag/v2.327.1) ##### Dependency Upgrades - Upgrade [@​octokit/request-error](https://github.com/octokit/request-error) and [@​actions/github](https://github.com/actions/github) by [@​dependabot](https://github.com/dependabot)\[bot] in [#​1227](https://github.com/actions/setup-node/pull/1227) - Upgrade uuid from 9.0.1 to 11.1.0 by [@​dependabot](https://github.com/dependabot)\[bot] in [#​1273](https://github.com/actions/setup-node/pull/1273) - Upgrade undici from 5.28.5 to 5.29.0 by [@​dependabot](https://github.com/dependabot)\[bot] in [#​1295](https://github.com/actions/setup-node/pull/1295) - Upgrade form-data to bring in fix for critical vulnerability by [@​gowridurgad](https://github.com/gowridurgad) in [#​1332](https://github.com/actions/setup-node/pull/1332) - Upgrade actions/checkout from 4 to 5 by [@​dependabot](https://github.com/dependabot)\[bot] in [#​1345](https://github.com/actions/setup-node/pull/1345) #### New Contributors - [@​priya-kinthali](https://github.com/priya-kinthali) made their first contribution in [#​1348](https://github.com/actions/setup-node/pull/1348) - [@​salmanmkc](https://github.com/salmanmkc) made their first contribution in [#​1325](https://github.com/actions/setup-node/pull/1325) **Full Changelog**: ### [`v5`](https://github.com/actions/setup-node/compare/v4.4.0...v5.0.0) [Compare Source](https://github.com/actions/setup-node/compare/v4.4.0...v5.0.0) ### [`v4.4.0`](https://github.com/actions/setup-node/releases/tag/v4.4.0) [Compare Source](https://github.com/actions/setup-node/compare/v4.3.0...v4.4.0) #### What's Changed ##### Bug fixes: - Make eslint-compact matcher compatible with Stylelint by [@​FloEdelmann](https://github.com/FloEdelmann) in [#​98](https://github.com/actions/setup-node/pull/98) - Add support for indented eslint output by [@​fregante](https://github.com/fregante) in [#​1245](https://github.com/actions/setup-node/pull/1245) ##### Enhancement: - Support private mirrors by [@​marco-ippolito](https://github.com/marco-ippolito) in [#​1240](https://github.com/actions/setup-node/pull/1240) ##### Dependency update: - Upgrade [@​action/cache](https://github.com/action/cache) from 4.0.2 to 4.0.3 by [@​aparnajyothi-y](https://github.com/aparnajyothi-y) in [#​1262](https://github.com/actions/setup-node/pull/1262) #### New Contributors - [@​FloEdelmann](https://github.com/FloEdelmann) made their first contribution in [#​98](https://github.com/actions/setup-node/pull/98) - [@​fregante](https://github.com/fregante) made their first contribution in [#​1245](https://github.com/actions/setup-node/pull/1245) - [@​marco-ippolito](https://github.com/marco-ippolito) made their first contribution in [#​1240](https://github.com/actions/setup-node/pull/1240) **Full Changelog**:  ### [`v4.3.0`](https://github.com/actions/setup-node/releases/tag/v4.3.0) [Compare Source](https://github.com/actions/setup-node/compare/v4.2.0...v4.3.0) #### What's Changed ##### Dependency updates - Upgrade [@​actions/glob](https://github.com/actions/glob) from 0.4.0 to 0.5.0 by [@​dependabot](https://github.com/dependabot) in [#​1200](https://github.com/actions/setup-node/pull/1200) - Upgrade [@​action/cache](https://github.com/action/cache) from 4.0.0 to 4.0.2 by [@​gowridurgad](https://github.com/gowridurgad) in [#​1251](https://github.com/actions/setup-node/pull/1251) - Upgrade [@​vercel/ncc](https://github.com/vercel/ncc) from 0.38.1 to 0.38.3 by [@​dependabot](https://github.com/dependabot) in [#​1203](https://github.com/actions/setup-node/pull/1203) - Upgrade [@​actions/tool-cache](https://github.com/actions/tool-cache) from 2.0.1 to 2.0.2 by [@​dependabot](https://github.com/dependabot) in [#​1220](https://github.com/actions/setup-node/pull/1220) #### New Contributors - [@​gowridurgad](https://github.com/gowridurgad) made their first contribution in [#​1251](https://github.com/actions/setup-node/pull/1251) **Full Changelog**: ### [`v4.2.0`](https://github.com/actions/setup-node/releases/tag/v4.2.0) [Compare Source](https://github.com/actions/setup-node/compare/v4.1.0...v4.2.0) #### What's Changed - Enhance workflows and upgrade publish-actions from 0.2.2 to 0.3.0 by [@​aparnajyothi-y](https://github.com/aparnajyothi-y) in [#​1174](https://github.com/actions/setup-node/pull/1174) - Add recommended permissions section to readme by [@​benwells](https://github.com/benwells) in [#​1193](https://github.com/actions/setup-node/pull/1193) - Configure Dependabot settings by [@​HarithaVattikuti](https://github.com/HarithaVattikuti) in [#​1192](https://github.com/actions/setup-node/pull/1192) - Upgrade `@actions/cache` to `^4.0.0` by [@​priyagupta108](https://github.com/priyagupta108) in [#​1191](https://github.com/actions/setup-node/pull/1191) - Upgrade pnpm/action-setup from 2 to 4 by [@​dependabot](https://github.com/dependabot) in [#​1194](https://github.com/actions/setup-node/pull/1194) - Upgrade actions/publish-immutable-action from 0.0.3 to 0.0.4 by [@​dependabot](https://github.com/dependabot) in [#​1195](https://github.com/actions/setup-node/pull/1195) - Upgrade semver from 7.6.0 to 7.6.3 by [@​dependabot](https://github.com/dependabot) in [#​1196](https://github.com/actions/setup-node/pull/1196) - Upgrade [@​types/jest](https://github.com/types/jest) from 29.5.12 to 29.5.14 by [@​dependabot](https://github.com/dependabot) in [#​1201](https://github.com/actions/setup-node/pull/1201) - Upgrade undici from 5.28.4 to 5.28.5 by [@​dependabot](https://github.com/dependabot) in [#​1205](https://github.com/actions/setup-node/pull/1205) #### New Contributors - [@​benwells](https://github.com/benwells) made their first contribution in [#​1193](https://github.com/actions/setup-node/pull/1193) **Full Changelog**: ### [`v4.1.0`](https://github.com/actions/setup-node/releases/tag/v4.1.0) [Compare Source](https://github.com/actions/setup-node/compare/v4.0.4...v4.1.0) #### What's Changed - Resolve High Security Alerts by upgrading Dependencies by [@​aparnajyothi-y](https://github.com/aparnajyothi-y) in [#​1132](https://github.com/actions/setup-node/pull/1132) - Upgrade IA Publish by [@​Jcambass](https://github.com/Jcambass) in [#​1134](https://github.com/actions/setup-node/pull/1134) - Revise `isGhes` logic by [@​jww3](https://github.com/jww3) in [#​1148](https://github.com/actions/setup-node/pull/1148) - Add architecture to cache key by [@​pengx17](https://github.com/pengx17) in [#​843](https://github.com/actions/setup-node/pull/843) This addresses issues with caching by adding the architecture (arch) to the cache key, ensuring that cache keys are accurate to prevent conflicts. Note: This change may break previous cache keys as they will no longer be compatible with the new format. #### New Contributors - [@​jww3](https://github.com/jww3) made their first contribution in [#​1148](https://github.com/actions/setup-node/pull/1148) - [@​pengx17](https://github.com/pengx17) made their first contribution in [#​843](https://github.com/actions/setup-node/pull/843) **Full Changelog**: ### [`v4.0.4`](https://github.com/actions/setup-node/releases/tag/v4.0.4) [Compare Source](https://github.com/actions/setup-node/compare/v4.0.3...v4.0.4) #### What's Changed - Add workflow file for publishing releases to immutable action package by [@​Jcambass](https://github.com/Jcambass) in [#​1125](https://github.com/actions/setup-node/pull/1125) - Enhance Windows ARM64 Setup and Update micromatch Dependency by [@​priyagupta108](https://github.com/priyagupta108) in [#​1126](https://github.com/actions/setup-node/pull/1126) ##### Documentation changes: - Documentation update in the README file by [@​suyashgaonkar](https://github.com/suyashgaonkar) in [#​1106](https://github.com/actions/setup-node/pull/1106) - Correct invalid 'lts' version string reference by [@​fulldecent](https://github.com/fulldecent) in [#​1124](https://github.com/actions/setup-node/pull/1124) #### New Contributors - [@​suyashgaonkar](https://github.com/suyashgaonkar) made their first contribution in [#​1106](https://github.com/actions/setup-node/pull/1106) - [@​priyagupta108](https://github.com/priyagupta108) made their first contribution in [#​1126](https://github.com/actions/setup-node/pull/1126) - [@​Jcambass](https://github.com/Jcambass) made their first contribution in [#​1125](https://github.com/actions/setup-node/pull/1125) - [@​fulldecent](https://github.com/fulldecent) made their first contribution in [#​1124](https://github.com/actions/setup-node/pull/1124) **Full Changelog**: ### [`v4.0.3`](https://github.com/actions/setup-node/releases/tag/v4.0.3) [Compare Source](https://github.com/actions/setup-node/compare/v4.0.2...v4.0.3) #### What's Changed ##### Bug fixes: - Fix macos latest check failures by [@​HarithaVattikuti](https://github.com/HarithaVattikuti) in [#​1041](https://github.com/actions/setup-node/pull/1041) ##### Documentation changes: - Documentation update to update default Node version to 20 by [@​bengreeley](https://github.com/bengreeley) in [#​949](https://github.com/actions/setup-node/pull/949) ##### Dependency updates: - Bump undici from 5.26.5 to 5.28.3 by [@​dependabot](https://github.com/dependabot) in [#​965](https://github.com/actions/setup-node/pull/965) - Bump braces from 3.0.2 to 3.0.3 and other dependency updates by [@​dependabot](https://github.com/dependabot) in [#​1087](https://github.com/actions/setup-node/pull/1087) #### New Contributors - [@​bengreeley](https://github.com/bengreeley) made their first contribution in [#​949](https://github.com/actions/setup-node/pull/949) - [@​HarithaVattikuti](https://github.com/HarithaVattikuti) made their first contribution in [#​1041](https://github.com/actions/setup-node/pull/1041) **Full Changelog**: ### [`v4.0.2`](https://github.com/actions/setup-node/releases/tag/v4.0.2) [Compare Source](https://github.com/actions/setup-node/compare/v4.0.1...v4.0.2) #### What's Changed - Add support for `volta.extends` by [@​ThisIsManta](https://github.com/ThisIsManta) in [#​921](https://github.com/actions/setup-node/pull/921) - Add support for arm64 Windows by [@​dmitry-shibanov](https://github.com/dmitry-shibanov) in [#​927](https://github.com/actions/setup-node/pull/927) #### New Contributors - [@​ThisIsManta](https://github.com/ThisIsManta) made their first contribution in [#​921](https://github.com/actions/setup-node/pull/921) **Full Changelog**: ### [`v4.0.1`](https://github.com/actions/setup-node/releases/tag/v4.0.1) [Compare Source](https://github.com/actions/setup-node/compare/v4...v4.0.1) #### What's Changed - Ignore engines in Yarn 1 e2e-cache tests by [@​trivikr](https://github.com/trivikr) in [#​882](https://github.com/actions/setup-node/pull/882) - Update setup-node references in the README.md file to setup-node\@​v4 by [@​jwetzell](https://github.com/jwetzell) in [#​884](https://github.com/actions/setup-node/pull/884) - Update reusable workflows to use Node.js v20 by [@​MaksimZhukov](https://github.com/MaksimZhukov) in [#​889](https://github.com/actions/setup-node/pull/889) - Add fix for cache to resolve slow post action step by [@​aparnajyothi-y](https://github.com/aparnajyothi-y) in [#​917](https://github.com/actions/setup-node/pull/917) - Fix README.md by [@​takayamaki](https://github.com/takayamaki) in [#​898](https://github.com/actions/setup-node/pull/898) - Add `package.json` to `node-version-file` list of examples. by [@​TWiStErRob](https://github.com/TWiStErRob) in [#​879](https://github.com/actions/setup-node/pull/879) - Fix node-version-file interprets entire package.json as a version by [@​NullVoxPopuli](https://github.com/NullVoxPopuli) in [#​865](https://github.com/actions/setup-node/pull/865) #### New Contributors - [@​trivikr](https://github.com/trivikr) made their first contribution in [#​882](https://github.com/actions/setup-node/pull/882) - [@​jwetzell](https://github.com/jwetzell) made their first contribution in [#​884](https://github.com/actions/setup-node/pull/884) - [@​aparnajyothi-y](https://github.com/aparnajyothi-y) made their first contribution in [#​917](https://github.com/actions/setup-node/pull/917) - [@​takayamaki](https://github.com/takayamaki) made their first contribution in [#​898](https://github.com/actions/setup-node/pull/898) - [@​TWiStErRob](https://github.com/TWiStErRob) made their first contribution in [#​879](https://github.com/actions/setup-node/pull/879) - [@​NullVoxPopuli](https://github.com/NullVoxPopuli) made their first contribution in [#​865](https://github.com/actions/setup-node/pull/865) **Full Changelog**:
--- ### Configuration 📅 **Schedule**: (UTC) - Branch creation - At any time (no schedule defined) - Automerge - At any time (no schedule defined) 🚦 **Automerge**: Disabled by config. Please merge this manually once you are satisfied. ♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the rebase/retry checkbox. 🔕 **Ignore**: Close this PR and you won't be reminded about this update again. --- - [ ] If you want to rebase/retry this PR, check this box --- This PR has been generated by [Mend Renovate](https://github.com/renovatebot/renovate). Reviewed-on: https://gitea.com/gitea/runner/pulls/991 Reviewed-by: silverwind <2021+silverwind@noreply.gitea.com> Co-authored-by: Renovate Bot Co-committed-by: Renovate Bot --- .gitea/workflows/pull-pr-title.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.gitea/workflows/pull-pr-title.yml b/.gitea/workflows/pull-pr-title.yml index 9ae1d9a9..315f11d8 100644 --- a/.gitea/workflows/pull-pr-title.yml +++ b/.gitea/workflows/pull-pr-title.yml @@ -19,7 +19,7 @@ jobs: timeout-minutes: 5 steps: - uses: actions/checkout@v6 - - uses: actions/setup-node@v4 + - uses: actions/setup-node@v6 with: node-version: 24 - run: make lint-pr-title From 38b69bb214fcd1070ae19995833b48f9611d7aa5 Mon Sep 17 00:00:00 2001 From: silverwind <2021+silverwind@noreply.gitea.com> Date: Fri, 22 May 2026 07:09:56 +0000 Subject: [PATCH 16/18] chore: pin Docker base images to explicit versions (#992) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Pin floating image tags: - `golang` → `1.26-alpine3.23` - `docker` dind variants → `29.5.2` - `alpine` (basic stage + test fixture) → `3.23` `ubuntu:24.04` and `scratch` left unchanged (no more-specific tag). --- This PR was written with the help of Claude Opus 4.7 Reviewed-on: https://gitea.com/gitea/runner/pulls/992 Reviewed-by: Lunny Xiao --- Dockerfile | 8 ++++---- .../docker/Dockerfile | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/Dockerfile b/Dockerfile index 63bfc7ba..543f2d29 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,7 +1,7 @@ ### BUILDER STAGE # # -FROM golang:1.26-alpine AS builder +FROM golang:1.26-alpine3.23 AS builder # Do not remove `git` here, it is required for getting runner version when executing `make build` RUN apk add --no-cache make git @@ -17,7 +17,7 @@ RUN make clean && make build ### DIND VARIANT # # -FROM docker:29-dind AS dind +FROM docker:29.5.2-dind AS dind ARG VERSION=dev @@ -37,7 +37,7 @@ ENTRYPOINT ["s6-svscan","/etc/s6"] ### DIND-ROOTLESS VARIANT # # -FROM docker:29-dind-rootless AS dind-rootless +FROM docker:29.5.2-dind-rootless AS dind-rootless ARG VERSION=dev @@ -63,7 +63,7 @@ ENTRYPOINT ["s6-svscan","/etc/s6"] ### BASIC VARIANT # # -FROM alpine AS basic +FROM alpine:3.23 AS basic ARG VERSION=dev diff --git a/act/runner/testdata/actions-environment-and-context-tests/docker/Dockerfile b/act/runner/testdata/actions-environment-and-context-tests/docker/Dockerfile index bd8fcb22..2471eac3 100644 --- a/act/runner/testdata/actions-environment-and-context-tests/docker/Dockerfile +++ b/act/runner/testdata/actions-environment-and-context-tests/docker/Dockerfile @@ -1,4 +1,4 @@ -FROM alpine:3 +FROM alpine:3.23 COPY entrypoint.sh /entrypoint.sh From 47ee45412a969de0144794cdc8265f4da09c0325 Mon Sep 17 00:00:00 2001 From: Renovate Bot Date: Fri, 22 May 2026 07:10:19 +0000 Subject: [PATCH 17/18] fix(deps): update module github.com/opencontainers/selinux to v1.15.0 (#990) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This PR contains the following updates: | Package | Change | [Age](https://docs.renovatebot.com/merge-confidence/) | [Confidence](https://docs.renovatebot.com/merge-confidence/) | |---|---|---|---| | [github.com/opencontainers/selinux](https://github.com/opencontainers/selinux) | `v1.14.1` → `v1.15.0` | ![age](https://developer.mend.io/api/mc/badges/age/go/github.com%2fopencontainers%2fselinux/v1.15.0?slim=true) | ![confidence](https://developer.mend.io/api/mc/badges/confidence/go/github.com%2fopencontainers%2fselinux/v1.14.1/v1.15.0?slim=true) | --- ### Release Notes
opencontainers/selinux (github.com/opencontainers/selinux) ### [`v1.15.0`](https://github.com/opencontainers/selinux/releases/tag/v1.15.0) [Compare Source](https://github.com/opencontainers/selinux/compare/v1.14.1...v1.15.0) This release adds a new function, SetProcessKind, which is to be used instead of KVMProcessLabel\[s] and InitProcessLabel\[s] in case the user only wants to change the type of the existing label, not generate a new one. It also fixes an CI issue and optimizes label.InitLabels for a few common cases. #### What's Changed - ci: set timeout for vm jobs by [@​kolyshkin](https://github.com/kolyshkin) in [#​270](https://github.com/opencontainers/selinux/pull/270) - label.InitLabels: optimize by [@​kolyshkin](https://github.com/kolyshkin) in [#​269](https://github.com/opencontainers/selinux/pull/269) - Add SetProcessKind by [@​kolyshkin](https://github.com/kolyshkin) in [#​271](https://github.com/opencontainers/selinux/pull/271) **Full Changelog**:
--- ### Configuration 📅 **Schedule**: (UTC) - Branch creation - At any time (no schedule defined) - Automerge - At any time (no schedule defined) 🚦 **Automerge**: Disabled by config. Please merge this manually once you are satisfied. ♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the rebase/retry checkbox. 🔕 **Ignore**: Close this PR and you won't be reminded about this update again. --- - [ ] If you want to rebase/retry this PR, check this box --- This PR has been generated by [Mend Renovate](https://github.com/renovatebot/renovate). Reviewed-on: https://gitea.com/gitea/runner/pulls/990 Reviewed-by: Lunny Xiao Co-authored-by: Renovate Bot Co-committed-by: Renovate Bot --- go.mod | 2 +- go.sum | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/go.mod b/go.mod index 26bff317..3cb863a1 100644 --- a/go.mod +++ b/go.mod @@ -26,7 +26,7 @@ require ( github.com/moby/moby/client v0.4.1 github.com/moby/patternmatcher v0.6.1 github.com/opencontainers/image-spec v1.1.1 - github.com/opencontainers/selinux v1.14.1 + github.com/opencontainers/selinux v1.15.0 github.com/pkg/errors v0.9.1 github.com/prometheus/client_golang v1.23.2 github.com/rhysd/actionlint v1.7.12 diff --git a/go.sum b/go.sum index bf724ba9..6c013146 100644 --- a/go.sum +++ b/go.sum @@ -151,6 +151,8 @@ github.com/opencontainers/image-spec v1.1.1 h1:y0fUlFfIZhPF1W537XOLg0/fcx6zcHCJw github.com/opencontainers/image-spec v1.1.1/go.mod h1:qpqAh3Dmcf36wStyyWU+kCeDgrGnAve2nCC8+7h8Q0M= github.com/opencontainers/selinux v1.14.1 h1:a7XlXV/nN/l5zFP1FWZYoExpClu1QOPMfWUV2CZ8kEQ= github.com/opencontainers/selinux v1.14.1/go.mod h1:LenyElirjUHszfxrjuFqC85HIeXZKumHcKMQtnaDlQQ= +github.com/opencontainers/selinux v1.15.0 h1:4Gs40e/R2FvM8PC1HPaPncLLaDor8Y2WDfk5gjU9o5M= +github.com/opencontainers/selinux v1.15.0/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= From 273f6b4247862aabfdbf3c9f8c87d2717b7c2944 Mon Sep 17 00:00:00 2001 From: Nicolas Date: Sat, 23 May 2026 17:28:44 +0000 Subject: [PATCH 18/18] fix(reporter): respect configured log level for job log forwarding (#989) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary - Non-raw_output log entries above the globally configured `log.level` are no longer forwarded to the Gitea job log output - Step output (`raw_output=true`) is always forwarded regardless of level — it is actual job stdout/stderr, not runner internals - State-machine fields (`stepResult`, `jobResult`) are always processed regardless of level, preserving correct tracking for skipped steps (whose `stepResult` is emitted at `DebugLevel` in `step.go`) - Extracts a `shouldAppendLogRow` helper to avoid repeating the combined `!duringSteps() && entry.Level <= log.GetLevel()` guard in three places ## Why not the approach in #677 PR #677 adds `if entry.Level != log.GetLevel() { return nil }` at the top of `Fire()`. That has two bugs: 1. Uses `!=` instead of `>`, so `Error`/`Fatal` entries are dropped when the configured level is `Warn` 2. Returns early before processing `stepResult`/`jobResult` state fields — skipped steps (whose `stepResult` is logged at `DebugLevel`) would never be marked complete This fix instead applies the level guard only at the `r.logRows` append sites, leaving state tracking unconditional. Relates to #409. Reviewed-on: https://gitea.com/gitea/runner/pulls/989 Reviewed-by: Lunny Xiao --- internal/pkg/report/reporter.go | 13 +++++-- internal/pkg/report/reporter_test.go | 53 ++++++++++++++++++++++++++++ 2 files changed, 63 insertions(+), 3 deletions(-) diff --git a/internal/pkg/report/reporter.go b/internal/pkg/report/reporter.go index d86f03a6..1a2767b4 100644 --- a/internal/pkg/report/reporter.go +++ b/internal/pkg/report/reporter.go @@ -205,7 +205,7 @@ func (r *Reporter) Fire(entry *log.Entry) error { urgentState = true } } - if !r.duringSteps() { + if r.shouldAppendLogRow(entry) { r.logRows = appendIfNotNil(r.logRows, r.parseLogRow(entry)) } r.unlockAndNotify(urgentState) @@ -219,7 +219,7 @@ func (r *Reporter) Fire(entry *log.Entry) error { } } if step == nil { - if !r.duringSteps() { + if r.shouldAppendLogRow(entry) { r.logRows = appendIfNotNil(r.logRows, r.parseLogRow(entry)) } r.unlockAndNotify(false) @@ -246,7 +246,7 @@ func (r *Reporter) Fire(entry *log.Entry) error { r.logRows = append(r.logRows, row) } } - } else if !r.duringSteps() { + } else if r.shouldAppendLogRow(entry) { r.logRows = appendIfNotNil(r.logRows, r.parseLogRow(entry)) } if v, ok := entry.Data["stepResult"]; ok && isJobStepEntry(entry) { @@ -576,6 +576,13 @@ func (r *Reporter) duringSteps() bool { return true } +// shouldAppendLogRow reports whether a non-raw_output entry should be written +// to the job log: only when we are between steps and the entry's level is +// within the globally configured log level. +func (r *Reporter) shouldAppendLogRow(entry *log.Entry) bool { + return !r.duringSteps() && entry.Level <= log.GetLevel() +} + var stringToResult = map[string]runnerv1.Result{ "success": runnerv1.Result_RESULT_SUCCESS, "failure": runnerv1.Result_RESULT_FAILURE, diff --git a/internal/pkg/report/reporter_test.go b/internal/pkg/report/reporter_test.go index f12ee1b4..b0c0280d 100644 --- a/internal/pkg/report/reporter_test.go +++ b/internal/pkg/report/reporter_test.go @@ -219,6 +219,59 @@ func TestReporter_Fire(t *testing.T) { }) } +func TestReporter_LogLevelFiltering(t *testing.T) { + // Set global level to Info so Debug entries should be filtered. + origLevel := log.GetLevel() + log.SetLevel(log.InfoLevel) + defer log.SetLevel(origLevel) + + client := mocks.NewClient(t) + client.On("UpdateLog", mock.Anything, mock.Anything).Return(func(_ context.Context, req *connect_go.Request[runnerv1.UpdateLogRequest]) (*connect_go.Response[runnerv1.UpdateLogResponse], error) { + return connect_go.NewResponse(&runnerv1.UpdateLogResponse{ + AckIndex: req.Msg.Index + int64(len(req.Msg.Rows)), + }), nil + }) + client.On("UpdateTask", mock.Anything, mock.Anything).Return(func(_ context.Context, req *connect_go.Request[runnerv1.UpdateTaskRequest]) (*connect_go.Response[runnerv1.UpdateTaskResponse], error) { + return connect_go.NewResponse(&runnerv1.UpdateTaskResponse{}), nil + }) + + ctx, cancel := context.WithCancel(context.Background()) + taskCtx, err := structpb.NewStruct(map[string]any{}) + require.NoError(t, err) + cfg, _ := config.LoadDefault("") + reporter := NewReporter(ctx, cancel, client, &runnerv1.Task{Context: taskCtx}, cfg) + reporter.RunDaemon() + defer func() { + require.NoError(t, reporter.Close("")) + }() + reporter.ResetSteps(2) + + dataStep0 := log.Fields{"stage": "Main", "stepNumber": 0, "raw_output": true} + dataStep0Internal := log.Fields{"stage": "Main", "stepNumber": 0} + + // raw_output entries always appear in job log regardless of level. + require.NoError(t, reporter.Fire(&log.Entry{Message: "step output", Data: dataStep0, Level: log.InfoLevel})) + require.NoError(t, reporter.Fire(&log.Entry{Message: "step debug output", Data: dataStep0, Level: log.DebugLevel})) + assert.Equal(t, int64(2), reporter.state.Steps[0].LogLength, "raw_output entries must always be forwarded") + + // Non-raw_output entries during steps are not added to logRows regardless of level. + require.NoError(t, reporter.Fire(&log.Entry{Message: "internal info", Data: dataStep0Internal, Level: log.InfoLevel})) + require.NoError(t, reporter.Fire(&log.Entry{Message: "internal debug", Data: dataStep0Internal, Level: log.DebugLevel})) + + // stepResult at DebugLevel (skipped step) must still update state even when filtered from log. + require.NoError(t, reporter.Fire(&log.Entry{ + Message: "Skipping step", + Data: log.Fields{ + "stage": "Main", + "stepNumber": 1, + "stepResult": "skipped", + }, + Level: log.DebugLevel, + })) + assert.Equal(t, runnerv1.Result_RESULT_SKIPPED, reporter.state.Steps[1].Result, + "stepResult at DebugLevel must update step state even when log entry is filtered from job log output") +} + // TestReporter_EphemeralRunnerDeletion reproduces the exact scenario from // https://gitea.com/gitea/runner/issues/793: //