Compare commits

..

6 Commits

Author SHA1 Message Date
bircni
4034634340 fix 2026-07-03 22:09:38 +02:00
bircni
4691a92ea8 fix: checkout pull_request_target head commit
For pull_request_target tasks, use the pull request head SHA from the event payload as the default checkout ref so actions/checkout resolves the submitted PR content instead of the base branch tip.

Fixes: https://gitea.com/gitea/runner/issues/563

Co-Authored-By: GPT-5 Codex <codex@openai.com>
2026-07-03 21:58:37 +02:00
silverwind
0ee4643d4a fix: install nftables in dind images to silence nft cleanup errors (#1064)
Fixes: https://gitea.com/gitea/runner/issues/1061
Related: https://github.com/moby/moby/pull/52886

Docker 29.6.0 changed `dockerd` to shell out to the external `nft` binary for firewall-rule cleanup instead of linking `libnftables` (release note: *"Mitigate a crash in libnftables when using nftables as the firewall backend by changing the default build option to execute the `nft` command instead."*). The upstream `docker:dind` image only ships `iptables`/`ip6tables`, so `dockerd` logs `failed to find nft tool` on startup and shutdown.

Networking is unaffected (default firewall backend is still iptables), but the errors are noisy. Install `nftables` in both dind variants to provide the binary.

*PR authored by Claude (Opus 4.8).*Reviewed-on: https://gitea.com/gitea/runner/pulls/1064
Reviewed-by: Lunny Xiao <xiaolunwen@gmail.com>
Co-authored-by: silverwind <me@silverwind.io>
2026-07-03 08:23:12 +00:00
Renovate Bot
e774003c18 chore(deps): update docker docker tag to v29.6.1 (#1063)
This PR contains the following updates:

| Package | Type | Update | Change |
|---|---|---|---|
| docker | stage | patch | `29.6.0-dind-rootless` → `29.6.1-dind-rootless` |
| docker | stage | patch | `29.6.0-dind` → `29.6.1-dind` |

Co-authored-by: Renovate Bot <renovate-bot@gitea.com>
2026-07-02 08:22:37 +00:00
Nicolas
eeb479ea89 fix: prevent exponential growth of RunContext masks in composite actions (#1059)
A composite action's `RunContext` is **seeded with its parent's `Masks` slice** in `newCompositeRunContext`. After the composite finished running, the *entire* `compositeRC.Masks` (the seeded parent masks plus any added during the run) was appended straight back into the parent:

```go
rc.Masks = append(rc.Masks, compositeRC.Masks...)
```

Because the composite already contained the parent's masks, every composite action roughly **doubled** the parent's `Masks` length. With many nested/repeated composite actions this grows exponentially:

```
before=1152 after=2304 compositeLen=1152
before=2304 after=4608 compositeLen=2304
before=4608 after=9216 compositeLen=4608
...
```

### Impact

The bloated `Masks` slice made per-log-line secret redaction (`strings.makeGenericReplacer`, rebuilt per line since #1001) extremely slow. Log writes fell behind enough to keep subprocess pipes open after the process had already exited, surfacing as seemingly random CI failures — most often in later steps of mask-heavy workflows:

```
Error: exec: WaitDelay expired before I/O complete
```

The split of `cmd.Run()` into `cmd.Start()` + `cmd.Wait()` (#883) is what turned this long-standing latency into a hard failure.

### Fix

Only append masks that aren't already present in the parent, via a small `appendUniqueMasks` helper. This keeps the mask count bounded by the number of *distinct* secrets instead of growing exponentially.

I kept `Masks` as a `[]string` rather than switching to a set type, since it's passed by pointer to loggers across `logger.go`, `run_context.go`, and `runner.go` — a type change would be a much wider, riskier refactor. The `O(n·m)` dedup is harmless precisely because `n` now stays small.

Fixes #1054Reviewed-on: https://gitea.com/gitea/runner/pulls/1059
Reviewed-by: Lunny Xiao <xiaolunwen@gmail.com>
2026-07-01 18:50:34 +00:00
Renovate Bot
eba33e178d fix(deps): update module github.com/docker/cli to v29.6.1+incompatible (#1060)
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.6.0+incompatible` → `v29.6.1+incompatible` | ![age](https://developer.mend.io/api/mc/badges/age/go/github.com%2fdocker%2fcli/v29.6.1+incompatible?slim=true) | ![confidence](https://developer.mend.io/api/mc/badges/confidence/go/github.com%2fdocker%2fcli/v29.6.0+incompatible/v29.6.1+incompatible?slim=true) |

---

### Release Notes

<details>
<summary>docker/cli (github.com/docker/cli)</summary>

### [`v29.6.1+incompatible`](https://github.com/docker/cli/compare/v29.6.0...v29.6.1)

[Compare Source](https://github.com/docker/cli/compare/v29.6.0...v29.6.1)

</details>

---

### 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.

---

 - [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check this box

---

This PR has been generated by [Mend Renovate](https://github.com/renovatebot/renovate).
<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiI0My4xOTEuMiIsInVwZGF0ZWRJblZlciI6IjQzLjE5MS4yIiwidGFyZ2V0QnJhbmNoIjoibWFpbiIsImxhYmVscyI6W119-->

---------

Co-authored-by: Lunny Xiao <xiaolunwen@gmail.com>
Reviewed-on: https://gitea.com/gitea/runner/pulls/1060
Reviewed-by: Lunny Xiao <xiaolunwen@gmail.com>
Co-authored-by: Renovate Bot <renovate-bot@gitea.com>
2026-07-01 18:44:29 +00:00
7 changed files with 194 additions and 6 deletions

View File

@@ -17,14 +17,14 @@ RUN make clean && make build
### DIND VARIANT ### DIND VARIANT
# #
# #
FROM docker:29.6.0-dind AS dind FROM docker:29.6.1-dind AS dind
ARG VERSION=dev ARG VERSION=dev
LABEL org.opencontainers.image.source="https://gitea.com/gitea/runner" LABEL org.opencontainers.image.source="https://gitea.com/gitea/runner"
LABEL org.opencontainers.image.version="${VERSION}" LABEL org.opencontainers.image.version="${VERSION}"
RUN apk add --no-cache s6 bash git tzdata RUN apk add --no-cache s6 bash git tzdata nftables
COPY --from=builder /opt/src/runner/gitea-runner /usr/local/bin/gitea-runner COPY --from=builder /opt/src/runner/gitea-runner /usr/local/bin/gitea-runner
COPY scripts/run.sh /usr/local/bin/run.sh COPY scripts/run.sh /usr/local/bin/run.sh
@@ -37,7 +37,7 @@ ENTRYPOINT ["s6-svscan","/etc/s6"]
### DIND-ROOTLESS VARIANT ### DIND-ROOTLESS VARIANT
# #
# #
FROM docker:29.6.0-dind-rootless AS dind-rootless FROM docker:29.6.1-dind-rootless AS dind-rootless
ARG VERSION=dev ARG VERSION=dev
@@ -45,7 +45,7 @@ LABEL org.opencontainers.image.source="https://gitea.com/gitea/runner"
LABEL org.opencontainers.image.version="${VERSION}" LABEL org.opencontainers.image.version="${VERSION}"
USER root USER root
RUN apk add --no-cache s6 bash git tzdata RUN apk add --no-cache s6 bash git tzdata nftables
COPY --from=builder /opt/src/runner/gitea-runner /usr/local/bin/gitea-runner COPY --from=builder /opt/src/runner/gitea-runner /usr/local/bin/gitea-runner
COPY scripts/run.sh /usr/local/bin/run.sh COPY scripts/run.sh /usr/local/bin/run.sh

View File

@@ -8,6 +8,7 @@ import (
"context" "context"
"errors" "errors"
"regexp" "regexp"
"slices"
"strconv" "strconv"
"strings" "strings"
@@ -85,6 +86,19 @@ func newCompositeRunContext(ctx context.Context, parent *RunContext, step action
return compositerc return compositerc
} }
// appendUniqueMasks appends the masks from src to dst, skipping any mask that
// is already present in dst. This prevents the parent RunContext's Masks slice
// from growing exponentially when composite actions are nested or repeated,
// since each composite RunContext is seeded with its parent's masks.
func appendUniqueMasks(dst, src []string) []string {
for _, m := range src {
if !slices.Contains(dst, m) {
dst = append(dst, m)
}
}
return dst
}
func execAsComposite(step actionStep) common.Executor { func execAsComposite(step actionStep) common.Executor {
rc := step.getRunContext() rc := step.getRunContext()
action := step.getActionModel() action := step.getActionModel()
@@ -110,7 +124,11 @@ func execAsComposite(step actionStep) common.Executor {
}, eval.Interpolate(ctx, output.Value)) }, eval.Interpolate(ctx, output.Value))
} }
rc.Masks = append(rc.Masks, compositeRC.Masks...) // compositeRC.Masks is seeded with rc.Masks (see newCompositeRunContext)
// and may have additional masks appended while the composite action runs.
// Only append masks that are not already present, otherwise nested or
// repeated composite actions grow rc.Masks exponentially.
rc.Masks = appendUniqueMasks(rc.Masks, compositeRC.Masks)
rc.ExtraPath = compositeRC.ExtraPath rc.ExtraPath = compositeRC.ExtraPath
// compositeRC.Env is dirty, contains INPUT_ and merged step env, only rely on compositeRC.GlobalEnv // compositeRC.Env is dirty, contains INPUT_ and merged step env, only rely on compositeRC.GlobalEnv
mergeIntoMap := mergeIntoMapCaseSensitive mergeIntoMap := mergeIntoMapCaseSensitive

View File

@@ -0,0 +1,70 @@
// Copyright 2026 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package runner
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestAppendUniqueMasks(t *testing.T) {
tests := []struct {
name string
dst []string
src []string
want []string
}{
{
name: "appends new masks",
dst: []string{"a"},
src: []string{"b", "c"},
want: []string{"a", "b", "c"},
},
{
name: "skips masks already present",
dst: []string{"a", "b"},
src: []string{"a", "b"},
want: []string{"a", "b"},
},
{
name: "deduplicates within src",
dst: []string{"a"},
src: []string{"b", "b", "a"},
want: []string{"a", "b"},
},
{
name: "empty src leaves dst unchanged",
dst: []string{"a"},
src: nil,
want: []string{"a"},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
assert.Equal(t, tt.want, appendUniqueMasks(tt.dst, tt.src))
})
}
}
// TestAppendUniqueMasksNoExponentialGrowth reproduces the exponential growth of
// the parent's Masks slice observed with nested/repeated composite actions. A
// composite RunContext is seeded with its parent's masks and the whole seeded
// slice was previously appended back into the parent, doubling its length on
// every composite action.
func TestAppendUniqueMasksNoExponentialGrowth(t *testing.T) {
parentMasks := []string{"secret"}
for range 20 {
// compositeRC.Masks starts as a copy of the parent's masks (it is
// seeded with parent.Masks in newCompositeRunContext).
compositeMasks := make([]string, len(parentMasks))
copy(compositeMasks, parentMasks)
parentMasks = appendUniqueMasks(parentMasks, compositeMasks)
}
assert.Equal(t, []string{"secret"}, parentMasks)
}

2
go.mod
View File

@@ -11,7 +11,7 @@ require (
github.com/containerd/errdefs v1.0.0 github.com/containerd/errdefs v1.0.0
github.com/creack/pty v1.1.24 github.com/creack/pty v1.1.24
github.com/distribution/reference v0.6.0 github.com/distribution/reference v0.6.0
github.com/docker/cli v29.6.0+incompatible github.com/docker/cli v29.6.1+incompatible
github.com/docker/go-connections v0.7.0 github.com/docker/go-connections v0.7.0
github.com/go-git/go-billy/v5 v5.9.0 github.com/go-git/go-billy/v5 v5.9.0
github.com/go-git/go-git/v5 v5.19.1 github.com/go-git/go-git/v5 v5.19.1

2
go.sum
View File

@@ -51,6 +51,8 @@ github.com/docker/cli v29.5.3+incompatible h1:nbEFfz774vBwQ5KRYv7c/AghjReqnGISvr
github.com/docker/cli v29.5.3+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8= github.com/docker/cli v29.5.3+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8=
github.com/docker/cli v29.6.0+incompatible h1:nw9himxMMZ7eIeherJNlKQq+acnlzGgHd+4uf10QRSc= github.com/docker/cli v29.6.0+incompatible h1:nw9himxMMZ7eIeherJNlKQq+acnlzGgHd+4uf10QRSc=
github.com/docker/cli v29.6.0+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8= github.com/docker/cli v29.6.0+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8=
github.com/docker/cli v29.6.1+incompatible h1:oO7F4nn3Ovr/5TlfTUWFbMwBSS/B7Xs6Epv26gBrUP8=
github.com/docker/cli v29.6.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 h1:cT2PbRPSlnMmNTfT2TDMXRyQ1KMWHG7xoTLBcn1ZNv0=
github.com/docker/docker-credential-helpers v0.9.6/go.mod h1:v1S+hepowrQXITkEfw6o4+BMbGot02wiKpzWhGUZK6c= 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= github.com/docker/go-connections v0.7.0 h1:6SsRfJddP22WMrCkj19x9WKjEDTB+ahsdiGYf0mN39c=

View File

@@ -366,6 +366,7 @@ func (r *Runner) run(ctx context.Context, task *runnerv1.Task, reporter *report.
} else if t := task.Secrets["GITHUB_TOKEN"]; t != "" { } else if t := task.Secrets["GITHUB_TOKEN"]; t != "" {
preset.Token = t preset.Token = t
} }
applyPullRequestTargetCheckoutContext(preset)
if actionsIDTokenRequestURL := taskContext["actions_id_token_request_url"].GetStringValue(); actionsIDTokenRequestURL != "" { if actionsIDTokenRequestURL := taskContext["actions_id_token_request_url"].GetStringValue(); actionsIDTokenRequestURL != "" {
envs["ACTIONS_ID_TOKEN_REQUEST_URL"] = actionsIDTokenRequestURL envs["ACTIONS_ID_TOKEN_REQUEST_URL"] = actionsIDTokenRequestURL
@@ -574,6 +575,41 @@ func postInternalCache(url, secret string, body map[string]string) error {
return nil return nil
} }
func applyPullRequestTargetCheckoutContext(preset *model.GithubContext) {
if preset == nil || preset.EventName != "pull_request_target" {
return
}
headSHA := nestedString(preset.Event, "pull_request", "head", "sha")
if headSHA == "" {
return
}
preset.Sha = headSHA
preset.Ref = headSHA
if headRef := nestedString(preset.Event, "pull_request", "head", "ref"); headRef != "" {
preset.HeadRef = headRef
preset.RefName = headRef
}
}
func nestedString(m map[string]any, keys ...string) string {
var current any = m
for _, key := range keys {
next, ok := current.(map[string]any)
if !ok {
return ""
}
current, ok = next[key]
if !ok {
return ""
}
}
value := current.(string)
if value == "" {
return ""
}
return value
}
func (r *Runner) RunningCount() int64 { func (r *Runner) RunningCount() int64 {
return r.runningCount.Load() return r.runningCount.Load()
} }

View File

@@ -7,6 +7,7 @@ import (
"context" "context"
"testing" "testing"
"gitea.com/gitea/runner/act/model"
clientmocks "gitea.com/gitea/runner/internal/pkg/client/mocks" clientmocks "gitea.com/gitea/runner/internal/pkg/client/mocks"
"gitea.com/gitea/runner/internal/pkg/config" "gitea.com/gitea/runner/internal/pkg/config"
"gitea.com/gitea/runner/internal/pkg/ver" "gitea.com/gitea/runner/internal/pkg/ver"
@@ -92,6 +93,67 @@ func TestNewRunnerInitializesLabelsAndEnvironment(t *testing.T) {
require.Nil(t, r.cacheHandler) require.Nil(t, r.cacheHandler)
} }
func TestApplyPullRequestTargetCheckoutContextUsesHeadSHA(t *testing.T) {
preset := &model.GithubContext{
EventName: "pull_request_target",
Sha: "base-sha",
Ref: "refs/heads/main",
RefName: "main",
HeadRef: "feature",
Event: map[string]any{
"pull_request": map[string]any{
"head": map[string]any{
"sha": "head-sha",
"ref": "contributor-branch",
},
},
},
}
applyPullRequestTargetCheckoutContext(preset)
require.Equal(t, "head-sha", preset.Sha)
require.Equal(t, "head-sha", preset.Ref)
require.Equal(t, "contributor-branch", preset.RefName)
require.Equal(t, "contributor-branch", preset.HeadRef)
}
func TestApplyPullRequestTargetCheckoutContextNoOpsWithoutHeadSHA(t *testing.T) {
preset := &model.GithubContext{
EventName: "pull_request_target",
Sha: "base-sha",
Ref: "refs/heads/main",
RefName: "main",
Event: map[string]any{},
}
applyPullRequestTargetCheckoutContext(preset)
require.Equal(t, "base-sha", preset.Sha)
require.Equal(t, "refs/heads/main", preset.Ref)
require.Equal(t, "main", preset.RefName)
}
func TestApplyPullRequestTargetCheckoutContextNoOpsForOtherEvents(t *testing.T) {
preset := &model.GithubContext{
EventName: "pull_request",
Sha: "merge-sha",
Ref: "refs/pull/1/merge",
Event: map[string]any{
"pull_request": map[string]any{
"head": map[string]any{
"sha": "head-sha",
},
},
},
}
applyPullRequestTargetCheckoutContext(preset)
require.Equal(t, "merge-sha", preset.Sha)
require.Equal(t, "refs/pull/1/merge", preset.Ref)
}
func taskWithDefaultActionsURL(url string) *runnerv1.Task { func taskWithDefaultActionsURL(url string) *runnerv1.Task {
return &runnerv1.Task{ return &runnerv1.Task{
Context: &structpb.Struct{ Context: &structpb.Struct{