mirror of
https://gitea.com/gitea/act_runner.git
synced 2026-06-21 17:24:23 +02:00
Compare commits
3 Commits
f23605c614
...
3c5f03ff8f
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3c5f03ff8f | ||
|
|
880e9755d9 | ||
|
|
8d7cf48a6f |
@@ -47,6 +47,7 @@ type NewContainerInput struct {
|
||||
// Gitea specific
|
||||
AutoRemove bool
|
||||
ValidVolumes []string
|
||||
AllocatePTY bool // allocate a pseudo-TTY for the container's exec processes
|
||||
}
|
||||
|
||||
// FileEntry is a file to copy to a container
|
||||
|
||||
@@ -41,7 +41,6 @@ import (
|
||||
"github.com/moby/moby/client"
|
||||
specs "github.com/opencontainers/image-spec/specs-go/v1"
|
||||
"github.com/spf13/pflag"
|
||||
"golang.org/x/term"
|
||||
)
|
||||
|
||||
// NewContainer creates a reference to a container
|
||||
@@ -450,7 +449,6 @@ func (cr *containerReference) create(capAdd, capDrop []string) common.Executor {
|
||||
return nil
|
||||
}
|
||||
logger := common.Logger(ctx)
|
||||
isTerminal := term.IsTerminal(int(os.Stdout.Fd()))
|
||||
input := cr.input
|
||||
exposedPorts, err := convertPortSet(input.ExposedPorts)
|
||||
if err != nil {
|
||||
@@ -466,7 +464,7 @@ func (cr *containerReference) create(capAdd, capDrop []string) common.Executor {
|
||||
WorkingDir: input.WorkingDir,
|
||||
Env: input.Env,
|
||||
ExposedPorts: exposedPorts,
|
||||
Tty: isTerminal,
|
||||
Tty: input.AllocatePTY,
|
||||
}
|
||||
// For Gitea, reduce log noise
|
||||
// logger.Debugf("Common container.Config ==> %+v", config)
|
||||
@@ -604,7 +602,7 @@ func (cr *containerReference) exec(cmd []string, env map[string]string, user, wo
|
||||
}
|
||||
|
||||
logger.Debugf("Exec command '%s'", cmd)
|
||||
isTerminal := term.IsTerminal(int(os.Stdout.Fd()))
|
||||
isTerminal := cr.input.AllocatePTY
|
||||
envList := make([]string, 0)
|
||||
for k, v := range env {
|
||||
envList = append(envList, fmt.Sprintf("%s=%s", k, v))
|
||||
@@ -899,7 +897,7 @@ func (cr *containerReference) attach() common.Executor {
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to attach to container: %w", err)
|
||||
}
|
||||
isTerminal := term.IsTerminal(int(os.Stdout.Fd()))
|
||||
isTerminal := cr.input.AllocatePTY
|
||||
|
||||
var outWriter io.Writer
|
||||
outWriter = cr.input.Stdout
|
||||
|
||||
@@ -19,6 +19,7 @@ import (
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"gitea.com/gitea/runner/act/common"
|
||||
@@ -42,6 +43,7 @@ type HostEnvironment struct {
|
||||
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{}
|
||||
@@ -200,12 +202,12 @@ func (e *HostEnvironment) Start(_ bool) common.Executor {
|
||||
|
||||
type ptyWriter struct {
|
||||
Out io.Writer
|
||||
AutoStop bool
|
||||
AutoStop atomic.Bool
|
||||
dirtyLine bool
|
||||
}
|
||||
|
||||
func (w *ptyWriter) Write(buf []byte) (int, error) {
|
||||
if w.AutoStop && len(buf) > 0 && buf[len(buf)-1] == 4 {
|
||||
if w.AutoStop.Load() && len(buf) > 0 && buf[len(buf)-1] == 4 {
|
||||
n, err := w.Out.Write(buf[:len(buf)-1])
|
||||
if err != nil {
|
||||
return n, err
|
||||
@@ -335,21 +337,20 @@ func (e *HostEnvironment) exec(ctx context.Context, command []string, cmdline st
|
||||
tty.Close()
|
||||
}
|
||||
}()
|
||||
if true /* allocate Terminal */ {
|
||||
if e.AllocatePTY {
|
||||
var err error
|
||||
ppty, tty, err = setupPty(cmd, cmdline)
|
||||
if err != nil {
|
||||
common.Logger(ctx).Debugf("Failed to setup Pty %v\n", err.Error())
|
||||
}
|
||||
}
|
||||
writer := &ptyWriter{Out: e.StdOut}
|
||||
logctx, finishLog := context.WithCancel(context.Background())
|
||||
var writer *ptyWriter
|
||||
var logctx context.Context
|
||||
if ppty != nil {
|
||||
writer = &ptyWriter{Out: e.StdOut}
|
||||
var finishLog context.CancelFunc
|
||||
logctx, finishLog = context.WithCancel(context.Background())
|
||||
go copyPtyOutput(writer, ppty, finishLog)
|
||||
} else {
|
||||
finishLog()
|
||||
}
|
||||
if ppty != nil {
|
||||
go writeKeepAlive(ppty)
|
||||
}
|
||||
// Split Start/Wait so the PID can be registered before the process can exit;
|
||||
@@ -379,14 +380,11 @@ func (e *HostEnvironment) exec(ctx context.Context, command []string, cmdline st
|
||||
return err
|
||||
}
|
||||
if tty != nil {
|
||||
writer.AutoStop = true
|
||||
writer.AutoStop.Store(true)
|
||||
if _, err := tty.WriteString("\x04"); err != nil {
|
||||
common.Logger(ctx).Debug("Failed to write EOT")
|
||||
}
|
||||
}
|
||||
<-logctx.Done()
|
||||
|
||||
if ppty != nil {
|
||||
<-logctx.Done()
|
||||
ppty.Close()
|
||||
ppty = nil
|
||||
}
|
||||
|
||||
@@ -6,12 +6,14 @@ package container
|
||||
|
||||
import (
|
||||
"archive/tar"
|
||||
"bytes"
|
||||
"context"
|
||||
"io"
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"gitea.com/gitea/runner/act/common"
|
||||
@@ -100,6 +102,45 @@ func TestHostEnvironmentExecExitCode(t *testing.T) {
|
||||
assert.Equal(t, "Process completed with exit code 3.", err.Error())
|
||||
}
|
||||
|
||||
func TestHostEnvironmentAllocatePTY(t *testing.T) {
|
||||
if runtime.GOOS == "windows" {
|
||||
t.Skip("uses POSIX shell")
|
||||
}
|
||||
for _, tc := range []struct {
|
||||
name string
|
||||
allocPTY bool
|
||||
expect string
|
||||
}{
|
||||
{name: "off", allocPTY: false, expect: "NOTTY"},
|
||||
{name: "on", allocPTY: true, expect: "TTY"},
|
||||
} {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
buf := &bytes.Buffer{}
|
||||
e := &HostEnvironment{
|
||||
Path: filepath.Join(dir, "path"),
|
||||
TmpDir: filepath.Join(dir, "tmp"),
|
||||
ToolCache: filepath.Join(dir, "tool_cache"),
|
||||
ActPath: filepath.Join(dir, "act_path"),
|
||||
StdOut: buf,
|
||||
Workdir: filepath.Join(dir, "path"),
|
||||
AllocatePTY: tc.allocPTY,
|
||||
}
|
||||
for _, p := range []string{e.Path, e.TmpDir, e.ToolCache, e.ActPath} {
|
||||
require.NoError(t, os.MkdirAll(p, 0o700))
|
||||
}
|
||||
|
||||
err := e.Exec(
|
||||
[]string{"sh", "-c", "[ -t 1 ] && printf TTY || printf NOTTY"},
|
||||
map[string]string{"PATH": os.Getenv("PATH")}, "", "",
|
||||
)(context.Background())
|
||||
require.NoError(t, err)
|
||||
got := strings.TrimSpace(strings.ReplaceAll(buf.String(), "\r", ""))
|
||||
assert.Equal(t, tc.expect, got)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestHostEnvironmentRemoveCleansWorkdir(t *testing.T) {
|
||||
logger := logrus.New()
|
||||
ctx := common.WithLogger(context.Background(), logrus.NewEntry(logger))
|
||||
|
||||
@@ -456,6 +456,7 @@ func newStepContainer(ctx context.Context, step step, image string, cmd, entrypo
|
||||
Options: rc.Config.ContainerOptions,
|
||||
AutoRemove: rc.Config.AutoRemove,
|
||||
ValidVolumes: rc.Config.ValidVolumes,
|
||||
AllocatePTY: rc.Config.AllocatePTY,
|
||||
})
|
||||
return stepContainer
|
||||
}
|
||||
|
||||
@@ -229,7 +229,8 @@ func (rc *RunContext) startHostEnvironment() common.Executor {
|
||||
CleanUp: func() {
|
||||
os.RemoveAll(miscpath)
|
||||
},
|
||||
StdOut: logWriter,
|
||||
StdOut: logWriter,
|
||||
AllocatePTY: rc.Config.AllocatePTY,
|
||||
}
|
||||
rc.cleanUpJobContainer = rc.JobContainer.Remove()
|
||||
for k, v := range rc.JobContainer.GetRunnerContext(ctx) {
|
||||
@@ -371,6 +372,7 @@ func (rc *RunContext) startJobContainer() common.Executor {
|
||||
NetworkAliases: []string{serviceID},
|
||||
ExposedPorts: exposedPorts,
|
||||
PortBindings: portBindings,
|
||||
AllocatePTY: rc.Config.AllocatePTY,
|
||||
})
|
||||
rc.ServiceContainers = append(rc.ServiceContainers, c)
|
||||
}
|
||||
@@ -431,6 +433,7 @@ func (rc *RunContext) startJobContainer() common.Executor {
|
||||
Options: rc.options(ctx),
|
||||
AutoRemove: rc.Config.AutoRemove,
|
||||
ValidVolumes: rc.Config.ValidVolumes,
|
||||
AllocatePTY: rc.Config.AllocatePTY,
|
||||
})
|
||||
if rc.JobContainer == nil {
|
||||
return errors.New("Failed to create job container")
|
||||
|
||||
@@ -79,6 +79,7 @@ type Config struct {
|
||||
ValidVolumes []string // only volumes (and bind mounts) in this slice can be mounted on the job container or service containers
|
||||
InsecureSkipTLS bool // whether to skip verifying TLS certificate of the Gitea instance
|
||||
MaxParallel int // max parallel jobs to run across all workflows (0 = no limit, uses CPU count)
|
||||
AllocatePTY bool // allocate a pseudo-TTY for each step's process
|
||||
}
|
||||
|
||||
// GetToken: Adapt to Gitea
|
||||
|
||||
@@ -139,6 +139,7 @@ func (sd *stepDocker) newStepContainer(ctx context.Context, image string, cmd, e
|
||||
Platform: rc.Config.ContainerArchitecture,
|
||||
AutoRemove: rc.Config.AutoRemove,
|
||||
ValidVolumes: rc.Config.ValidVolumes,
|
||||
AllocatePTY: rc.Config.AllocatePTY,
|
||||
})
|
||||
return stepContainer
|
||||
}
|
||||
|
||||
@@ -109,6 +109,55 @@ func TestStepDockerMain(t *testing.T) {
|
||||
cm.AssertExpectations(t)
|
||||
}
|
||||
|
||||
func TestStepDockerNewStepContainerAllocatePTY(t *testing.T) {
|
||||
for _, tc := range []struct {
|
||||
name string
|
||||
allocPTY bool
|
||||
}{
|
||||
{name: "off", allocPTY: false},
|
||||
{name: "on", allocPTY: true},
|
||||
} {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
cm := &containerMock{}
|
||||
|
||||
var captured *container.NewContainerInput
|
||||
origContainerNewContainer := ContainerNewContainer
|
||||
ContainerNewContainer = func(input *container.NewContainerInput) container.ExecutionsEnvironment {
|
||||
captured = input
|
||||
return cm
|
||||
}
|
||||
defer func() {
|
||||
ContainerNewContainer = origContainerNewContainer
|
||||
}()
|
||||
|
||||
ctx := context.Background()
|
||||
sd := &stepDocker{
|
||||
RunContext: &RunContext{
|
||||
StepResults: map[string]*model.StepResult{},
|
||||
Config: &Config{
|
||||
AllocatePTY: tc.allocPTY,
|
||||
PlatformPicker: func(_ []string) string {
|
||||
return "node:14"
|
||||
},
|
||||
},
|
||||
Run: &model.Run{
|
||||
JobID: "1",
|
||||
Workflow: &model.Workflow{
|
||||
Jobs: map[string]*model.Job{"1": {}},
|
||||
},
|
||||
},
|
||||
JobContainer: cm,
|
||||
},
|
||||
Step: &model.Step{ID: "1", Uses: "docker://node:14"},
|
||||
}
|
||||
sd.RunContext.ExprEval = sd.RunContext.NewExpressionEvaluator(ctx)
|
||||
|
||||
_ = sd.newStepContainer(ctx, "node:14", []string{"echo", "hi"}, nil)
|
||||
assert.Equal(t, tc.allocPTY, captured.AllocatePTY)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestStepDockerPrePost(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
sd := &stepDocker{}
|
||||
|
||||
@@ -9,9 +9,9 @@ inputs:
|
||||
runs:
|
||||
using: "composite"
|
||||
steps:
|
||||
- uses: actions/setup-node@v3
|
||||
- uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version: '16'
|
||||
node-version: '24'
|
||||
- run: |
|
||||
console.log(process.version);
|
||||
console.log("Hi from node");
|
||||
|
||||
2
go.mod
2
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.4.3+incompatible
|
||||
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
|
||||
|
||||
2
go.sum
2
go.sum
@@ -49,6 +49,8 @@ github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5Qvfr
|
||||
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/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=
|
||||
|
||||
@@ -347,6 +347,7 @@ func (r *Runner) run(ctx context.Context, task *runnerv1.Task, reporter *report.
|
||||
Workdir: workdir,
|
||||
BindWorkdir: r.cfg.Container.BindWorkdir,
|
||||
ActionCacheDir: filepath.FromSlash(r.cfg.Host.WorkdirParent),
|
||||
AllocatePTY: r.cfg.Runner.AllocatePTY,
|
||||
|
||||
ReuseContainers: false,
|
||||
ForcePull: r.cfg.Container.ForcePull,
|
||||
|
||||
@@ -74,6 +74,11 @@ runner:
|
||||
- "ubuntu-latest:docker://docker.gitea.com/runner-images:ubuntu-latest"
|
||||
- "ubuntu-24.04:docker://docker.gitea.com/runner-images:ubuntu-24.04"
|
||||
- "ubuntu-22.04:docker://docker.gitea.com/runner-images:ubuntu-22.04"
|
||||
# Allocate a pseudo-TTY for each step's process. Applies to both host and docker backends.
|
||||
# Default false matches GitHub actions/runner. Enable only for jobs that need an interactive
|
||||
# terminal; tools like `docker build` emit redrawing progress frames into the captured log
|
||||
# when a TTY is present.
|
||||
allocate_pty: false
|
||||
|
||||
cache:
|
||||
# Enable cache server to use actions/cache.
|
||||
|
||||
@@ -41,6 +41,7 @@ type Runner struct {
|
||||
StateReportInterval time.Duration `yaml:"state_report_interval"` // StateReportInterval specifies the interval for state reporting.
|
||||
Labels []string `yaml:"labels"` // Labels specify the labels of the runner. Labels are declared on each startup
|
||||
GithubMirror string `yaml:"github_mirror"` // GithubMirror defines what mirrors should be used when using github
|
||||
AllocatePTY bool `yaml:"allocate_pty"` // AllocatePTY allocates a pseudo-TTY for each step's process. Default is false, matching GitHub's actions/runner. Enable only for jobs that need an interactive terminal; tools like docker build emit redrawing progress frames into the captured log when a TTY is present. Applies to both host and docker backends.
|
||||
}
|
||||
|
||||
// Cache represents the configuration for caching.
|
||||
|
||||
Reference in New Issue
Block a user