mirror of
https://gitea.com/gitea/act_runner.git
synced 2026-05-08 16:23:23 +02:00
Compare commits
4 Commits
dff63b3ecc
...
v1.0.2
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3ea7d39690 | ||
|
|
861d351845 | ||
|
|
cce8543d06 | ||
|
|
75643645f0 |
@@ -38,9 +38,11 @@ var (
|
|||||||
ErrNoRepo = errors.New("unable to find git repo")
|
ErrNoRepo = errors.New("unable to find git repo")
|
||||||
)
|
)
|
||||||
|
|
||||||
// acquireCloneLock returns an unlock function after locking the per-directory mutex for dir.
|
// AcquireCloneLock returns an unlock function after locking the per-directory mutex for dir.
|
||||||
// Only concurrent operations targeting the same directory are erialized; clones into different directories run in parallel.
|
// Only concurrent operations targeting the same directory are serialized; clones into different directories run in parallel.
|
||||||
func acquireCloneLock(dir string) func() {
|
// Callers reading files inside dir (e.g. tarring a checked-out action into a job container) must hold this lock too,
|
||||||
|
// otherwise a concurrent NewGitCloneExecutor on the same dir can mutate the worktree mid-read.
|
||||||
|
func AcquireCloneLock(dir string) func() {
|
||||||
v, _ := cloneLocks.LoadOrStore(dir, &sync.Mutex{})
|
v, _ := cloneLocks.LoadOrStore(dir, &sync.Mutex{})
|
||||||
mu := v.(*sync.Mutex)
|
mu := v.(*sync.Mutex)
|
||||||
mu.Lock()
|
mu.Lock()
|
||||||
@@ -305,10 +307,10 @@ func gitOptions(token string) (fetchOptions git.FetchOptions, pullOptions git.Pu
|
|||||||
func NewGitCloneExecutor(input NewGitCloneExecutorInput) common.Executor {
|
func NewGitCloneExecutor(input NewGitCloneExecutorInput) common.Executor {
|
||||||
return func(ctx context.Context) error {
|
return func(ctx context.Context) error {
|
||||||
logger := common.Logger(ctx)
|
logger := common.Logger(ctx)
|
||||||
logger.Infof(" \u2601 git clone '%s' # ref=%s", input.URL, input.Ref)
|
logger.Infof("git clone '%s' # ref=%s", input.URL, input.Ref)
|
||||||
logger.Debugf(" cloning %s to %s", input.URL, input.Dir)
|
logger.Debugf(" cloning %s to %s", input.URL, input.Dir)
|
||||||
|
|
||||||
defer acquireCloneLock(input.Dir)()
|
defer AcquireCloneLock(input.Dir)()
|
||||||
|
|
||||||
refName := plumbing.ReferenceName("refs/heads/" + input.Ref)
|
refName := plumbing.ReferenceName("refs/heads/" + input.Ref)
|
||||||
r, err := CloneIfRequired(ctx, refName, input, logger)
|
r, err := CloneIfRequired(ctx, refName, input, logger)
|
||||||
|
|||||||
@@ -310,11 +310,11 @@ func TestAcquireCloneLock(t *testing.T) {
|
|||||||
t.Run("same directory serializes", func(t *testing.T) {
|
t.Run("same directory serializes", func(t *testing.T) {
|
||||||
dir := t.TempDir()
|
dir := t.TempDir()
|
||||||
|
|
||||||
unlock1 := acquireCloneLock(dir)
|
unlock1 := AcquireCloneLock(dir)
|
||||||
|
|
||||||
secondAcquired := make(chan struct{})
|
secondAcquired := make(chan struct{})
|
||||||
go func() {
|
go func() {
|
||||||
unlock := acquireCloneLock(dir)
|
unlock := AcquireCloneLock(dir)
|
||||||
close(secondAcquired)
|
close(secondAcquired)
|
||||||
unlock()
|
unlock()
|
||||||
}()
|
}()
|
||||||
@@ -338,12 +338,12 @@ func TestAcquireCloneLock(t *testing.T) {
|
|||||||
dirA := t.TempDir()
|
dirA := t.TempDir()
|
||||||
dirB := t.TempDir()
|
dirB := t.TempDir()
|
||||||
|
|
||||||
unlockA := acquireCloneLock(dirA)
|
unlockA := AcquireCloneLock(dirA)
|
||||||
defer unlockA()
|
defer unlockA()
|
||||||
|
|
||||||
done := make(chan struct{})
|
done := make(chan struct{})
|
||||||
go func() {
|
go func() {
|
||||||
unlock := acquireCloneLock(dirB)
|
unlock := AcquireCloneLock(dirB)
|
||||||
unlock()
|
unlock()
|
||||||
close(done)
|
close(done)
|
||||||
}()
|
}()
|
||||||
|
|||||||
@@ -25,9 +25,9 @@ func NewDockerBuildExecutor(input NewDockerBuildExecutorInput) common.Executor {
|
|||||||
return func(ctx context.Context) error {
|
return func(ctx context.Context) error {
|
||||||
logger := common.Logger(ctx)
|
logger := common.Logger(ctx)
|
||||||
if input.Platform != "" {
|
if input.Platform != "" {
|
||||||
logger.Infof("%sdocker build -t %s --platform %s %s", logPrefix, input.ImageTag, input.Platform, input.ContextDir)
|
logger.Infof("docker build -t %s --platform %s %s", input.ImageTag, input.Platform, input.ContextDir)
|
||||||
} else {
|
} else {
|
||||||
logger.Infof("%sdocker build -t %s %s", logPrefix, input.ImageTag, input.ContextDir)
|
logger.Infof("docker build -t %s %s", input.ImageTag, input.ContextDir)
|
||||||
}
|
}
|
||||||
if common.Dryrun(ctx) {
|
if common.Dryrun(ctx) {
|
||||||
return nil
|
return nil
|
||||||
|
|||||||
@@ -26,8 +26,6 @@ type dockerMessage struct {
|
|||||||
Progress string `json:"progress"`
|
Progress string `json:"progress"`
|
||||||
}
|
}
|
||||||
|
|
||||||
const logPrefix = " \U0001F433 "
|
|
||||||
|
|
||||||
func logDockerResponse(logger logrus.FieldLogger, dockerResponse io.ReadCloser, isError bool) error {
|
func logDockerResponse(logger logrus.FieldLogger, dockerResponse io.ReadCloser, isError bool) error {
|
||||||
if dockerResponse == nil {
|
if dockerResponse == nil {
|
||||||
return nil
|
return nil
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ import (
|
|||||||
func NewDockerPullExecutor(input NewDockerPullExecutorInput) common.Executor {
|
func NewDockerPullExecutor(input NewDockerPullExecutorInput) common.Executor {
|
||||||
return func(ctx context.Context) error {
|
return func(ctx context.Context) error {
|
||||||
logger := common.Logger(ctx)
|
logger := common.Logger(ctx)
|
||||||
logger.Debugf("%sdocker pull %v", logPrefix, input.Image)
|
logger.Debugf("docker pull %v", input.Image)
|
||||||
|
|
||||||
if common.Dryrun(ctx) {
|
if common.Dryrun(ctx) {
|
||||||
return nil
|
return nil
|
||||||
|
|||||||
@@ -53,7 +53,7 @@ func NewContainer(input *NewContainerInput) ExecutionsEnvironment {
|
|||||||
|
|
||||||
func (cr *containerReference) ConnectToNetwork(name string) common.Executor {
|
func (cr *containerReference) ConnectToNetwork(name string) common.Executor {
|
||||||
return common.
|
return common.
|
||||||
NewDebugExecutor("%sdocker network connect %s %s", logPrefix, name, cr.input.Name).
|
NewDebugExecutor("docker network connect %s %s", name, cr.input.Name).
|
||||||
Then(
|
Then(
|
||||||
common.NewPipelineExecutor(
|
common.NewPipelineExecutor(
|
||||||
cr.connect(),
|
cr.connect(),
|
||||||
@@ -90,7 +90,7 @@ func supportsContainerImagePlatform(ctx context.Context, cli client.APIClient) b
|
|||||||
|
|
||||||
func (cr *containerReference) Create(capAdd, capDrop []string) common.Executor {
|
func (cr *containerReference) Create(capAdd, capDrop []string) common.Executor {
|
||||||
return common.
|
return common.
|
||||||
NewInfoExecutor("%sdocker create image=%s platform=%s entrypoint=%+q cmd=%+q network=%+q", logPrefix, cr.input.Image, cr.input.Platform, cr.input.Entrypoint, cr.input.Cmd, cr.input.NetworkMode).
|
NewInfoExecutor("docker create image=%s platform=%s entrypoint=%+q cmd=%+q network=%+q", cr.input.Image, cr.input.Platform, cr.input.Entrypoint, cr.input.Cmd, cr.input.NetworkMode).
|
||||||
Then(
|
Then(
|
||||||
common.NewPipelineExecutor(
|
common.NewPipelineExecutor(
|
||||||
cr.connect(),
|
cr.connect(),
|
||||||
@@ -102,7 +102,7 @@ func (cr *containerReference) Create(capAdd, capDrop []string) common.Executor {
|
|||||||
|
|
||||||
func (cr *containerReference) Start(attach bool) common.Executor {
|
func (cr *containerReference) Start(attach bool) common.Executor {
|
||||||
return common.
|
return common.
|
||||||
NewInfoExecutor("%sdocker run image=%s platform=%s entrypoint=%+q cmd=%+q network=%+q", logPrefix, cr.input.Image, cr.input.Platform, cr.input.Entrypoint, cr.input.Cmd, cr.input.NetworkMode).
|
NewInfoExecutor("docker run image=%s platform=%s entrypoint=%+q cmd=%+q network=%+q", cr.input.Image, cr.input.Platform, cr.input.Entrypoint, cr.input.Cmd, cr.input.NetworkMode).
|
||||||
Then(
|
Then(
|
||||||
common.NewPipelineExecutor(
|
common.NewPipelineExecutor(
|
||||||
cr.connect(),
|
cr.connect(),
|
||||||
@@ -125,7 +125,7 @@ func (cr *containerReference) Start(attach bool) common.Executor {
|
|||||||
|
|
||||||
func (cr *containerReference) Pull(forcePull bool) common.Executor {
|
func (cr *containerReference) Pull(forcePull bool) common.Executor {
|
||||||
return common.
|
return common.
|
||||||
NewInfoExecutor("%sdocker pull image=%s platform=%s username=%s forcePull=%t", logPrefix, cr.input.Image, cr.input.Platform, cr.input.Username, forcePull).
|
NewInfoExecutor("docker pull image=%s platform=%s username=%s forcePull=%t", cr.input.Image, cr.input.Platform, cr.input.Username, forcePull).
|
||||||
Then(
|
Then(
|
||||||
NewDockerPullExecutor(NewDockerPullExecutorInput{
|
NewDockerPullExecutor(NewDockerPullExecutorInput{
|
||||||
Image: cr.input.Image,
|
Image: cr.input.Image,
|
||||||
@@ -147,7 +147,7 @@ func (cr *containerReference) Copy(destPath string, files ...*FileEntry) common.
|
|||||||
|
|
||||||
func (cr *containerReference) CopyDir(destPath, srcPath string, useGitIgnore bool) common.Executor {
|
func (cr *containerReference) CopyDir(destPath, srcPath string, useGitIgnore bool) common.Executor {
|
||||||
return common.NewPipelineExecutor(
|
return common.NewPipelineExecutor(
|
||||||
common.NewInfoExecutor("%sdocker cp src=%s dst=%s", logPrefix, srcPath, destPath),
|
common.NewInfoExecutor("docker cp src=%s dst=%s", srcPath, destPath),
|
||||||
cr.copyDir(destPath, srcPath, useGitIgnore),
|
cr.copyDir(destPath, srcPath, useGitIgnore),
|
||||||
func(ctx context.Context) error {
|
func(ctx context.Context) error {
|
||||||
// If this fails, then folders have wrong permissions on non root container
|
// If this fails, then folders have wrong permissions on non root container
|
||||||
@@ -177,7 +177,7 @@ func (cr *containerReference) UpdateFromImageEnv(env *map[string]string) common.
|
|||||||
|
|
||||||
func (cr *containerReference) Exec(command []string, env map[string]string, user, workdir string) common.Executor {
|
func (cr *containerReference) Exec(command []string, env map[string]string, user, workdir string) common.Executor {
|
||||||
return common.NewPipelineExecutor(
|
return common.NewPipelineExecutor(
|
||||||
common.NewInfoExecutor("%sdocker exec cmd=[%s] user=%s workdir=%s", logPrefix, strings.Join(command, " "), user, workdir),
|
common.NewInfoExecutor("docker exec cmd=[%s] user=%s workdir=%s", strings.Join(command, " "), user, workdir),
|
||||||
cr.connect(),
|
cr.connect(),
|
||||||
cr.find(),
|
cr.find(),
|
||||||
cr.exec(command, env, user, workdir),
|
cr.exec(command, env, user, workdir),
|
||||||
|
|||||||
@@ -42,7 +42,7 @@ func NewDockerVolumeRemoveExecutor(volumeName string, force bool) common.Executo
|
|||||||
func removeExecutor(volume string, force bool) common.Executor {
|
func removeExecutor(volume string, force bool) common.Executor {
|
||||||
return func(ctx context.Context) error {
|
return func(ctx context.Context) error {
|
||||||
logger := common.Logger(ctx)
|
logger := common.Logger(ctx)
|
||||||
logger.Debugf("%sdocker volume rm %s", logPrefix, volume)
|
logger.Debugf("docker volume rm %s", volume)
|
||||||
|
|
||||||
if common.Dryrun(ctx) {
|
if common.Dryrun(ctx) {
|
||||||
return nil
|
return nil
|
||||||
|
|||||||
@@ -73,10 +73,16 @@ func (cc *CopyCollector) WriteFile(fpath string, fi fs.FileInfo, linkName string
|
|||||||
if err := os.MkdirAll(filepath.Dir(fdestpath), 0o777); err != nil {
|
if err := os.MkdirAll(filepath.Dir(fdestpath), 0o777); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
// Remove any existing destination so we can overwrite read-only files
|
||||||
|
// (e.g. git pack files at mode 0444 trip EACCES on macOS and "Access is
|
||||||
|
// denied" on Windows when reopened with O_WRONLY) and so os.Symlink does
|
||||||
|
// not fail with EEXIST. os.Remove clears the Windows read-only attribute
|
||||||
|
// internally; on Unix unlink only needs write permission on the parent.
|
||||||
|
_ = os.Remove(fdestpath)
|
||||||
if f == nil {
|
if f == nil {
|
||||||
return os.Symlink(linkName, fdestpath)
|
return os.Symlink(linkName, fdestpath)
|
||||||
}
|
}
|
||||||
df, err := os.OpenFile(fdestpath, os.O_CREATE|os.O_WRONLY, fi.Mode())
|
df, err := os.OpenFile(fdestpath, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, fi.Mode())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,7 +8,9 @@ import (
|
|||||||
"archive/tar"
|
"archive/tar"
|
||||||
"context"
|
"context"
|
||||||
"io"
|
"io"
|
||||||
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
"runtime"
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
@@ -20,6 +22,7 @@ import (
|
|||||||
"github.com/go-git/go-git/v5/plumbing/format/index"
|
"github.com/go-git/go-git/v5/plumbing/format/index"
|
||||||
"github.com/go-git/go-git/v5/storage/filesystem"
|
"github.com/go-git/go-git/v5/storage/filesystem"
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
)
|
)
|
||||||
|
|
||||||
type memoryFs struct {
|
type memoryFs struct {
|
||||||
@@ -174,3 +177,47 @@ func TestSymlinks(t *testing.T) {
|
|||||||
assert.Equal(t, ".env", files["test.env"].Linkname)
|
assert.Equal(t, ".env", files["test.env"].Linkname)
|
||||||
assert.ErrorIs(t, err, io.EOF, "tar must be read cleanly to EOF")
|
assert.ErrorIs(t, err, io.EOF, "tar must be read cleanly to EOF")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Regression for https://gitea.com/gitea/runner/issues/876 and /941:
|
||||||
|
// re-copying an action directory must overwrite a pre-existing read-only
|
||||||
|
// file (e.g. a git pack .idx at mode 0444) instead of failing with EACCES
|
||||||
|
// on macOS or "Access is denied" on Windows.
|
||||||
|
func TestCopyCollectorWriteFileOverwritesReadOnlyFile(t *testing.T) {
|
||||||
|
dst := t.TempDir()
|
||||||
|
target := filepath.Join(dst, "sub", "pack.idx")
|
||||||
|
require.NoError(t, os.MkdirAll(filepath.Dir(target), 0o755))
|
||||||
|
require.NoError(t, os.WriteFile(target, []byte("old"), 0o444))
|
||||||
|
|
||||||
|
src := filepath.Join(t.TempDir(), "pack.idx")
|
||||||
|
require.NoError(t, os.WriteFile(src, []byte("new"), 0o444))
|
||||||
|
fi, err := os.Stat(src)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
cc := &CopyCollector{DstDir: dst}
|
||||||
|
require.NoError(t, cc.WriteFile("sub/pack.idx", fi, "", strings.NewReader("new")))
|
||||||
|
|
||||||
|
got, err := os.ReadFile(target)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, "new", string(got))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Without the destination removal, os.Symlink fails with EEXIST when the
|
||||||
|
// path already holds a regular file from an earlier copy of the action.
|
||||||
|
func TestCopyCollectorWriteFileOverwritesFileWithSymlink(t *testing.T) {
|
||||||
|
if runtime.GOOS == "windows" {
|
||||||
|
t.Skip("creating symlinks requires elevated privileges on Windows")
|
||||||
|
}
|
||||||
|
dst := t.TempDir()
|
||||||
|
target := filepath.Join(dst, "link")
|
||||||
|
require.NoError(t, os.WriteFile(target, []byte("stale"), 0o644))
|
||||||
|
|
||||||
|
fi, err := os.Lstat(target)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
cc := &CopyCollector{DstDir: dst}
|
||||||
|
require.NoError(t, cc.WriteFile("link", fi, "target", nil))
|
||||||
|
|
||||||
|
resolved, err := os.Readlink(target)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, "target", resolved)
|
||||||
|
}
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"gitea.com/gitea/runner/act/common"
|
"gitea.com/gitea/runner/act/common"
|
||||||
|
"gitea.com/gitea/runner/act/common/git"
|
||||||
"gitea.com/gitea/runner/act/container"
|
"gitea.com/gitea/runner/act/container"
|
||||||
"gitea.com/gitea/runner/act/model"
|
"gitea.com/gitea/runner/act/model"
|
||||||
|
|
||||||
@@ -44,6 +45,11 @@ type runAction func(step actionStep, actionDir string, remoteAction *remoteActio
|
|||||||
//go:embed res/trampoline.js
|
//go:embed res/trampoline.js
|
||||||
var trampoline embed.FS
|
var trampoline embed.FS
|
||||||
|
|
||||||
|
var (
|
||||||
|
ContainerImageExistsLocally = container.ImageExistsLocally
|
||||||
|
ContainerNewDockerBuildExecutor = container.NewDockerBuildExecutor
|
||||||
|
)
|
||||||
|
|
||||||
func readActionImpl(ctx context.Context, step *model.Step, actionDir, actionPath string, readFile actionYamlReader, writeFile fileWriter) (*model.Action, error) {
|
func readActionImpl(ctx context.Context, step *model.Step, actionDir, actionPath string, readFile actionYamlReader, writeFile fileWriter) (*model.Action, error) {
|
||||||
logger := common.Logger(ctx)
|
logger := common.Logger(ctx)
|
||||||
allErrors := []error{}
|
allErrors := []error{}
|
||||||
@@ -148,6 +154,8 @@ func maybeCopyToActionDir(ctx context.Context, step actionStep, actionDir, actio
|
|||||||
return rc.JobContainer.CopyTarStream(ctx, containerActionDirCopy, ta)
|
return rc.JobContainer.CopyTarStream(ctx, containerActionDirCopy, ta)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
defer git.AcquireCloneLock(actionDir)()
|
||||||
|
|
||||||
if err := removeGitIgnore(ctx, actionDir); err != nil {
|
if err := removeGitIgnore(ctx, actionDir); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -197,7 +205,7 @@ func runActionImpl(step actionStep, actionDir string, remoteAction *remoteAction
|
|||||||
if remoteAction == nil {
|
if remoteAction == nil {
|
||||||
location = containerActionDir
|
location = containerActionDir
|
||||||
}
|
}
|
||||||
return execAsDocker(ctx, step, actionName, location, remoteAction == nil)
|
return execAsDocker(ctx, step, actionName, actionDir, location, remoteAction == nil)
|
||||||
case x.IsComposite():
|
case x.IsComposite():
|
||||||
if err := maybeCopyToActionDir(ctx, step, actionDir, actionPath, containerActionDir); err != nil {
|
if err := maybeCopyToActionDir(ctx, step, actionDir, actionPath, containerActionDir); err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -265,7 +273,7 @@ func removeGitIgnore(ctx context.Context, directory string) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// TODO: break out parts of function to reduce complexicity
|
// TODO: break out parts of function to reduce complexicity
|
||||||
func execAsDocker(ctx context.Context, step actionStep, actionName, basedir string, localAction bool) error {
|
func execAsDocker(ctx context.Context, step actionStep, actionName, actionDir, basedir string, localAction bool) error {
|
||||||
logger := common.Logger(ctx)
|
logger := common.Logger(ctx)
|
||||||
rc := step.getRunContext()
|
rc := step.getRunContext()
|
||||||
action := step.getActionModel()
|
action := step.getActionModel()
|
||||||
@@ -284,12 +292,12 @@ func execAsDocker(ctx context.Context, step actionStep, actionName, basedir stri
|
|||||||
image = strings.ToLower(image)
|
image = strings.ToLower(image)
|
||||||
contextDir, fileName := filepath.Split(filepath.Join(basedir, action.Runs.Image))
|
contextDir, fileName := filepath.Split(filepath.Join(basedir, action.Runs.Image))
|
||||||
|
|
||||||
anyArchExists, err := container.ImageExistsLocally(ctx, image, "any")
|
anyArchExists, err := ContainerImageExistsLocally(ctx, image, "any")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
correctArchExists, err := container.ImageExistsLocally(ctx, image, rc.Config.ContainerArchitecture)
|
correctArchExists, err := ContainerImageExistsLocally(ctx, image, rc.Config.ContainerArchitecture)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -321,13 +329,21 @@ func execAsDocker(ctx context.Context, step actionStep, actionName, basedir stri
|
|||||||
}
|
}
|
||||||
defer buildContext.Close()
|
defer buildContext.Close()
|
||||||
}
|
}
|
||||||
prepImage = container.NewDockerBuildExecutor(container.NewDockerBuildExecutorInput{
|
prepImage = ContainerNewDockerBuildExecutor(container.NewDockerBuildExecutorInput{
|
||||||
ContextDir: contextDir,
|
ContextDir: contextDir,
|
||||||
Dockerfile: fileName,
|
Dockerfile: fileName,
|
||||||
ImageTag: image,
|
ImageTag: image,
|
||||||
BuildContext: buildContext,
|
BuildContext: buildContext,
|
||||||
Platform: rc.Config.ContainerArchitecture,
|
Platform: rc.Config.ContainerArchitecture,
|
||||||
})
|
})
|
||||||
|
if buildContext == nil {
|
||||||
|
// Held across the whole build: the daemon drains contextDir lazily.
|
||||||
|
inner := prepImage
|
||||||
|
prepImage = func(ctx context.Context) error {
|
||||||
|
defer git.AcquireCloneLock(actionDir)()
|
||||||
|
return inner(ctx)
|
||||||
|
}
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
logger.Debugf("image '%s' for architecture '%s' already exists", image, rc.Config.ContainerArchitecture)
|
logger.Debugf("image '%s' for architecture '%s' already exists", image, rc.Config.ContainerArchitecture)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,8 +9,13 @@ import (
|
|||||||
"io"
|
"io"
|
||||||
"io/fs"
|
"io/fs"
|
||||||
"strings"
|
"strings"
|
||||||
|
"sync"
|
||||||
"testing"
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"gitea.com/gitea/runner/act/common"
|
||||||
|
"gitea.com/gitea/runner/act/common/git"
|
||||||
|
"gitea.com/gitea/runner/act/container"
|
||||||
"gitea.com/gitea/runner/act/model"
|
"gitea.com/gitea/runner/act/model"
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
@@ -252,3 +257,153 @@ func TestActionRunner(t *testing.T) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestMaybeCopyToActionDirHoldsCloneLock(t *testing.T) {
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
|
actionDir := t.TempDir()
|
||||||
|
|
||||||
|
releaseCopy := make(chan struct{})
|
||||||
|
release := sync.OnceFunc(func() { close(releaseCopy) })
|
||||||
|
defer release()
|
||||||
|
|
||||||
|
copyEntered := make(chan struct{})
|
||||||
|
|
||||||
|
cm := &containerMock{}
|
||||||
|
cm.On("CopyDir", "/var/run/act/actions/", actionDir+"/", false).Return(func(ctx context.Context) error {
|
||||||
|
close(copyEntered)
|
||||||
|
<-releaseCopy
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
|
||||||
|
step := &stepActionRemote{
|
||||||
|
Step: &model.Step{Uses: "remote/action@v1"},
|
||||||
|
RunContext: &RunContext{
|
||||||
|
Config: &Config{},
|
||||||
|
JobContainer: cm,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
copyDone := make(chan error, 1)
|
||||||
|
go func() {
|
||||||
|
copyDone <- maybeCopyToActionDir(ctx, step, actionDir, "", "/var/run/act/actions/")
|
||||||
|
}()
|
||||||
|
|
||||||
|
select {
|
||||||
|
case <-copyEntered:
|
||||||
|
case err := <-copyDone:
|
||||||
|
t.Fatalf("maybeCopyToActionDir returned before CopyDir was entered: %v", err)
|
||||||
|
case <-time.After(time.Second):
|
||||||
|
t.Fatal("CopyDir was not entered within 1 second")
|
||||||
|
}
|
||||||
|
|
||||||
|
peerAcquired := make(chan struct{})
|
||||||
|
go func() {
|
||||||
|
unlock := git.AcquireCloneLock(actionDir)
|
||||||
|
close(peerAcquired)
|
||||||
|
unlock()
|
||||||
|
}()
|
||||||
|
|
||||||
|
select {
|
||||||
|
case <-peerAcquired:
|
||||||
|
t.Fatal("peer AcquireCloneLock returned while CopyDir was running")
|
||||||
|
case <-time.After(50 * time.Millisecond):
|
||||||
|
}
|
||||||
|
|
||||||
|
release()
|
||||||
|
|
||||||
|
select {
|
||||||
|
case err := <-copyDone:
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("maybeCopyToActionDir returned error: %v", err)
|
||||||
|
}
|
||||||
|
case <-time.After(time.Second):
|
||||||
|
t.Fatal("maybeCopyToActionDir did not return after CopyDir was unblocked")
|
||||||
|
}
|
||||||
|
|
||||||
|
select {
|
||||||
|
case <-peerAcquired:
|
||||||
|
case <-time.After(time.Second):
|
||||||
|
t.Fatal("peer AcquireCloneLock did not proceed after lock released")
|
||||||
|
}
|
||||||
|
|
||||||
|
cm.AssertExpectations(t)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestExecAsDockerHoldsCloneLockForRemoteUncached(t *testing.T) {
|
||||||
|
actionDir := t.TempDir()
|
||||||
|
|
||||||
|
unlockOnce := sync.OnceFunc(git.AcquireCloneLock(actionDir))
|
||||||
|
defer unlockOnce()
|
||||||
|
|
||||||
|
innerEntered := make(chan struct{})
|
||||||
|
releaseInner := make(chan struct{})
|
||||||
|
releaseOnce := sync.OnceFunc(func() { close(releaseInner) })
|
||||||
|
defer releaseOnce()
|
||||||
|
|
||||||
|
origImageExists := ContainerImageExistsLocally
|
||||||
|
ContainerImageExistsLocally = func(_ context.Context, _, _ string) (bool, error) {
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
defer func() { ContainerImageExistsLocally = origImageExists }()
|
||||||
|
|
||||||
|
origBuildExec := ContainerNewDockerBuildExecutor
|
||||||
|
ContainerNewDockerBuildExecutor = func(_ container.NewDockerBuildExecutorInput) common.Executor {
|
||||||
|
return func(_ context.Context) error {
|
||||||
|
close(innerEntered)
|
||||||
|
<-releaseInner
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
defer func() { ContainerNewDockerBuildExecutor = origBuildExec }()
|
||||||
|
|
||||||
|
step := &stepActionRemote{
|
||||||
|
Step: &model.Step{ID: "1", Uses: "remote/action@v1", With: map[string]string{}},
|
||||||
|
RunContext: &RunContext{
|
||||||
|
Config: &Config{},
|
||||||
|
Run: &model.Run{
|
||||||
|
JobID: "1",
|
||||||
|
Workflow: &model.Workflow{
|
||||||
|
Name: "wf",
|
||||||
|
Jobs: map[string]*model.Job{"1": {}},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
JobContainer: &containerMock{},
|
||||||
|
},
|
||||||
|
action: &model.Action{Runs: model.ActionRuns{Using: "docker", Image: "Dockerfile"}},
|
||||||
|
env: map[string]string{},
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
done := make(chan error, 1)
|
||||||
|
go func() { done <- execAsDocker(ctx, step, "test-action", actionDir, actionDir, false) }()
|
||||||
|
|
||||||
|
select {
|
||||||
|
case <-innerEntered:
|
||||||
|
t.Fatal("inner build executor ran before clone lock was released")
|
||||||
|
case err := <-done:
|
||||||
|
t.Fatalf("execAsDocker returned before inner was entered: %v", err)
|
||||||
|
case <-time.After(50 * time.Millisecond):
|
||||||
|
}
|
||||||
|
|
||||||
|
unlockOnce()
|
||||||
|
|
||||||
|
select {
|
||||||
|
case <-innerEntered:
|
||||||
|
case err := <-done:
|
||||||
|
t.Fatalf("execAsDocker returned without entering inner: %v", err)
|
||||||
|
case <-time.After(time.Second):
|
||||||
|
t.Fatal("inner build executor not entered after lock released")
|
||||||
|
}
|
||||||
|
|
||||||
|
cancel()
|
||||||
|
releaseOnce()
|
||||||
|
|
||||||
|
select {
|
||||||
|
case <-done:
|
||||||
|
case <-time.After(time.Second):
|
||||||
|
t.Fatal("execAsDocker did not return after inner was released and ctx was canceled")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -120,7 +120,7 @@ func (rc *RunContext) setOutput(ctx context.Context, kvPairs map[string]string,
|
|||||||
|
|
||||||
result, ok := rc.StepResults[stepID]
|
result, ok := rc.StepResults[stepID]
|
||||||
if !ok {
|
if !ok {
|
||||||
logger.Infof(" \U00002757 no outputs used step '%s'", stepID)
|
logger.Infof("No outputs registered for step '%s'", stepID)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -142,9 +142,16 @@ func cloneRemoteReusableWorkflow(rc *RunContext, cloneURL, ref, targetDirectory,
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var modelNewWorkflowPlanner = model.NewWorkflowPlanner
|
||||||
|
|
||||||
func newReusableWorkflowExecutor(rc *RunContext, directory, workflow string) common.Executor {
|
func newReusableWorkflowExecutor(rc *RunContext, directory, workflow string) common.Executor {
|
||||||
return func(ctx context.Context) error {
|
return func(ctx context.Context) error {
|
||||||
planner, err := model.NewWorkflowPlanner(path.Join(directory, workflow), true)
|
// Scoped to the yaml read so concurrent invocations don't serialize
|
||||||
|
// on the whole job run.
|
||||||
|
planner, err := func() (model.WorkflowPlanner, error) {
|
||||||
|
defer git.AcquireCloneLock(directory)()
|
||||||
|
return modelNewWorkflowPlanner(path.Join(directory, workflow), true)
|
||||||
|
}()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -278,7 +285,7 @@ func setReusedWorkflowCallerResult(rc *RunContext, runner Runner) common.Executo
|
|||||||
rc.caller.setReusedWorkflowJobResult(rc.JobName, reusedWorkflowJobResult)
|
rc.caller.setReusedWorkflowJobResult(rc.JobName, reusedWorkflowJobResult)
|
||||||
} else {
|
} else {
|
||||||
rc.result(reusedWorkflowJobResult)
|
rc.result(reusedWorkflowJobResult)
|
||||||
logger.WithField("jobResult", reusedWorkflowJobResult).Infof("\U0001F3C1 Job %s", reusedWorkflowJobResultMessage)
|
logger.WithField("jobResult", reusedWorkflowJobResult).Infof("Job %s", reusedWorkflowJobResultMessage)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -5,11 +5,15 @@ package runner
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"errors"
|
||||||
"os"
|
"os"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
"sync"
|
||||||
"testing"
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"gitea.com/gitea/runner/act/common/git"
|
||||||
"gitea.com/gitea/runner/act/model"
|
"gitea.com/gitea/runner/act/model"
|
||||||
|
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
@@ -71,6 +75,54 @@ func TestReusableWorkflowCachedBranchRefRefreshes(t *testing.T) {
|
|||||||
require.Equal(t, tmpl("v2"), string(got), "cached workflow file must reflect the updated branch tip")
|
require.Equal(t, tmpl("v2"), string(got), "cached workflow file must reflect the updated branch tip")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestNewReusableWorkflowExecutorHoldsCloneLock(t *testing.T) {
|
||||||
|
workflowDir := t.TempDir()
|
||||||
|
|
||||||
|
unlockOnce := sync.OnceFunc(git.AcquireCloneLock(workflowDir))
|
||||||
|
defer unlockOnce()
|
||||||
|
|
||||||
|
plannerCalled := make(chan struct{})
|
||||||
|
|
||||||
|
origPlanner := modelNewWorkflowPlanner
|
||||||
|
modelNewWorkflowPlanner = func(string, bool) (model.WorkflowPlanner, error) {
|
||||||
|
close(plannerCalled)
|
||||||
|
return nil, errors.New("stop")
|
||||||
|
}
|
||||||
|
defer func() { modelNewWorkflowPlanner = origPlanner }()
|
||||||
|
|
||||||
|
rc := &RunContext{
|
||||||
|
Config: &Config{},
|
||||||
|
Run: &model.Run{Workflow: &model.Workflow{Jobs: map[string]*model.Job{}}},
|
||||||
|
}
|
||||||
|
exec := newReusableWorkflowExecutor(rc, workflowDir, "reusable.yml")
|
||||||
|
|
||||||
|
done := make(chan error, 1)
|
||||||
|
go func() { done <- exec(context.Background()) }()
|
||||||
|
|
||||||
|
select {
|
||||||
|
case <-plannerCalled:
|
||||||
|
t.Fatal("planner ran while clone lock was held")
|
||||||
|
case err := <-done:
|
||||||
|
t.Fatalf("executor returned before planner was reached: %v", err)
|
||||||
|
case <-time.After(50 * time.Millisecond):
|
||||||
|
}
|
||||||
|
|
||||||
|
unlockOnce()
|
||||||
|
|
||||||
|
select {
|
||||||
|
case <-plannerCalled:
|
||||||
|
case <-time.After(time.Second):
|
||||||
|
t.Fatal("planner not called after lock was released")
|
||||||
|
}
|
||||||
|
|
||||||
|
select {
|
||||||
|
case err := <-done:
|
||||||
|
require.Error(t, err)
|
||||||
|
case <-time.After(time.Second):
|
||||||
|
t.Fatal("executor did not return after planner ran")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func gitMust(t *testing.T, dir string, args ...string) {
|
func gitMust(t *testing.T, dir string, args ...string) {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
cmd := exec.Command("git", args...)
|
cmd := exec.Command("git", args...)
|
||||||
|
|||||||
@@ -193,7 +193,7 @@ func (rc *RunContext) GetBindsAndMounts() ([]string, map[string]string) {
|
|||||||
func (rc *RunContext) startHostEnvironment() common.Executor {
|
func (rc *RunContext) startHostEnvironment() common.Executor {
|
||||||
return func(ctx context.Context) error {
|
return func(ctx context.Context) error {
|
||||||
logger := common.Logger(ctx)
|
logger := common.Logger(ctx)
|
||||||
rawLogger := logger.WithField("raw_output", true)
|
rawLogger := logger.WithField(rawOutputField, true)
|
||||||
logWriter := common.NewLineWriter(rc.commandHandler(ctx), func(s string) bool {
|
logWriter := common.NewLineWriter(rc.commandHandler(ctx), func(s string) bool {
|
||||||
if rc.Config.LogOutput {
|
if rc.Config.LogOutput {
|
||||||
rawLogger.Infof("%s", s)
|
rawLogger.Infof("%s", s)
|
||||||
@@ -260,11 +260,24 @@ func (rc *RunContext) startHostEnvironment() common.Executor {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// printStartJobContainerGroup mirrors actions/runner's "Starting job container"
|
||||||
|
// section: emit the group header and summary, return a closer for ::endgroup::.
|
||||||
|
func printStartJobContainerGroup(ctx context.Context, image, name, network string) func() {
|
||||||
|
rawLogger := common.Logger(ctx).WithField(rawOutputField, true)
|
||||||
|
rawLogger.Infof("::group::Starting job container")
|
||||||
|
rawLogger.Infof("image: %s", image)
|
||||||
|
rawLogger.Infof("name: %s", name)
|
||||||
|
rawLogger.Infof("network: %s", network)
|
||||||
|
return func() {
|
||||||
|
rawLogger.Infof("::endgroup::")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func (rc *RunContext) startJobContainer() common.Executor {
|
func (rc *RunContext) startJobContainer() common.Executor {
|
||||||
return func(ctx context.Context) error {
|
return func(ctx context.Context) error {
|
||||||
logger := common.Logger(ctx)
|
logger := common.Logger(ctx)
|
||||||
image := rc.platformImage(ctx)
|
image := rc.platformImage(ctx)
|
||||||
rawLogger := logger.WithField("raw_output", true)
|
rawLogger := logger.WithField(rawOutputField, true)
|
||||||
logWriter := common.NewLineWriter(rc.commandHandler(ctx), func(s string) bool {
|
logWriter := common.NewLineWriter(rc.commandHandler(ctx), func(s string) bool {
|
||||||
if rc.Config.LogOutput {
|
if rc.Config.LogOutput {
|
||||||
rawLogger.Infof("%s", s)
|
rawLogger.Infof("%s", s)
|
||||||
@@ -279,7 +292,6 @@ func (rc *RunContext) startJobContainer() common.Executor {
|
|||||||
return fmt.Errorf("failed to handle credentials: %s", err)
|
return fmt.Errorf("failed to handle credentials: %s", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.Infof("\U0001f680 Start image=%s", image)
|
|
||||||
name := rc.jobContainerName()
|
name := rc.jobContainerName()
|
||||||
// For gitea, to support --volumes-from <container_name_or_id> in options.
|
// For gitea, to support --volumes-from <container_name_or_id> in options.
|
||||||
// We need to set the container name to the environment variable.
|
// We need to set the container name to the environment variable.
|
||||||
@@ -424,6 +436,7 @@ func (rc *RunContext) startJobContainer() common.Executor {
|
|||||||
return errors.New("Failed to create job container")
|
return errors.New("Failed to create job container")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
defer printStartJobContainerGroup(ctx, image, name, networkName)()
|
||||||
return common.NewPipelineExecutor(
|
return common.NewPipelineExecutor(
|
||||||
rc.pullServicesImages(rc.Config.ForcePull),
|
rc.pullServicesImages(rc.Config.ForcePull),
|
||||||
rc.JobContainer.Pull(rc.Config.ForcePull),
|
rc.JobContainer.Pull(rc.Config.ForcePull),
|
||||||
@@ -753,7 +766,7 @@ func (rc *RunContext) isEnabled(ctx context.Context) (bool, error) {
|
|||||||
img := rc.platformImage(ctx)
|
img := rc.platformImage(ctx)
|
||||||
if img == "" {
|
if img == "" {
|
||||||
for _, platformName := range rc.runsOnPlatformNames(ctx) {
|
for _, platformName := range rc.runsOnPlatformNames(ctx) {
|
||||||
l.Infof("\U0001F6A7 Skipping unsupported platform -- Try running with `-P %+v=...`", platformName)
|
l.Infof("Skipping unsupported platform -- Try running with `-P %+v=...`", platformName)
|
||||||
}
|
}
|
||||||
return false, nil
|
return false, nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,6 +5,7 @@
|
|||||||
package runner
|
package runner
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
@@ -12,6 +13,7 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
"gitea.com/gitea/runner/act/common"
|
||||||
"gitea.com/gitea/runner/act/exprparser"
|
"gitea.com/gitea/runner/act/exprparser"
|
||||||
"gitea.com/gitea/runner/act/model"
|
"gitea.com/gitea/runner/act/model"
|
||||||
|
|
||||||
@@ -635,3 +637,25 @@ func TestCreateContainerNameBoundedForLongMatrixInput(t *testing.T) {
|
|||||||
assert.LessOrEqual(t, len(name+"-network"), 255)
|
assert.LessOrEqual(t, len(name+"-network"), 255)
|
||||||
assert.LessOrEqual(t, len(name+"-job1234567890"), 255)
|
assert.LessOrEqual(t, len(name+"-job1234567890"), 255)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestPrintStartJobContainerGroupGolden(t *testing.T) {
|
||||||
|
buf := &bytes.Buffer{}
|
||||||
|
logger := log.New()
|
||||||
|
logger.SetOutput(buf)
|
||||||
|
logger.SetLevel(log.InfoLevel)
|
||||||
|
logger.SetFormatter(&jobLogFormatter{color: cyan})
|
||||||
|
entry := logger.WithFields(log.Fields{"job": "j1"})
|
||||||
|
ctx := common.WithLogger(context.Background(), entry)
|
||||||
|
|
||||||
|
printStartJobContainerGroup(ctx, "node:20", "GITEA-WORKFLOW-build-JOB-test", "gitea-runner-network")()
|
||||||
|
|
||||||
|
want := strings.Join([]string{
|
||||||
|
"[j1] | ::group::Starting job container",
|
||||||
|
"[j1] | image: node:20",
|
||||||
|
"[j1] | name: GITEA-WORKFLOW-build-JOB-test",
|
||||||
|
"[j1] | network: gitea-runner-network",
|
||||||
|
"[j1] | ::endgroup::",
|
||||||
|
"",
|
||||||
|
}, "\n")
|
||||||
|
assert.Equal(t, want, buf.String())
|
||||||
|
}
|
||||||
|
|||||||
@@ -145,6 +145,7 @@ func (sar *stepActionRemote) prepareActionExecutor() common.Executor {
|
|||||||
return common.NewPipelineExecutor(
|
return common.NewPipelineExecutor(
|
||||||
ntErr,
|
ntErr,
|
||||||
func(ctx context.Context) error {
|
func(ctx context.Context) error {
|
||||||
|
defer git.AcquireCloneLock(actionDir)()
|
||||||
actionModel, err := sar.readAction(ctx, sar.Step, actionDir, sar.remoteAction.Path, remoteReader(ctx), os.WriteFile)
|
actionModel, err := sar.readAction(ctx, sar.Step, actionDir, sar.remoteAction.Path, remoteReader(ctx), os.WriteFile)
|
||||||
sar.action = actionModel
|
sar.action = actionModel
|
||||||
return err
|
return err
|
||||||
|
|||||||
@@ -40,14 +40,21 @@
|
|||||||
|
|
||||||
### Running `gitea-runner` using Docker-in-Docker (DIND)
|
### Running `gitea-runner` using Docker-in-Docker (DIND)
|
||||||
|
|
||||||
|
- `privileged` has to be set to `true` because in-container Docker daemon requires a lot of kernel capabilities and file system mounts like `procfs` and `sysfs`
|
||||||
|
- `security_opt` sets the `apparmor` profile to `rootlesskit` for hosts running AppArmor (e.g. Ubuntu, Debian), where the kernel might otherwise block user namespace changes that Docker daemon requires for startup. The `rootlesskit` profile is provided by the `docker-ce-rootless-extras` package and is present on hosts where Docker was installed via the official installer or distro packages
|
||||||
|
|
||||||
```yml
|
```yml
|
||||||
...
|
...
|
||||||
runner:
|
runner:
|
||||||
image: gitea/runner:latest-dind-rootless
|
image: gitea/runner:latest-dind-rootless
|
||||||
restart: always
|
restart: always
|
||||||
privileged: true
|
privileged: true
|
||||||
|
security_opt:
|
||||||
|
- apparmor=rootlesskit
|
||||||
depends_on:
|
depends_on:
|
||||||
- gitea
|
gitea:
|
||||||
|
condition: service_healthy
|
||||||
|
restart: true
|
||||||
volumes:
|
volumes:
|
||||||
- ./data/runner:/data
|
- ./data/runner:/data
|
||||||
environment:
|
environment:
|
||||||
|
|||||||
Reference in New Issue
Block a user